Waterfall Chart

Gross Margin Bridge Analysis

Waterfall showing gross margin evolution from volume, price, mix, and cost factors.

Output
Gross Margin Bridge Analysis
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

Did this help you?

Support PyLucid to keep it free & growing

Support