Skip to content

Grouped and Stacked

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

Grouped Bar Chart

Place bars side by side to compare multiple series.

1. Basic Grouped Bars

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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()