How to Create a Stacked Bar Chart in Matplotlib

• Stacked bar charts excel at showing part-to-whole relationships over categories, but become unreadable with more than 5-6 segments—use grouped bars or separate charts instead.

Key Insights

• Stacked bar charts excel at showing part-to-whole relationships over categories, but become unreadable with more than 5-6 segments—use grouped bars or separate charts instead. • The bottom parameter in plt.bar() is the foundation of stacking: each segment sits on top of the cumulative sum of previous segments. • Pandas’ plot(kind='bar', stacked=True) eliminates manual bottom calculations, making it the preferred method when working with tabular data.

Understanding When to Use Stacked Bar Charts

Stacked bar charts display multiple data series on top of each other, showing both individual values and cumulative totals. They’re ideal for comparing part-to-whole relationships across categories: budget allocations across departments, market share composition over time, or survey responses broken down by demographics.

Use stacked bars when the total height matters as much as individual segments. If you only care about comparing individual series across categories, grouped bar charts work better. Stacked bars force readers to compare segments that don’t share a common baseline (except the bottom segment), making precise comparisons difficult.

Common use cases include quarterly revenue by product line, website traffic sources over months, and project time allocation across team members. Avoid them when you have more than six segments—the visual complexity overwhelms the message.

Building Your First Stacked Bar Chart

The core technique uses matplotlib’s bar() function with the bottom parameter. Each segment specifies where its baseline starts, which equals the cumulative sum of all previous segments.

import matplotlib.pyplot as plt
import numpy as np

# Sales data for three quarters
quarters = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = [120, 145, 130, 160]
product_b = [85, 95, 110, 100]
product_c = [60, 70, 75, 85]

# Create figure and axis
fig, ax = plt.subplots(figsize=(10, 6))

# First segment sits on the x-axis (no bottom parameter)
ax.bar(quarters, product_a, label='Product A', color='#2E86AB')

# Second segment sits on top of Product A
ax.bar(quarters, product_b, bottom=product_a, label='Product B', color='#A23B72')

# Third segment sits on top of Product A + Product B
bottom_c = np.array(product_a) + np.array(product_b)
ax.bar(quarters, product_c, bottom=bottom_c, label='Product C', color='#F18F01')

ax.set_ylabel('Sales (thousands)')
ax.set_title('Quarterly Sales by Product')
ax.legend()

plt.tight_layout()
plt.show()

The key insight: you manually calculate cumulative sums for each bottom parameter. This gives you complete control but requires careful bookkeeping as segments increase.

Horizontal Stacked Bars for Readability

Horizontal orientation improves readability when category labels are long. Use barh() instead of bar(), and swap the bottom parameter for left.

import matplotlib.pyplot as plt
import numpy as np

# Department budget data
departments = ['Engineering', 'Marketing', 'Sales', 'Operations']
salaries = [450, 280, 320, 180]
software = [120, 45, 30, 25]
equipment = [80, 35, 20, 60]
travel = [40, 90, 110, 25]

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

# Build from left to right
ax.barh(departments, salaries, label='Salaries', color='#264653')
ax.barh(departments, software, left=salaries, label='Software', color='#2A9D8F')

left_equipment = np.array(salaries) + np.array(software)
ax.barh(departments, equipment, left=left_equipment, label='Equipment', color='#E9C46A')

left_travel = left_equipment + np.array(equipment)
ax.barh(departments, travel, left=left_travel, label='Travel', color='#F4A261')

ax.set_xlabel('Budget (thousands)')
ax.set_title('Department Budget Allocation')
ax.legend(loc='lower right')

plt.tight_layout()
plt.show()

Horizontal bars work particularly well for ranking data or when you have 8+ categories that would crowd the x-axis.

Customizing Visual Appearance

Professional charts require thoughtful styling. Control colors with explicit palettes, add value labels to segments, and position legends strategically.

import matplotlib.pyplot as plt
import numpy as np

quarters = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = np.array([120, 145, 130, 160])
product_b = np.array([85, 95, 110, 100])

fig, ax = plt.subplots(figsize=(12, 7))

# Custom color palette (colorblind-friendly)
colors = ['#0173B2', '#DE8F05']

bars1 = ax.bar(quarters, product_a, label='Product A', 
               color=colors[0], edgecolor='white', linewidth=2)
bars2 = ax.bar(quarters, product_b, bottom=product_a, 
               label='Product B', color=colors[1], 
               edgecolor='white', linewidth=2)

# Add value labels on each segment
for i, (bar1, bar2) in enumerate(zip(bars1, bars2)):
    height1 = bar1.get_height()
    height2 = bar2.get_height()
    
    # Label for first segment (centered)
    ax.text(bar1.get_x() + bar1.get_width()/2, height1/2,
            f'{product_a[i]}', ha='center', va='center', 
            color='white', fontweight='bold')
    
    # Label for second segment (centered on its portion)
    ax.text(bar2.get_x() + bar2.get_width()/2, 
            product_a[i] + height2/2,
            f'{product_b[i]}', ha='center', va='center',
            color='white', fontweight='bold')

ax.set_ylabel('Sales (thousands)', fontsize=12)
ax.set_title('Quarterly Sales by Product', fontsize=14, fontweight='bold')
ax.legend(loc='upper left', bbox_to_anchor=(1, 1))
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()

The bbox_to_anchor parameter moves the legend outside the plot area, preventing it from obscuring data. Edge colors create visual separation between segments.

Streamlined Approach with Pandas

When working with tabular data, pandas eliminates manual bottom calculations entirely. The DataFrame’s plot() method handles stacking automatically.

import pandas as pd
import matplotlib.pyplot as plt

# Create DataFrame from dictionary
data = {
    'Product A': [120, 145, 130, 160],
    'Product B': [85, 95, 110, 100],
    'Product C': [60, 70, 75, 85]
}
df = pd.DataFrame(data, index=['Q1', 'Q2', 'Q3', 'Q4'])

# Create stacked bar chart with one line
ax = df.plot(kind='bar', stacked=True, figsize=(10, 6),
             color=['#003f5c', '#7a5195', '#ef5675'])

ax.set_ylabel('Sales (thousands)')
ax.set_title('Quarterly Sales by Product')
ax.set_xlabel('Quarter')
ax.legend(title='Products', bbox_to_anchor=(1.05, 1))

plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

This approach scales effortlessly. Add more columns to the DataFrame, and they automatically appear as new segments. For CSV data, use pd.read_csv() and set the appropriate index column.

Advanced Techniques for Complex Scenarios

Percentage-based stacking (100% stacked charts) shows relative proportions instead of absolute values. Normalize each row to sum to 100.

import pandas as pd
import matplotlib.pyplot as plt

data = {
    'Product A': [120, 145, 130, 160],
    'Product B': [85, 95, 110, 100],
    'Product C': [60, 70, 75, 85]
}
df = pd.DataFrame(data, index=['Q1', 'Q2', 'Q3', 'Q4'])

# Normalize to percentages
df_percent = df.div(df.sum(axis=1), axis=0) * 100

ax = df_percent.plot(kind='bar', stacked=True, figsize=(10, 6),
                     color=['#4e79a7', '#f28e2c', '#e15759'])

ax.set_ylabel('Percentage (%)')
ax.set_title('Product Mix by Quarter (Percentage)')
ax.set_ylim(0, 100)
ax.legend(title='Products', bbox_to_anchor=(1.05, 1))

plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

Handling negative values requires careful consideration. Stacked bars with mixed positive and negative values can confuse readers. Separate positive and negative segments or use a different chart type.

import matplotlib.pyplot as plt
import numpy as np

# Profit/loss scenario
quarters = ['Q1', 'Q2', 'Q3', 'Q4']
revenue = np.array([200, 250, 230, 280])
costs = np.array([-150, -180, -160, -190])
taxes = np.array([-20, -25, -30, -35])

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

# Positive values stack upward
ax.bar(quarters, revenue, label='Revenue', color='#2E7D32')

# Negative values stack downward
ax.bar(quarters, costs, label='Operating Costs', color='#C62828')
bottom_taxes = costs
ax.bar(quarters, taxes, bottom=bottom_taxes, label='Taxes', color='#AD1457')

ax.axhline(y=0, color='black', linewidth=0.8)
ax.set_ylabel('Amount (thousands)')
ax.set_title('Quarterly Financial Breakdown')
ax.legend()

plt.tight_layout()
plt.show()

Avoiding Common Pitfalls

Stacked bar charts fail when you exceed five or six segments. Each additional segment makes comparison harder since only the bottom segment has a consistent baseline. Consider small multiples or grouped bars instead.

Color choice matters for accessibility. Use colorblind-friendly palettes and ensure sufficient contrast between adjacent segments. Tools like ColorBrewer provide tested palettes.

Don’t use stacked bars when precise value comparison is critical. The human eye struggles to compare lengths without a common baseline. If readers need exact values, add data labels or choose a different visualization.

Avoid 3D effects, gradients, or decorative elements that reduce clarity. Your goal is communication, not decoration. Keep it simple and let the data speak.

When totals vary dramatically across categories, consider normalizing to percentages. A bar ranging from 10 to 100 next to one from 1,000 to 5,000 creates scaling problems that obscure the story.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.