Skip to content

Grouped and Stacked

Create multi-series bar charts to compare categories across groups or show composition of totals.

Mental Model

Grouped bars place multiple series side by side by offsetting their x-positions, while stacked bars pile series on top of each other using the bottom parameter. Grouped bars make it easy to compare values within a category; stacked bars make it easy to see each category's total and its composition.

Grouped vs Stacked = Different Questions

Grouped bars → "How do the components compare within each category?" Stacked bars → "What is each category's total, and how is it composed?"

Choose grouped when the reader needs to compare individual series values. Choose stacked when the total and its breakdown matter more than precise per-series comparison.

Stacked Bar Pitfall

Stacked bars make it hard to compare individual segments across categories — only the bottom segment shares a common baseline. If precise per-series comparison matters, use grouped bars instead.

Bar Chart Decision Guide

Data situation Use
Single series Basic bar
Multiple series, need comparison Grouped bar
Show composition of totals Stacked bar
Show proportions 100% stacked bar
Long category labels Horizontal bar

Limit to 2–4 series for readability. More than that creates clutter regardless of layout.

Grouped Bar Chart

Place bars side by side to compare multiple series.

1. Basic Grouped Bars

```python import matplotlib.pyplot as plt import numpy as np

categories = ['A', 'B', 'C', 'D', 'E'] series1 = [23, 45, 56, 78, 32] series2 = [28, 40, 62, 70, 38]

x = np.arange(len(categories)) width = 0.35

fig, ax = plt.subplots() ax.bar(x - width/2, series1, width, label='Series 1') ax.bar(x + width/2, series2, width, label='Series 2')

ax.set_xticks(x) ax.set_xticklabels(categories) ax.legend() plt.show() ```

2. Three Groups

```python series1 = [23, 45, 56, 78, 32] series2 = [28, 40, 62, 70, 38] series3 = [20, 35, 50, 65, 30]

x = np.arange(len(categories)) width = 0.25

fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(x - width, series1, width, label='2022') ax.bar(x, series2, width, label='2023') ax.bar(x + width, series3, width, label='2024')

ax.set_xticks(x) ax.set_xticklabels(categories) ax.legend() plt.show() ```

3. Dynamic Width Calculation

```python def grouped_bar(ax, data, labels, group_labels): n_groups = len(data) n_categories = len(data[0])

total_width = 0.8
bar_width = total_width / n_groups
x = np.arange(n_categories)

for i, (series, label) in enumerate(zip(data, labels)):
    offset = (i - n_groups/2 + 0.5) * bar_width
    ax.bar(x + offset, series, bar_width, label=label)

ax.set_xticks(x)
ax.set_xticklabels(group_labels)
ax.legend()

fig, ax = plt.subplots() data = [series1, series2, series3] grouped_bar(ax, data, ['2022', '2023', '2024'], categories) plt.show() ```

Pandas Grouped Bars

Use DataFrame's built-in plotting for automatic grouped bars.

1. DataFrame with Multiple Columns

```python import pandas as pd

data = {'Student': ['Brandon', 'Vanessa', 'Daniel', 'Kevin', 'William'], 'Midterm': [85, 60, 60, 65, 100], 'Final': [90, 90, 65, 80, 95]} df = pd.DataFrame(data).set_index('Student')

fig, ax = plt.subplots(figsize=(12, 3)) df.plot(kind='bar', ax=ax) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.show() ```

2. Matplotlib Equivalent

```python position = np.arange(df.shape[0]) student = df.index midterm = df.Midterm final = df.Final

fig, ax = plt.subplots(figsize=(12, 3)) width = 0.3 ax.bar(position - width/2, midterm, width=width, label='Midterm') ax.bar(position + width/2, final, width=width, label='Final') ax.set_xticks(position, student) ax.set_xlabel('Student') ax.legend() ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.show() ```

3. Horizontal Grouped with Matplotlib

python fig, ax = plt.subplots(figsize=(12, 3)) height = 0.3 ax.barh(position - height/2, midterm, height=height, label='Midterm') ax.barh(position + height/2, final, height=height, label='Final') ax.set_yticks(position, student) ax.set_ylabel('Student') ax.legend() ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.show()

Stacked Bar Chart

Stack bars vertically to show composition.

1. Basic Stacked Bars

```python categories = ['Q1', 'Q2', 'Q3', 'Q4'] product_a = [20, 25, 30, 35] product_b = [15, 20, 25, 20] product_c = [10, 15, 10, 15]

fig, ax = plt.subplots() ax.bar(categories, product_a, label='Product A') ax.bar(categories, product_b, bottom=product_a, label='Product B') ax.bar(categories, product_c, bottom=np.array(product_a) + np.array(product_b), label='Product C')

ax.legend() ax.set_ylabel('Sales') plt.show() ```

2. Using NumPy for Bottom

```python product_a = np.array([20, 25, 30, 35]) product_b = np.array([15, 20, 25, 20]) product_c = np.array([10, 15, 10, 15])

fig, ax = plt.subplots() ax.bar(categories, product_a, label='Product A') ax.bar(categories, product_b, bottom=product_a, label='Product B') ax.bar(categories, product_c, bottom=product_a + product_b, label='Product C')

ax.legend() plt.show() ```

3. Dynamic Stacking

```python def stacked_bar(ax, data, labels, categories): bottom = np.zeros(len(categories))

for series, label in zip(data, labels):
    ax.bar(categories, series, bottom=bottom, label=label)
    bottom += np.array(series)

ax.legend()

fig, ax = plt.subplots() data = [product_a, product_b, product_c] stacked_bar(ax, data, ['Product A', 'Product B', 'Product C'], categories) plt.show() ```

Horizontal Stacked Bars

Stack bars horizontally using barh.

1. Basic Horizontal Stack

```python categories = ['Team A', 'Team B', 'Team C', 'Team D'] wins = [15, 12, 18, 10] losses = [5, 8, 2, 10] draws = [2, 2, 2, 2]

fig, ax = plt.subplots() ax.barh(categories, wins, label='Wins') ax.barh(categories, losses, left=wins, label='Losses') ax.barh(categories, draws, left=np.array(wins) + np.array(losses), label='Draws')

ax.legend() ax.set_xlabel('Games') plt.show() ```

2. Diverging Stacked Bar

```python categories = ['Q1', 'Q2', 'Q3', 'Q4'] positive = [30, 40, 35, 45] negative = [-20, -15, -25, -10]

fig, ax = plt.subplots() ax.barh(categories, positive, label='Gains', color='green') ax.barh(categories, negative, label='Losses', color='red') ax.axvline(x=0, color='black', linewidth=0.8) ax.legend() plt.show() ```

3. Centered Diverging

```python survey = ['Strongly Disagree', 'Disagree', 'Neutral', 'Agree', 'Strongly Agree'] responses = [10, 20, 15, 35, 20]

colors = ['darkred', 'red', 'gray', 'green', 'darkgreen'] starts = [0, 0, 0, 0, 0]

Center on neutral

starts[0] = -responses[0] - responses[1] - responses[2]/2 starts[1] = -responses[1] - responses[2]/2 starts[2] = -responses[2]/2 starts[3] = responses[2]/2 starts[4] = responses[2]/2 + responses[3]

fig, ax = plt.subplots() for i, (response, start, color, label) in enumerate(zip(responses, starts, colors, survey)): ax.barh(['Survey'], response, left=start, color=color, label=label)

ax.axvline(x=0, color='black', linewidth=0.8) ax.legend(loc='lower right') plt.show() ```

Percentage Stacked Bars

Show proportions that sum to 100%.

1. Calculate Percentages

```python categories = ['2021', '2022', '2023', '2024'] cat_a = np.array([20, 25, 30, 35]) cat_b = np.array([15, 20, 25, 20]) cat_c = np.array([10, 15, 10, 15])

totals = cat_a + cat_b + cat_c pct_a = cat_a / totals * 100 pct_b = cat_b / totals * 100 pct_c = cat_c / totals * 100 ```

2. Create Percentage Stack

```python fig, ax = plt.subplots() ax.bar(categories, pct_a, label='Category A') ax.bar(categories, pct_b, bottom=pct_a, label='Category B') ax.bar(categories, pct_c, bottom=pct_a + pct_b, label='Category C')

ax.set_ylabel('Percentage (%)') ax.set_ylim(0, 100) ax.legend() plt.show() ```

3. Reusable Function

```python def percentage_stacked_bar(ax, data, labels, categories): data = np.array(data) totals = data.sum(axis=0) percentages = data / totals * 100

bottom = np.zeros(len(categories))
for pct, label in zip(percentages, labels):
    ax.bar(categories, pct, bottom=bottom, label=label)
    bottom += pct

ax.set_ylim(0, 100)
ax.set_ylabel('Percentage (%)')
ax.legend()

fig, ax = plt.subplots() percentage_stacked_bar(ax, [cat_a, cat_b, cat_c], ['A', 'B', 'C'], categories) plt.show() ```

Adding Labels to Grouped/Stacked

Annotate bars with values.

1. Grouped Bar Labels

```python x = np.arange(len(categories)) width = 0.35

fig, ax = plt.subplots() bars1 = ax.bar(x - width/2, series1, width, label='2023') bars2 = ax.bar(x + width/2, series2, width, label='2024')

ax.bar_label(bars1, padding=3) ax.bar_label(bars2, padding=3)

ax.set_xticks(x) ax.set_xticklabels(categories) ax.legend() plt.show() ```

2. Stacked Bar Labels

```python fig, ax = plt.subplots() bars1 = ax.bar(categories, product_a, label='Product A') bars2 = ax.bar(categories, product_b, bottom=product_a, label='Product B') bars3 = ax.bar(categories, product_c, bottom=product_a + product_b, label='Product C')

ax.bar_label(bars1, label_type='center') ax.bar_label(bars2, label_type='center') ax.bar_label(bars3, label_type='center')

ax.legend() plt.show() ```

3. Total Labels on Stack

```python fig, ax = plt.subplots() bars1 = ax.bar(categories, product_a, label='Product A') bars2 = ax.bar(categories, product_b, bottom=product_a, label='Product B') bars3 = ax.bar(categories, product_c, bottom=product_a + product_b, label='Product C')

totals = product_a + product_b + product_c for i, total in enumerate(totals): ax.text(i, total + 1, str(total), ha='center', fontweight='bold')

ax.legend() plt.show() ```

Styling Grouped and Stacked

Apply consistent styling across multi-series charts.

1. Color Schemes

```python colors = ['#2ecc71', '#3498db', '#9b59b6']

fig, ax = plt.subplots() ax.bar(categories, product_a, label='A', color=colors[0]) ax.bar(categories, product_b, bottom=product_a, label='B', color=colors[1]) ax.bar(categories, product_c, bottom=product_a + product_b, label='C', color=colors[2]) ax.legend() plt.show() ```

2. Edge and Hatch

python fig, ax = plt.subplots() ax.bar(categories, product_a, label='A', color='white', edgecolor='blue', hatch='//') ax.bar(categories, product_b, bottom=product_a, label='B', color='white', edgecolor='green', hatch='\\\\') ax.bar(categories, product_c, bottom=product_a + product_b, label='C', color='white', edgecolor='red', hatch='xx') ax.legend() plt.show()

3. Complete Example

```python fig, ax = plt.subplots(figsize=(10, 6))

x = np.arange(len(categories)) width = 0.25

bars1 = ax.bar(x - width, series1, width, label='2022', color='#3498db', edgecolor='navy') bars2 = ax.bar(x, series2, width, label='2023', color='#2ecc71', edgecolor='darkgreen') bars3 = ax.bar(x + width, series3, width, label='2024', color='#e74c3c', edgecolor='darkred')

ax.set_xticks(x) ax.set_xticklabels(categories) ax.legend(title='Year') ax.set_ylabel('Value') ax.set_title('Grouped Bar Chart Comparison') ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.grid(axis='y', alpha=0.3)

plt.tight_layout() plt.show() ```


Exercises

Exercise 1. Create a stacked bar chart showing monthly sales of three products over four months. Use months ['Jan', 'Feb', 'Mar', 'Apr'] and values: Product A [10, 15, 12, 18], Product B [8, 12, 15, 10], Product C [5, 8, 10, 12]. Add a legend and value labels inside each bar segment.

Solution to Exercise 1
import matplotlib.pyplot as plt
import numpy as np

months = ['Jan', 'Feb', 'Mar', 'Apr']
prod_a = [10, 15, 12, 18]
prod_b = [8, 12, 15, 10]
prod_c = [5, 8, 10, 12]

fig, ax = plt.subplots(figsize=(8, 6))
b1 = ax.bar(months, prod_a, label='Product A', color='steelblue')
b2 = ax.bar(months, prod_b, bottom=prod_a, label='Product B', color='coral')
bottom_c = [a + b for a, b in zip(prod_a, prod_b)]
b3 = ax.bar(months, prod_c, bottom=bottom_c, label='Product C', color='mediumseagreen')

ax.bar_label(b1, label_type='center', fontsize=9)
ax.bar_label(b2, label_type='center', fontsize=9)
ax.bar_label(b3, label_type='center', fontsize=9)

ax.legend()
ax.set_ylabel('Sales')
ax.set_title('Stacked Bar Chart')
plt.show()

Exercise 2. Create a 100% stacked bar chart (normalized) from the same data in Exercise 1. Convert each month's values to percentages of the total, so each bar reaches 100%. Use ax.bar with computed bottom offsets and percentage labels.

Solution to Exercise 2
import matplotlib.pyplot as plt
import numpy as np

months = ['Jan', 'Feb', 'Mar', 'Apr']
prod_a = np.array([10, 15, 12, 18])
prod_b = np.array([8, 12, 15, 10])
prod_c = np.array([5, 8, 10, 12])
totals = prod_a + prod_b + prod_c

pct_a = prod_a / totals * 100
pct_b = prod_b / totals * 100
pct_c = prod_c / totals * 100

fig, ax = plt.subplots(figsize=(8, 6))
ax.bar(months, pct_a, label='Product A', color='steelblue')
ax.bar(months, pct_b, bottom=pct_a, label='Product B', color='coral')
ax.bar(months, pct_c, bottom=pct_a + pct_b, label='Product C', color='mediumseagreen')

ax.set_ylabel('Percentage (%)')
ax.set_title('100% Stacked Bar Chart')
ax.legend()
ax.set_ylim(0, 100)
plt.show()

Exercise 3. Create a grouped horizontal bar chart using ax.barh for comparing exam scores of three students across four subjects. Students: Alice [85, 90, 78, 92], Bob [70, 88, 95, 80], Carol [92, 75, 82, 88]. Subjects: ['Math', 'Science', 'English', 'History']. Use height=0.25 for grouping.

Solution to Exercise 3
import matplotlib.pyplot as plt
import numpy as np

subjects = ['Math', 'Science', 'English', 'History']
alice = [85, 90, 78, 92]
bob = [70, 88, 95, 80]
carol = [92, 75, 82, 88]

y = np.arange(len(subjects))
height = 0.25

fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(y - height, alice, height, label='Alice', color='steelblue')
ax.barh(y, bob, height, label='Bob', color='coral')
ax.barh(y + height, carol, height, label='Carol', color='mediumseagreen')

ax.set_yticks(y)
ax.set_yticklabels(subjects)
ax.set_xlabel('Score')
ax.set_title('Exam Scores by Student')
ax.legend()
plt.tight_layout()
plt.show()