Spaces:
Sleeping
Sleeping
""" | |
Chart generation for equity analysis visualization | |
""" | |
import plotly.graph_objects as go | |
from plotly.subplots import make_subplots | |
from typing import List, Optional | |
from models import ScenarioResult, CapTable, EquityCalculator | |
class EquityCharts: | |
"""Handles all chart generation for equity analysis""" | |
def create_multi_scenario_comparison(results: List[ScenarioResult]) -> Optional[go.Figure]: | |
"""Create comparison chart showing option values and exit valuations""" | |
if not results: | |
return None | |
scenario_names = [r.scenario_name for r in results] | |
option_values = [r.option_value for r in results] | |
exit_values = [r.exit_valuation for r in results] | |
fig = make_subplots( | |
rows=2, cols=1, | |
subplot_titles=("Your Option Value by Scenario", "Exit Valuation by Scenario"), | |
vertical_spacing=0.20, | |
specs=[[{"secondary_y": False}], [{"secondary_y": False}]] | |
) | |
# Option values bar chart | |
fig.add_trace( | |
go.Bar( | |
x=scenario_names, | |
y=option_values, | |
name="Option Value", | |
marker_color='#2E86AB', | |
text=[f"${val:,.0f}" for val in option_values], | |
textposition='outside' | |
), | |
row=1, col=1 | |
) | |
# Exit valuations bar chart | |
fig.add_trace( | |
go.Bar( | |
x=scenario_names, | |
y=exit_values, | |
name="Exit Valuation", | |
marker_color='#F18F01', | |
text=[f"${val:,.0f}" for val in exit_values], | |
textposition='outside', | |
showlegend=False | |
), | |
row=2, col=1 | |
) | |
fig.update_layout( | |
title="Multi-Scenario Equity Analysis", | |
height=650, | |
showlegend=True, | |
margin=dict(t=80, b=50, l=80, r=50) | |
) | |
# Add extra space for text labels above bars | |
if option_values: | |
fig.update_yaxes(title_text="Your Option Value ($)", row=1, col=1, range=[0, max(option_values) * 1.15]) | |
if exit_values: | |
fig.update_yaxes(title_text="Company Valuation ($)", row=2, col=1, range=[0, max(exit_values) * 1.15]) | |
return fig | |
def create_liquidation_waterfall( | |
cap_table: CapTable, | |
exit_valuation: float, | |
scenario_name: str = "Best Scenario" | |
) -> go.Figure: | |
"""Create detailed liquidation waterfall chart for a specific exit value""" | |
calculator = EquityCalculator(cap_table) | |
remaining_proceeds = exit_valuation | |
waterfall_data = [] | |
participating_shareholders = [] | |
# Sort funding rounds (newest first for liquidation preferences) | |
sorted_rounds = sorted(cap_table.funding_rounds, | |
key=lambda x: ['Seed', 'Series A', 'Series B', 'Series C'].index(x.name) | |
if x.name in ['Seed', 'Series A', 'Series B', 'Series C'] else 999, | |
reverse=True) | |
# Phase 1: Liquidation preferences | |
for round in sorted_rounds: | |
if round.shares_issued > 0 and round.capital_raised > 0: | |
preference_payout = min(remaining_proceeds, round.liquidation_preference) | |
remaining_proceeds -= preference_payout | |
if round.is_participating: | |
participating_shareholders.append({ | |
'round': round.name, | |
'shares': round.shares_issued | |
}) | |
waterfall_data.append({ | |
'Round': f'{round.name} (Pref)', | |
'Payout': preference_payout, | |
'Type': 'Preference' | |
}) | |
# Phase 2: Participating preferred and common distribution | |
participating_preferred_shares = sum(p['shares'] for p in participating_shareholders) | |
total_participating_shares = cap_table.common_shares + participating_preferred_shares | |
if total_participating_shares > 0: | |
price_per_share = remaining_proceeds / total_participating_shares | |
common_proceeds = price_per_share * cap_table.common_shares | |
# Add participating preferred distributions | |
for participant in participating_shareholders: | |
participating_payout = price_per_share * participant['shares'] | |
waterfall_data.append({ | |
'Round': f"{participant['round']} (Part.)", | |
'Payout': participating_payout, | |
'Type': 'Participation' | |
}) | |
else: | |
common_proceeds = remaining_proceeds | |
# Add common stock | |
waterfall_data.append({ | |
'Round': 'Common Stock', | |
'Payout': common_proceeds, | |
'Type': 'Common' | |
}) | |
# Create the chart | |
fig = go.Figure() | |
color_map = { | |
'Preference': '#FF6B6B', # Red for liquidation preferences | |
'Participation': '#4ECDC4', # Teal for participating preferred | |
'Common': '#F7DC6F' # Yellow for common stock | |
} | |
for item in waterfall_data: | |
if item['Payout'] > 0: | |
color = color_map.get(item['Type'], '#96CEB4') | |
fig.add_trace(go.Bar( | |
x=[item['Round']], | |
y=[item['Payout']], | |
name=f"{item['Round']} (${item['Payout']:,.0f})", | |
marker_color=color, | |
text=f"${item['Payout']:,.0f}", | |
textposition='outside' | |
)) | |
fig.update_layout( | |
title=f"Liquidation Waterfall - ${exit_valuation:,.0f} Exit", | |
xaxis_title="Stakeholder", | |
yaxis_title="Payout ($)", | |
height=450, | |
showlegend=True, | |
margin=dict(t=60, b=50, l=80, r=50) | |
) | |
return fig | |
def create_roi_analysis(results: List[ScenarioResult], investment_cost: float) -> Optional[go.Figure]: | |
"""Create ROI analysis chart""" | |
if not results: | |
return None | |
roi_data = [] | |
for result in results: | |
roi = result.roi_percentage(investment_cost) | |
# Cap very high ROI for display purposes | |
display_roi = roi if roi < 999999 else 999999 | |
roi_data.append({ | |
'scenario': result.scenario_name, | |
'roi': display_roi, | |
'absolute_gain': result.option_value - investment_cost | |
}) | |
fig = go.Figure() | |
fig.add_trace(go.Bar( | |
x=[d['scenario'] for d in roi_data], | |
y=[d['roi'] for d in roi_data], | |
name="ROI %", | |
marker_color='#28A745', | |
text=[f"{d['roi']:.0f}%" if d['roi'] < 999999 else "β%" for d in roi_data], | |
textposition='outside' | |
)) | |
fig.update_layout( | |
title="Return on Investment (ROI) by Scenario", | |
xaxis_title="Scenario", | |
yaxis_title="ROI (%)", | |
height=450, | |
margin=dict(t=60, b=50, l=60, r=50) | |
) | |
return fig | |
def format_results_table(results: List[ScenarioResult]) -> str: | |
"""Format scenario results as a markdown table""" | |
if not results: | |
return "No scenarios to display" | |
table = "## π Exit Scenario Comparison\n\n" | |
table += "| Scenario | Exit Value | Your Option Value | Value per Option | Common Proceeds |\n" | |
table += "|----------|------------|-------------------|------------------|------------------|\n" | |
for result in results: | |
table += f"| **{result.scenario_name}** | ${result.exit_valuation:,.0f} | " | |
table += f"${result.option_value:,.2f} | ${result.value_per_option:.4f} | " | |
table += f"${result.common_proceeds:,.0f} |\n" | |
return table | |
def format_equity_summary(summary: dict, results: List[ScenarioResult]) -> str: | |
"""Format complete equity analysis summary""" | |
results_table = format_results_table(results) | |
summary_text = f""" | |
## π° Your Equity Summary | |
**Your Option Grant:** {summary['your_options']:,} options | |
**Strike Price:** ${summary['strike_price']:.4f} per share | |
**Your Equity Stake:** {summary['your_equity_percentage']:.3f}% | |
{results_table} | |
## ποΈ Cap Table Summary | |
**Total Shares:** {summary['total_shares']:,} | |
**Common Shares:** {summary['common_shares']:,} | |
**Preferred Shares:** {summary['preferred_shares']:,} | |
**Liquidation Terms:** {' | '.join(summary['participating_status']) if summary['participating_status'] else 'No preferred rounds'} | |
**Break-even Price per Share:** ${summary['break_even_price']:.4f} | |
*(Price needed for your options to have positive value)* | |
""" | |
return summary_text |