Waterfall Chart
Gross Margin Bridge Analysis
Waterfall showing gross margin evolution from volume, price, mix, and cost factors.
Output
Python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch
# Gross margin bridge (in millions)
categories = ['Prior Year\nGM', 'Volume\nEffect', 'Price\nRealization', 'Product\nMix',
'Material\nCosts', 'Labor\nCosts', 'Freight', 'Current Year\nGM']
values = [0, 28, 42, -15, -35, -18, -12, 0]
initial = 285
running_total = initial
bottoms, heights, colors = [], [], []
for i, (cat, val) in enumerate(zip(categories, values)):
if 'Prior' in cat:
bottoms.append(0)
heights.append(initial)
colors.append('#4927F5')
elif 'Current' in cat:
bottoms.append(0)
heights.append(running_total)
colors.append('#6CF527')
elif val > 0:
bottoms.append(running_total)
heights.append(val)
colors.append('#27F5B0')
running_total += val
else:
bottoms.append(running_total + val)
heights.append(abs(val))
colors.append('#F5276C')
running_total += val
fig, ax = plt.subplots(figsize=(14, 8), facecolor='#ffffff')
ax.set_facecolor('#ffffff')
x = np.arange(len(categories))
bars = ax.bar(x, heights, bottom=bottoms, color=colors, width=0.65, edgecolor='#e5e7eb', linewidth=1)
for i, (bar, val, bot, height) in enumerate(zip(bars, values, bottoms, heights)):
y_pos = bot + height / 2
if 'Prior' in categories[i] or 'Current' in categories[i]:
label = f"${height}M"
ax.text(bar.get_x() + bar.get_width()/2, y_pos, label,
ha='center', va='center', fontsize=11, fontweight='bold', color='#374151')
else:
label = f"+${val}M" if val > 0 else f"-${abs(val)}M"
ax.text(bar.get_x() + bar.get_width()/2, y_pos, label,
ha='center', va='center', fontsize=10, fontweight='bold', color='#374151')
for i in range(len(x) - 1):
y = initial if i == 0 else bottoms[i] + heights[i]
ax.plot([x[i] + 0.35, x[i+1] - 0.35], [y, y], color='#9ca3af', linestyle='--', linewidth=1.5, alpha=0.7)
ax.set_xlim(-0.6, len(categories) - 0.4)
ax.set_ylim(0, max(b + h for b, h in zip(bottoms, heights)) * 1.1)
ax.set_xticks(x)
ax.set_xticklabels(categories, fontsize=9, color='#374151')
ax.set_ylabel('Gross Margin ($ Millions)', fontsize=12, color='#374151', fontweight='500')
ax.set_title('Gross Margin Year-over-Year Bridge', fontsize=16, color='#374151', fontweight='bold', pad=20)
ax.tick_params(axis='y', colors='#e2e8f0', labelsize=10)
ax.yaxis.grid(True, linestyle='--', alpha=0.3, color='#e5e7eb')
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_color('#334155')
change = running_total - initial
pct = (change / initial) * 100
ax.annotate(f'YoY Change: {"+" if change > 0 else ""}{change}M ({pct:+.1f}%)', xy=(0.98, 0.95), xycoords='axes fraction',
fontsize=11, color='#F5276C' if change < 0 else '#6CF527', ha='right', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.4', facecolor='white', edgecolor='#F5276C' if change < 0 else '#6CF527', alpha=0.9))
legend_elements = [Patch(facecolor='#4927F5', label='Prior Year'), Patch(facecolor='#27F5B0', label='Positive'),
Patch(facecolor='#F5276C', label='Negative'), Patch(facecolor='#6CF527', label='Current Year')]
ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0, -0.1), ncol=4, fontsize=9,
facecolor='white', edgecolor='#e5e7eb', labelcolor='#374151')
plt.tight_layout()
plt.subplots_adjust(bottom=0.15)
plt.show()
Library
Matplotlib
Category
Financial
More Waterfall Chart examples
☕