|
|
"""Response formatting utilities""" |
|
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple |
|
|
import plotly.graph_objects as go |
|
|
import plotly.express as px |
|
|
import traceback |
|
|
|
|
|
def safe_get(data: Dict, key: str, default: Any = None) -> Any: |
|
|
"""Safely get value from dictionary""" |
|
|
try: |
|
|
return data.get(key, default) |
|
|
except: |
|
|
return default |
|
|
|
|
|
|
|
|
def format_card_display(card_data: Dict) -> str: |
|
|
"""Format card recommendation for display""" |
|
|
card_name = card_data.get("card_name", "Unknown Card") |
|
|
reward_rate = card_data.get("reward_rate", 0) |
|
|
reward_amount = card_data.get("reward_amount", 0) |
|
|
category = card_data.get("category", "General") |
|
|
reasoning = card_data.get("reasoning", "") |
|
|
|
|
|
return f""" |
|
|
### 💳 {card_name} |
|
|
|
|
|
**Reward Rate:** {reward_rate}x points |
|
|
**Reward Amount:** ${reward_amount:.2f} |
|
|
**Category:** {category} |
|
|
**Why:** {reasoning} |
|
|
""" |
|
|
|
|
|
def format_full_recommendation(response: Dict) -> str: |
|
|
"""Format complete recommendation response""" |
|
|
if response.get("error"): |
|
|
return f"❌ **Error:** {response.get('message', 'Unknown error')}" |
|
|
|
|
|
|
|
|
output = f""" |
|
|
# 🎯 Recommendation for {response.get('merchant', 'Unknown')} |
|
|
|
|
|
**Amount:** ${response.get('amount_usd', 0):.2f} |
|
|
**Date:** {response.get('transaction_date', 'N/A')} |
|
|
**User:** {response.get('user_id', 'N/A')} |
|
|
|
|
|
--- |
|
|
|
|
|
## 🏆 Best Card to Use |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
recommended = response.get("recommended_card", {}) |
|
|
output += format_card_display(recommended) |
|
|
|
|
|
|
|
|
rag_insights = response.get("rag_insights") |
|
|
if rag_insights: |
|
|
output += f""" |
|
|
--- |
|
|
|
|
|
## 📚 Card Benefits |
|
|
|
|
|
{rag_insights.get('benefits', 'No additional information available.')} |
|
|
""" |
|
|
if rag_insights.get('tips'): |
|
|
output += f""" |
|
|
💡 **Pro Tip:** {rag_insights.get('tips')} |
|
|
""" |
|
|
|
|
|
|
|
|
forecast = response.get("forecast_warning") |
|
|
if forecast: |
|
|
risk_level = forecast.get("risk_level", "low") |
|
|
message = forecast.get("message", "") |
|
|
|
|
|
if risk_level == "high": |
|
|
emoji = "🚨" |
|
|
elif risk_level == "medium": |
|
|
emoji = "⚠️" |
|
|
else: |
|
|
emoji = "✅" |
|
|
|
|
|
output += f""" |
|
|
--- |
|
|
|
|
|
## {emoji} Spending Status |
|
|
|
|
|
{message} |
|
|
|
|
|
**Current Spend:** ${forecast.get('current_spend', 0):.2f} |
|
|
**Spending Cap:** ${forecast.get('cap', 0):.2f} |
|
|
**Projected Spend:** ${forecast.get('projected_spend', 0):.2f} |
|
|
""" |
|
|
|
|
|
|
|
|
alternatives = response.get("alternative_cards", []) |
|
|
if alternatives: |
|
|
output += "\n---\n\n## 🔄 Alternative Cards\n\n" |
|
|
for i, alt in enumerate(alternatives[:2], 1): |
|
|
output += f"### Option {i}\n" |
|
|
output += format_card_display(alt) |
|
|
|
|
|
|
|
|
services = response.get("services_used", []) |
|
|
time_ms = response.get("orchestration_time_ms", 0) |
|
|
|
|
|
output += f""" |
|
|
--- |
|
|
|
|
|
**Services Used:** {', '.join(services)} |
|
|
**Response Time:** {time_ms:.0f}ms |
|
|
""" |
|
|
|
|
|
return output |
|
|
|
|
|
def format_comparison_table(cards: list) -> str: |
|
|
"""Format card comparison as markdown table""" |
|
|
if not cards: |
|
|
return "No cards to compare." |
|
|
|
|
|
table = """ |
|
|
| Card | Reward Rate | Reward Amount | Category | |
|
|
|------|-------------|---------------|----------| |
|
|
""" |
|
|
|
|
|
for card in cards: |
|
|
name = card.get("card_name", "Unknown") |
|
|
rate = card.get("reward_rate", 0) |
|
|
amount = card.get("reward_amount", 0) |
|
|
category = card.get("category", "N/A") |
|
|
table += f"| {name} | {rate}x | ${amount:.2f} | {category} |\n" |
|
|
|
|
|
return table |
|
|
|
|
|
|
|
|
|
|
|
def format_analytics_metrics(data: Dict) -> Tuple[str, str, str, str]: |
|
|
"""Format analytics data into HTML metrics, table, insights, and forecast""" |
|
|
|
|
|
user_id = data.get("user_id", "Unknown User") |
|
|
total_spending = data.get("total_spending", 0) |
|
|
total_rewards = data.get("total_rewards", 0) |
|
|
optimization_score = data.get("optimization_score", 0) |
|
|
potential_savings = data.get("potential_savings", 0) |
|
|
optimized_count = data.get("optimized_count", 0) |
|
|
|
|
|
|
|
|
rewards_rate = (total_rewards / total_spending * 100) if total_spending > 0 else 0 |
|
|
|
|
|
|
|
|
metrics_html = f""" |
|
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> |
|
|
<div class="metric-card" style="flex: 1; min-width: 200px;"> |
|
|
<h2>${potential_savings:,.2f}</h2> |
|
|
<p>💰 Potential Annual Savings</p> |
|
|
<small style="opacity: 0.8;">for {user_id}</small> |
|
|
</div> |
|
|
<div class="metric-card metric-card-green" style="flex: 1; min-width: 200px;"> |
|
|
<h2>{rewards_rate:.1f}%</h2> |
|
|
<p>📈 Rewards Rate</p> |
|
|
<small style="opacity: 0.8;">Effective return</small> |
|
|
</div> |
|
|
<div class="metric-card metric-card-orange" style="flex: 1; min-width: 200px;"> |
|
|
<h2>{optimized_count}</h2> |
|
|
<p>✅ Optimized Transactions</p> |
|
|
<small style="opacity: 0.8;">This month</small> |
|
|
</div> |
|
|
<div class="metric-card metric-card-blue" style="flex: 1; min-width: 200px;"> |
|
|
<h2>{optimization_score}/100</h2> |
|
|
<p>⭐ Optimization Score</p> |
|
|
<small style="opacity: 0.8;">{"Excellent!" if optimization_score >= 85 else "Good!" if optimization_score >= 70 else "Needs work"}</small> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
spending_by_category = data.get("spending_by_category", {}) |
|
|
|
|
|
if spending_by_category: |
|
|
table_rows = "\n".join([ |
|
|
f"| {category} | ${amount:,.2f} | {(amount/total_spending*100):.1f}% |" |
|
|
for category, amount in spending_by_category.items() |
|
|
]) |
|
|
|
|
|
spending_table = f""" |
|
|
## 💳 Spending Breakdown for **{user_id}** |
|
|
|
|
|
| Category | Amount | % of Total | |
|
|
|----------|--------|------------| |
|
|
{table_rows} |
|
|
| **TOTAL** | **${total_spending:,.2f}** | **100%** | |
|
|
""" |
|
|
else: |
|
|
spending_table = f"## 💳 Spending Breakdown for **{user_id}**\n\n*No spending data available*" |
|
|
|
|
|
|
|
|
if spending_by_category: |
|
|
top_category = max(spending_by_category.items(), key=lambda x: x[0])[0] |
|
|
top_amount = spending_by_category[top_category] |
|
|
else: |
|
|
top_category = "Unknown" |
|
|
top_amount = 0 |
|
|
|
|
|
insights = f""" |
|
|
## 💡 Personalized Insights for **{user_id}** |
|
|
|
|
|
- 🎯 Your top spending category is **{top_category}** (${top_amount:,.2f}) |
|
|
- 📊 Optimization score: **{optimization_score}/100** - {"🎉 Excellent!" if optimization_score >= 85 else "👍 Good!" if optimization_score >= 70 else "⚠️ Room for improvement"} |
|
|
- 💰 You could save **${potential_savings:,.2f}** annually with better card selection |
|
|
- 🏆 Current rewards rate: **{rewards_rate:.2f}%** effective return |
|
|
- ✅ Optimized **{optimized_count}** transactions this period |
|
|
|
|
|
**💡 Quick Tips:** |
|
|
- Consider cards with higher rewards in {top_category} |
|
|
- Watch for spending caps on premium cards |
|
|
- Review quarterly bonus categories |
|
|
""" |
|
|
|
|
|
|
|
|
projected_spending = total_spending * 1.05 |
|
|
projected_rewards = total_rewards * 1.05 |
|
|
|
|
|
forecast = f""" |
|
|
## 🔮 Forecast for **{user_id}** |
|
|
|
|
|
Based on your spending patterns: |
|
|
|
|
|
- 📈 Projected next month spending: **${projected_spending:,.2f}** (↑ 5%) |
|
|
- 💎 Projected rewards: **${projected_rewards:,.2f}** |
|
|
- 🎯 Potential with optimization: **${projected_rewards * 1.15:,.2f}** |
|
|
|
|
|
**⚠️ Heads Up:** |
|
|
- Watch out for category spending caps on premium cards |
|
|
- Q4 bonus categories may be ending soon |
|
|
- Consider timing large purchases for maximum rewards |
|
|
""" |
|
|
|
|
|
return metrics_html, spending_table, insights, forecast |
|
|
|
|
|
def create_spending_chart(data: Dict) -> go.Figure: |
|
|
"""Create spending vs rewards chart""" |
|
|
try: |
|
|
|
|
|
spending_by_category = data.get('spending_by_category', {}) |
|
|
|
|
|
if not spending_by_category: |
|
|
|
|
|
spending_by_category = { |
|
|
'Groceries': 1200.00, |
|
|
'Restaurants': 850.00, |
|
|
'Gas Stations': 420.00, |
|
|
'Online Shopping': 800.00 |
|
|
} |
|
|
|
|
|
categories = list(spending_by_category.keys()) |
|
|
spending = list(spending_by_category.values()) |
|
|
|
|
|
|
|
|
rewards = [s * 0.02 for s in spending] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Spending', |
|
|
x=categories, |
|
|
y=spending, |
|
|
marker_color='#667eea', |
|
|
text=[f'${s:,.0f}' for s in spending], |
|
|
textposition='outside' |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
name='Rewards', |
|
|
x=categories, |
|
|
y=rewards, |
|
|
marker_color='#38ef7d', |
|
|
text=[f'${r:.2f}' for r in rewards], |
|
|
textposition='outside' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title='Spending vs Rewards by Category', |
|
|
xaxis_title='Category', |
|
|
yaxis_title='Amount ($)', |
|
|
barmode='group', |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=True, |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Error creating spending chart: {e}") |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="Chart data unavailable", |
|
|
xref="paper", yref="paper", |
|
|
x=0.5, y=0.5, showarrow=False, |
|
|
font=dict(size=14, color="#666") |
|
|
) |
|
|
fig.update_layout(height=400, template='plotly_white') |
|
|
return fig |
|
|
|
|
|
|
|
|
def create_rewards_pie_chart(data: Dict) -> go.Figure: |
|
|
"""Create rewards distribution pie chart""" |
|
|
try: |
|
|
rewards_by_card = data.get('rewards_by_card', {}) |
|
|
|
|
|
if not rewards_by_card: |
|
|
|
|
|
rewards_by_card = { |
|
|
'Amex Gold': 95.50, |
|
|
'Chase Sapphire Reserve': 62.00, |
|
|
'Citi Double Cash': 30.00 |
|
|
} |
|
|
|
|
|
cards = list(rewards_by_card.keys()) |
|
|
rewards = list(rewards_by_card.values()) |
|
|
|
|
|
fig = go.Figure(data=[go.Pie( |
|
|
labels=cards, |
|
|
values=rewards, |
|
|
hole=0.4, |
|
|
marker=dict(colors=['#667eea', '#38ef7d', '#f093fb', '#4facfe']), |
|
|
textinfo='label+percent', |
|
|
textposition='outside', |
|
|
hovertemplate='<b>%{label}</b><br>$%{value:.2f}<br>%{percent}<extra></extra>' |
|
|
)]) |
|
|
|
|
|
fig.update_layout( |
|
|
title='Rewards Distribution by Card', |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=True, |
|
|
legend=dict(orientation="v", yanchor="middle", y=0.5, xanchor="left", x=1.1) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Error creating pie chart: {e}") |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="Chart data unavailable", |
|
|
xref="paper", yref="paper", |
|
|
x=0.5, y=0.5, showarrow=False, |
|
|
font=dict(size=14, color="#666") |
|
|
) |
|
|
fig.update_layout(height=400, template='plotly_white') |
|
|
return fig |
|
|
|
|
|
|
|
|
|
|
|
def create_optimization_gauge(data: Dict) -> go.Figure: |
|
|
"""Create optimization score gauge chart""" |
|
|
try: |
|
|
|
|
|
if isinstance(data, dict): |
|
|
score = data.get('optimization_score', 0) |
|
|
else: |
|
|
score = int(data) if data else 0 |
|
|
|
|
|
fig = go.Figure(go.Indicator( |
|
|
mode="gauge+number+delta", |
|
|
value=score, |
|
|
domain={'x': [0, 1], 'y': [0, 1]}, |
|
|
title={'text': "Optimization Score", 'font': {'size': 20}}, |
|
|
delta={'reference': 80, 'increasing': {'color': "green"}}, |
|
|
gauge={ |
|
|
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, |
|
|
'bar': {'color': "#667eea"}, |
|
|
'bgcolor': "white", |
|
|
'borderwidth': 2, |
|
|
'bordercolor': "gray", |
|
|
'steps': [ |
|
|
{'range': [0, 60], 'color': '#ffcccc'}, |
|
|
{'range': [60, 80], 'color': '#fff4cc'}, |
|
|
{'range': [80, 100], 'color': '#ccffcc'} |
|
|
], |
|
|
'threshold': { |
|
|
'line': {'color': "red", 'width': 4}, |
|
|
'thickness': 0.75, |
|
|
'value': 90 |
|
|
} |
|
|
} |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
margin=dict(l=20, r=20, t=50, b=20) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Error creating gauge: {e}") |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="Chart data unavailable", |
|
|
xref="paper", yref="paper", |
|
|
x=0.5, y=0.5, showarrow=False, |
|
|
font=dict(size=14, color="#666") |
|
|
) |
|
|
fig.update_layout(height=400, template='plotly_white') |
|
|
return fig |
|
|
|
|
|
|
|
|
def create_trend_line_chart(data: Dict) -> go.Figure: |
|
|
"""Create monthly trend line chart""" |
|
|
try: |
|
|
monthly_trends = data.get('monthly_trends', []) |
|
|
|
|
|
if not monthly_trends: |
|
|
|
|
|
monthly_trends = [ |
|
|
{"month": "Aug", "spending": 1200, "rewards": 52}, |
|
|
{"month": "Sep", "spending": 1450, "rewards": 63}, |
|
|
{"month": "Oct", "spending": 1600, "rewards": 72} |
|
|
] |
|
|
|
|
|
months = [t['month'] for t in monthly_trends] |
|
|
spending = [t['spending'] for t in monthly_trends] |
|
|
rewards = [t['rewards'] for t in monthly_trends] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=months, |
|
|
y=spending, |
|
|
mode='lines+markers', |
|
|
name='Spending', |
|
|
line=dict(color='#667eea', width=3), |
|
|
marker=dict(size=10), |
|
|
yaxis='y' |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=months, |
|
|
y=rewards, |
|
|
mode='lines+markers', |
|
|
name='Rewards', |
|
|
line=dict(color='#38ef7d', width=3), |
|
|
marker=dict(size=10), |
|
|
yaxis='y2' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title='Monthly Spending & Rewards Trends', |
|
|
xaxis=dict(title='Month'), |
|
|
yaxis=dict( |
|
|
title='Spending ($)', |
|
|
titlefont=dict(color='#667eea'), |
|
|
tickfont=dict(color='#667eea') |
|
|
), |
|
|
yaxis2=dict( |
|
|
title='Rewards ($)', |
|
|
titlefont=dict(color='#38ef7d'), |
|
|
tickfont=dict(color='#38ef7d'), |
|
|
overlaying='y', |
|
|
side='right' |
|
|
), |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
hovermode='x unified', |
|
|
showlegend=True, |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Error creating trend chart: {e}") |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="Chart data unavailable", |
|
|
xref="paper", yref="paper", |
|
|
x=0.5, y=0.5, showarrow=False, |
|
|
font=dict(size=14, color="#666") |
|
|
) |
|
|
fig.update_layout(height=400, template='plotly_white') |
|
|
return fig |
|
|
|
|
|
|
|
|
def create_card_performance_chart(data: Dict) -> go.Figure: |
|
|
"""Create card performance comparison chart""" |
|
|
try: |
|
|
|
|
|
rewards_by_card = ( |
|
|
data.get('rewards_by_card') or |
|
|
data.get('card_performance') or |
|
|
data.get('top_cards') or |
|
|
{} |
|
|
) |
|
|
|
|
|
print(f"🔍 DEBUG - rewards_by_card type: {type(rewards_by_card)}") |
|
|
print(f"🔍 DEBUG - rewards_by_card content: {rewards_by_card}") |
|
|
|
|
|
|
|
|
cards = [] |
|
|
rewards = [] |
|
|
|
|
|
if isinstance(rewards_by_card, list): |
|
|
|
|
|
for item in rewards_by_card: |
|
|
card_name = str(item.get('card', item.get('card_name', 'Unknown'))) |
|
|
reward_value = item.get('rewards', item.get('reward_amount', 0)) |
|
|
|
|
|
cards.append(card_name) |
|
|
|
|
|
try: |
|
|
rewards.append(float(reward_value)) |
|
|
except (ValueError, TypeError): |
|
|
print(f"⚠️ Could not convert reward value: {reward_value}") |
|
|
rewards.append(0.0) |
|
|
|
|
|
elif isinstance(rewards_by_card, dict): |
|
|
|
|
|
for card_name, reward_value in rewards_by_card.items(): |
|
|
cards.append(str(card_name)) |
|
|
|
|
|
try: |
|
|
reward_float = float(reward_value) |
|
|
rewards.append(reward_float) |
|
|
print(f"✅ Converted {card_name}: {reward_value} ({type(reward_value)}) -> {reward_float}") |
|
|
except (ValueError, TypeError) as e: |
|
|
print(f"⚠️ Could not convert reward value for {card_name}: {reward_value} - {e}") |
|
|
rewards.append(0.0) |
|
|
|
|
|
|
|
|
print(f"📊 BEFORE SORT - Cards: {cards}") |
|
|
print(f"📊 BEFORE SORT - Rewards: {rewards}") |
|
|
print(f"📊 BEFORE SORT - Rewards types: {[type(r) for r in rewards]}") |
|
|
|
|
|
|
|
|
if not cards or sum(rewards) == 0: |
|
|
print("⚠️ No card performance data, using sample data") |
|
|
cards = ['Amex Gold', 'Chase Sapphire Reserve', 'Citi Double Cash'] |
|
|
rewards = [95.50, 62.00, 30.00] |
|
|
|
|
|
|
|
|
sorted_pairs = sorted( |
|
|
list(zip(cards, rewards)), |
|
|
key=lambda pair: pair[1], |
|
|
reverse=True |
|
|
) |
|
|
|
|
|
|
|
|
cards = [pair[0] for pair in sorted_pairs] |
|
|
rewards = [pair[1] for pair in sorted_pairs] |
|
|
|
|
|
print(f"📊 AFTER SORT - Cards: {cards}") |
|
|
print(f"📊 AFTER SORT - Rewards: {rewards}") |
|
|
|
|
|
|
|
|
cards = cards[:5] |
|
|
rewards = rewards[:5] |
|
|
|
|
|
|
|
|
cards = cards[::-1] |
|
|
rewards = rewards[::-1] |
|
|
|
|
|
print(f"📊 FINAL - Cards: {cards}") |
|
|
print(f"📊 FINAL - Rewards: {rewards}") |
|
|
|
|
|
|
|
|
if not rewards: |
|
|
rewards = [0.0] |
|
|
|
|
|
max_reward = max(rewards) |
|
|
print(f"📊 Max reward: {max_reward} (type: {type(max_reward)})") |
|
|
|
|
|
|
|
|
colors = [] |
|
|
for r in rewards: |
|
|
opacity = 0.4 + 0.6 * (r / max_reward) if max_reward > 0 else 0.5 |
|
|
color = f'rgba(102, 126, 234, {opacity:.2f})' |
|
|
colors.append(color) |
|
|
print(f" Color for ${r:.2f}: {color}") |
|
|
|
|
|
fig = go.Figure(data=[go.Bar( |
|
|
y=cards, |
|
|
x=rewards, |
|
|
orientation='h', |
|
|
marker=dict( |
|
|
color=colors, |
|
|
line=dict(color='#667eea', width=1) |
|
|
), |
|
|
text=[f'${r:.2f}' for r in rewards], |
|
|
textposition='outside', |
|
|
textfont=dict(size=12, color='#333'), |
|
|
hovertemplate='<b>%{y}</b><br>Total Rewards: $%{x:.2f}<extra></extra>' |
|
|
)]) |
|
|
|
|
|
fig.update_layout( |
|
|
title={ |
|
|
'text': '🏆 Top Performing Cards', |
|
|
'x': 0.5, |
|
|
'xanchor': 'center', |
|
|
'font': {'size': 16, 'color': '#333'} |
|
|
}, |
|
|
xaxis=dict( |
|
|
title='Total Rewards Earned ($)', |
|
|
showgrid=True, |
|
|
gridcolor='#f0f0f0', |
|
|
zeroline=False |
|
|
), |
|
|
yaxis=dict( |
|
|
title='', |
|
|
showgrid=False, |
|
|
tickfont=dict(size=11) |
|
|
), |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=False, |
|
|
margin=dict(l=180, r=80, t=60, b=50), |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
paper_bgcolor='white' |
|
|
) |
|
|
|
|
|
print("✅ Chart created successfully!") |
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ Error creating card performance chart: {e}") |
|
|
print(f"📋 Traceback: {traceback.format_exc()}") |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="Chart data unavailable", |
|
|
xref="paper", yref="paper", |
|
|
x=0.5, y=0.5, showarrow=False, |
|
|
font=dict(size=14, color="#666") |
|
|
) |
|
|
fig.update_layout( |
|
|
height=400, |
|
|
template='plotly_white', |
|
|
title='Top Performing Cards' |
|
|
) |
|
|
return fig |