Waterfall Chart

Quarterly Revenue Bridge

Dark-themed waterfall chart showing the bridge between Q3 and Q4 revenue with contributing factors.

Output
Quarterly Revenue Bridge
Python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch

# Quarterly revenue bridge (in millions)
categories = ['Q1\nRevenue', 'New\nCustomers', 'Expansion', 'Churn', 
              'Downgrades', 'Price\nIncrease', 'Q2\nRevenue']
values = [0, 28, 15, -12, -8, 5, 0]

# Calculate running total
initial = 180
running_total = initial
bottoms = []
heights = []
colors = []

for i, (cat, val) in enumerate(zip(categories, values)):
    if 'Q1' in cat:
        bottoms.append(0)
        heights.append(initial)
        colors.append('#27D3F5')
    elif 'Q2' 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

# Create figure
fig, ax = plt.subplots(figsize=(12, 8), facecolor='#ffffff')
ax.set_facecolor('#ffffff')

x = np.arange(len(categories))
bars = ax.bar(x, heights, bottom=bottoms, color=colors, width=0.6, edgecolor='#e5e7eb', linewidth=1)

# Add value labels
for i, (bar, val, bot, height) in enumerate(zip(bars, values, bottoms, heights)):
    y_pos = bot + height / 2
    if 'Q1' in categories[i] or 'Q2' 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='#0a0a0f')
    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')

# Connect bars
for i in range(len(x) - 1):
    if i == 0:
        y = initial
    else:
        y = 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)

# Styling
ax.set_xlim(-0.6, len(categories) - 0.4)
ax.set_ylim(0, max(bottoms[i] + heights[i] for i in range(len(heights))) * 1.1)
ax.set_xticks(x)
ax.set_xticklabels(categories, fontsize=10, color='#374151')
ax.set_ylabel('Revenue ($ Millions)', fontsize=12, color='#374151', fontweight='500')
ax.set_title('Quarterly Revenue Bridge: Q1 to Q2', 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')

# Growth annotation
growth = running_total - initial
pct = (growth / initial) * 100
ax.annotate(f'QoQ Growth: +{pct:.1f}%', xy=(0.98, 0.95), xycoords='axes fraction',
            fontsize=11, color='#6CF527', ha='right', fontweight='bold',
            bbox=dict(boxstyle='round,pad=0.4', facecolor='white', edgecolor='#6CF527', alpha=0.9))

# Legend outside plot
legend_elements = [
    Patch(facecolor='#27D3F5', label='Starting Revenue'),
    Patch(facecolor='#27F5B0', label='Growth Drivers'),
    Patch(facecolor='#F5276C', label='Revenue Loss'),
    Patch(facecolor='#6CF527', label='Ending Revenue')
]
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