Waterfall Chart

SaaS Unit Economics Waterfall

Unit economics breakdown from gross revenue to contribution margin per customer.

Output
SaaS Unit Economics Waterfall
Python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch

# Unit economics (monthly $ per customer)
categories = ['Gross\nRevenue', 'Payment\nFees', 'Hosting\nCosts', 'Support\nCosts', 
              'Gross\nMargin', 'CAC\nPayback', 'Retention\nCosts', 'Contribution\nMargin']
values = [0, -8.50, -12.00, -15.50, 0, -18.00, -6.00, 0]

# Calculate through the waterfall
initial = 89.00
running_total = initial
bottoms = []
heights = []
colors = []

for i, (cat, val) in enumerate(zip(categories, values)):
    if cat == 'Gross\nRevenue':
        bottoms.append(0)
        heights.append(initial)
        colors.append('#27D3F5')
    elif cat == 'Gross\nMargin':
        bottoms.append(0)
        heights.append(running_total)
        colors.append('#F5B027')
    elif cat == 'Contribution\nMargin':
        bottoms.append(0)
        heights.append(running_total)
        colors.append('#6CF527')
    elif val < 0:
        bottoms.append(running_total + val)
        heights.append(abs(val))
        colors.append('#F5276C')
        running_total += val

# Create figure - light theme
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)

# Add value labels
labels = [89.00, 8.50, 12.00, 15.50, 53.00, 18.00, 6.00, 29.00]
for i, (bar, label, bot, height) in enumerate(zip(bars, labels, bottoms, heights)):
    y_pos = bot + height / 2
    if categories[i] in ['Gross\nRevenue', 'Gross\nMargin', 'Contribution\nMargin']:
        text = f"${label:.2f}"
        color = 'white' if height > 35 else '#374151'
    else:
        text = f"-${label:.2f}"
        color = 'white'
    ax.text(bar.get_x() + bar.get_width()/2, y_pos, text, 
            ha='center', va='center', fontsize=10, fontweight='bold', color=color)

# Styling
ax.set_xlim(-0.6, len(categories) - 0.4)
ax.set_ylim(0, initial * 1.1)
ax.set_xticks(x)
ax.set_xticklabels(categories, fontsize=9, color='#374151')
ax.set_ylabel('Monthly $ per Customer', fontsize=12, color='#374151', fontweight='500')
ax.set_title('SaaS Unit Economics: Revenue to Contribution Margin', fontsize=16, color='#111827', fontweight='bold', pad=20)

ax.tick_params(axis='y', colors='#374151', labelsize=10)
ax.yaxis.grid(True, linestyle='--', alpha=0.4, color='#e5e7eb')
ax.set_axisbelow(True)

for spine in ax.spines.values():
    spine.set_color('#d1d5db')

# Margin percentages
gross_margin_pct = 53.00 / 89.00 * 100
contrib_margin_pct = 29.00 / 89.00 * 100
ax.annotate(f'Gross Margin: {gross_margin_pct:.1f}% | Contribution Margin: {contrib_margin_pct:.1f}%', 
            xy=(0.5, 0.95), xycoords='axes fraction',
            fontsize=11, color='#374151', ha='center', fontweight='bold',
            bbox=dict(boxstyle='round,pad=0.4', facecolor='#f3f4f6', edgecolor='#d1d5db'))

# Legend outside plot
legend_elements = [
    Patch(facecolor='#27D3F5', label='Revenue'),
    Patch(facecolor='#F5276C', label='Costs'),
    Patch(facecolor='#F5B027', label='Gross Margin'),
    Patch(facecolor='#6CF527', label='Contribution Margin')
]
ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0, -0.1), 
          ncol=4, fontsize=9, facecolor='white', edgecolor='#d1d5db', 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