Waterfall Chart

Subscription Revenue Lifecycle

SaaS subscription journey from trial to renewal showing conversion and churn.

Output
Subscription Revenue Lifecycle
Python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch

categories = ['Trial\nSignups', 'Trial\nDropoff', 'Paid\nConversion', 'Month 1\nChurn', 
              'Month 3\nChurn', 'Upgrades', 'Annual\nRenewal', 'Active\nSubscribers']
values = [0, -4200, 0, -380, -290, 420, 0, 0]

initial = 10000
running_total = initial
bottoms, heights, colors = [], [], []

for i, (cat, val) in enumerate(zip(categories, values)):
    if 'Trial\nSignups' in cat:
        bottoms.append(0)
        heights.append(initial)
        colors.append('#27D3F5')
    elif 'Paid\nConversion' in cat:
        bottoms.append(0)
        heights.append(running_total)
        colors.append('#F5B027')
    elif 'Annual\nRenewal' in cat:
        bottoms.append(0)
        heights.append(running_total)
        colors.append('#4927F5')
    elif 'Active' 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='#0a0a0f')
ax.set_facecolor('#0a0a0f')

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

display_vals = [10000, -4200, 5800, -380, -290, 420, 5550, 5550]
for i, (bar, val, bot, height) in enumerate(zip(bars, display_vals, bottoms, heights)):
    y_pos = bot + height / 2
    if height > 2000:
        label = f"{height:,}"
        ax.text(bar.get_x() + bar.get_width()/2, y_pos, label, ha='center', va='center', 
                fontsize=10, fontweight='bold', color='#0a0a0f')
    else:
        label = f"{val:+,}" if i not in [0, 2, 6, 7] else f"{height:,}"
        ax.text(bar.get_x() + bar.get_width()/2, y_pos, label, ha='center', va='center', 
                fontsize=9, fontweight='bold', color='white')

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='#e2e8f0')
ax.set_ylabel('Subscribers', fontsize=12, color='#e2e8f0', fontweight='500')
ax.set_title('Subscription Funnel: Trial to Active Subscriber', fontsize=16, color='white', fontweight='bold', pad=20)
ax.tick_params(axis='y', colors='#e2e8f0', labelsize=10)
ax.yaxis.grid(True, linestyle='--', alpha=0.3, color='#334155')
ax.set_axisbelow(True)
for spine in ax.spines.values():
    spine.set_color('#334155')

conversion = (5550 / 10000) * 100
ax.annotate(f'Overall Conversion: {conversion:.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='#1e293b', edgecolor='#6CF527', alpha=0.9))

legend_elements = [Patch(facecolor='#27D3F5', label='Trials'), Patch(facecolor='#F5276C', label='Churn'),
                   Patch(facecolor='#27F5B0', label='Upgrades'), Patch(facecolor='#6CF527', label='Active')]
ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0, -0.1), ncol=4, fontsize=9,
          facecolor='#1e293b', edgecolor='#334155', labelcolor='white')

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