Datavision / backend /core /real_dashboard.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
157 kB
"""
TRUE AUTONOMOUS VISUAL INTELLIGENCE ENGINE - $50B Silicon Valley Feature
==========================================================================
THIS IS NOT HARDCODED!
The system AUTONOMOUSLY decides:
1. Which chart types to use (from 15+ types)
2. Which colors match the data domain
3. Which columns to visualize together
4. How many charts and what layout
5. What insights are most valuable
Based on:
- Data types (numeric, categorical, datetime, text)
- Column relationships and correlations
- Data distributions and patterns
- Domain detection (finance, sales, HR, marketing, etc.)
ALL calculations are REAL - done with pandas!
"""
import json
import logging
import hashlib
import time as time_module
import re
from typing import Dict, List, Optional, Tuple
import pandas as pd
import numpy as np
from datetime import datetime
import random
from core.llm import chat
logger = logging.getLogger(__name__)
# πŸš€ DASHBOARD CACHE - Avoid regenerating same dashboard
_dashboard_cache = {}
_CACHE_TTL = 30 # 30 seconds cache
def dashboard_column_profile(df: pd.DataFrame) -> Dict[str, List[str]]:
"""Choose fields that form meaningful business visuals, excluding raw IDs."""
identifiers, dates, metrics, dimensions = [], [], [], []
id_pattern = re.compile(r"(^|[_\s-])(id|uuid|guid|key|code|reference|ref|number)([_\s-]|$)|account.?id|transaction.?id", re.I)
for col in df.columns:
series = df[col]
name = str(col)
non_null = series.dropna()
unique_ratio = (non_null.nunique() / len(non_null)) if len(non_null) else 0
is_date_name = any(token in name.lower() for token in ("date", "time", "timestamp", "month", "year", "period"))
if pd.api.types.is_datetime64_any_dtype(series) or is_date_name:
if pd.to_datetime(series, errors="coerce").notna().mean() >= 0.70:
dates.append(col)
continue
is_identifier = bool(id_pattern.search(name)) or (
unique_ratio > 0.98 and any(token in name.lower() for token in ("account", "transaction", "customer", "order"))
)
if is_identifier:
identifiers.append(col)
continue
if pd.api.types.is_numeric_dtype(series):
metrics.append(col)
elif 2 <= non_null.nunique() <= 40 and unique_ratio < 0.60:
dimensions.append(col)
return {"identifiers": identifiers, "dates": dates, "metrics": metrics, "dimensions": dimensions}
def find_business_metric(columns: List[str], terms: Tuple[str, ...]) -> Optional[str]:
"""Find a real business measure by name; never infer profit from unrelated data."""
return next((column for column in columns if any(term in column.lower() for term in terms)), None)
def sanitize_for_json(obj):
"""Recursively convert numpy/pandas types to Python native types for JSON serialization."""
if isinstance(obj, dict):
return {k: sanitize_for_json(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [sanitize_for_json(v) for v in obj]
elif isinstance(obj, (np.integer, np.int8, np.int16, np.int32, np.int64,
np.uint8, np.uint16, np.uint32, np.uint64)):
return int(obj)
elif isinstance(obj, (np.floating, np.float16, np.float32, np.float64)):
return float(obj)
elif isinstance(obj, (np.ndarray,)):
return sanitize_for_json(obj.tolist())
elif isinstance(obj, (pd.Series,)):
return sanitize_for_json(obj.tolist())
elif pd.isna(obj): # Handle NaN
return None
return obj
# ================= AUTONOMOUS CHART TYPE SELECTION =================
# All supported chart types - PREMIUM ENTERPRISE COLLECTION
CHART_TYPES = [
# Basic Charts
'bar', 'horizontal_bar', 'stacked_bar', 'grouped_bar',
'line', 'area', 'stacked_area',
'scatter', 'bubble',
'pie', 'donut', 'sunburst',
'heatmap', 'treemap',
'histogram', 'box', 'violin',
'funnel', 'waterfall', 'pareto',
'radar', 'gauge', 'bullet',
'sankey', 'candlestick', 'parcoords',
'choropleth', 'scatter_3d', 'calendar_heatmap',
# NEW ADVANCED CHARTS - Premium Analytics
'combo_bar_line', # Bar + Line dual axis (Power BI style)
'lollipop', # Lollipop chart for rankings
'dumbbell', # Before/After comparisons
'bullet_comparison', # Multi-metric bullet charts
'sparkline_table', # Mini charts in table cells
'marimekko', # Market share visualization
'scatter_matrix', # Correlation matrix with scatter plots
'ridgeline', # Distribution comparisons over time
'slope', # Slope chart for period comparison
'diverging_bar', # Positive/negative from center
'pyramid', # Age/demographic pyramid
'waffle', # Percentage visualization
'density_contour', # 2D density plot
'band_chart', # Confidence interval bands
'step_line', # Step function line chart
'range_plot', # Min-Max range visualization
]
# Color palettes for different domains - POWERBI-LEVEL PREMIUM COLORS
COLOR_PALETTES = {
'finance': {
'primary': ['#10b981', '#059669', '#047857', '#065f46', '#064e3b'],
'accent': ['#14b8a6', '#0d9488', '#0f766e', '#115e59'],
'chart': ['#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#22c55e', '#eab308', '#f59e0b', '#ef4444']
},
'sales': {
'primary': ['#8b5cf6', '#7c3aed', '#6d28d9', '#5b21b6', '#4c1d95'],
'accent': ['#a78bfa', '#c4b5fd', '#ddd6fe'],
'chart': ['#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#fb7185', '#14b8a6', '#06b6d4', '#3b82f6', '#eab308', '#22c55e', '#f97316']
},
'marketing': {
'primary': ['#f97316', '#ea580c', '#c2410c', '#9a3412', '#7c2d12'],
'accent': ['#fb923c', '#fdba74', '#fed7aa'],
'chart': ['#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899', '#ef4444', '#10b981']
},
'hr': {
'primary': ['#06b6d4', '#0891b2', '#0e7490', '#155e75', '#164e63'],
'accent': ['#22d3ee', '#67e8f9', '#a5f3fc'],
'chart': ['#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#14b8a6', '#22c55e', '#f59e0b', '#ef4444']
},
'operations': {
'primary': ['#3b82f6', '#2563eb', '#1d4ed8', '#1e40af', '#1e3a8a'],
'accent': ['#60a5fa', '#93c5fd', '#bfdbfe'],
'chart': ['#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#14b8a6', '#22c55e', '#eab308', '#f97316', '#ef4444', '#06b6d4']
},
'general': {
'primary': ['#14b8a6', '#0d9488', '#0f766e', '#115e59', '#134e4a'],
'accent': ['#2dd4bf', '#5eead4', '#99f6e4'],
'chart': ['#14b8a6', '#22c55e', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#f59e0b', '#ef4444', '#ec4899']
}
}
# Schema-led domain intelligence. These are decision lenses, not templates:
# the chart planner still uses only columns present in the customer's data.
DOMAIN_PROFILES = {
'finance': {'keywords': ['revenue', 'profit', 'cost', 'expense', 'income', 'margin', 'budget', 'invoice', 'payment', 'balance', 'asset', 'liability'], 'sections': ['Financial Overview', 'Profitability', 'Cash & Revenue Trends', 'Risk & Variance', 'Drivers & Segments']},
'sales': {'keywords': ['sales', 'order', 'deal', 'lead', 'opportunity', 'quota', 'discount', 'pipeline', 'quantity'], 'sections': ['Sales Overview', 'Pipeline & Conversion', 'Product Performance', 'Customer Segments', 'Growth Drivers']},
'marketing': {'keywords': ['campaign', 'conversion', 'click', 'impression', 'reach', 'engagement', 'traffic', 'subscriber', 'roi'], 'sections': ['Campaign Overview', 'Acquisition Funnel', 'Channel Performance', 'Audience Segments', 'ROI & Optimisation']},
'hr': {'keywords': ['employee', 'salary', 'hire', 'department', 'position', 'performance', 'leave', 'attendance', 'attrition'], 'sections': ['Workforce Overview', 'Hiring & Attrition', 'Compensation', 'Performance & Attendance', 'Department Insights']},
'operations': {'keywords': ['inventory', 'supply', 'warehouse', 'stock', 'vendor', 'supplier', 'capacity', 'procurement'], 'sections': ['Operations Overview', 'Inventory Health', 'Supplier Performance', 'Capacity & Throughput', 'Exceptions & Risk']},
'retail': {'keywords': ['store', 'sku', 'basket', 'merchant', 'shop', 'retail', 'return', 'product'], 'sections': ['Retail Overview', 'Sales by Product', 'Store & Channel Performance', 'Customer Behaviour', 'Inventory & Returns']},
'healthcare': {'keywords': ['patient', 'diagnosis', 'clinical', 'hospital', 'treatment', 'admission', 'provider', 'medication'], 'sections': ['Care Overview', 'Patient Flow', 'Clinical Outcomes', 'Capacity & Utilisation', 'Quality & Risk']},
'manufacturing': {'keywords': ['production', 'machine', 'factory', 'defect', 'yield', 'assembly', 'downtime', 'quality'], 'sections': ['Production Overview', 'Quality & Defects', 'Yield & Throughput', 'Machine Utilisation', 'Downtime Drivers']},
'education': {'keywords': ['student', 'course', 'grade', 'exam', 'school', 'teacher', 'enrollment', 'attendance'], 'sections': ['Learning Overview', 'Enrollment & Retention', 'Academic Performance', 'Attendance', 'Programme Insights']},
'logistics': {'keywords': ['delivery', 'shipping', 'freight', 'route', 'carrier', 'dispatch', 'shipment', 'transport'], 'sections': ['Logistics Overview', 'Delivery Performance', 'Route & Carrier Analysis', 'Cost & Capacity', 'Service Exceptions']},
'real_estate': {'keywords': ['property', 'rent', 'lease', 'tenant', 'occupancy', 'building', 'mortgage', 'listing'], 'sections': ['Portfolio Overview', 'Occupancy & Leasing', 'Revenue & Costs', 'Property Performance', 'Market Insights']},
'technology': {'keywords': ['subscription', 'saas', 'user', 'session', 'api', 'software', 'ticket', 'incident', 'churn'], 'sections': ['Product Overview', 'Adoption & Engagement', 'Retention & Churn', 'Service Reliability', 'Account Growth']},
'telecom': {'keywords': ['network', 'telecom', 'call', 'bandwidth', 'latency', 'tower', 'subscriber', 'usage'], 'sections': ['Network Overview', 'Subscriber Growth', 'Usage & Revenue', 'Service Quality', 'Churn & Retention']},
'energy': {'keywords': ['energy', 'power', 'electricity', 'utility', 'meter', 'emission', 'generation', 'consumption'], 'sections': ['Energy Overview', 'Generation & Demand', 'Consumption Patterns', 'Cost & Emissions', 'Asset Reliability']},
'public_sector': {'keywords': ['citizen', 'municipal', 'government', 'public', 'permit', 'district', 'case', 'service'], 'sections': ['Service Overview', 'Demand & Cases', 'Programme Performance', 'Geographic Insights', 'Service Quality']},
}
def detect_data_domain(df: pd.DataFrame) -> str:
"""Identify the best fitting business domain from schema and field names."""
columns_lower = ' '.join(df.columns.str.lower())
scores = {domain: sum(keyword in columns_lower for keyword in profile['keywords']) for domain, profile in DOMAIN_PROFILES.items()}
best_domain = max(scores, key=scores.get)
return best_domain if scores[best_domain] > 0 else 'general'
def analyze_domain_and_sections(df: pd.DataFrame) -> Dict:
"""
AUTONOMOUS AGENT: Dynamically determines the domain and dashboard sections.
"""
try:
columns = list(df.columns)
dtypes = {col: str(dtype) for col, dtype in df.dtypes.items()}
supported_domains = '|'.join(DOMAIN_PROFILES)
prompt = f"""
Analyze this dataset profile to determine its domain and 4-6 logical dashboard sections.
Columns: {columns}
Types: {dtypes}
Respond ONLY with a valid JSON object matching this exact format:
{{
"domain": "{supported_domains}|general",
"dashboard_title": "A 3-5 word professional title",
"sections": ["Section 1 Name", "Section 2 Name", "Section 3 Name", "Section 4 Name"]
}}
"""
response = chat(prompt, temperature=0.2, max_tokens=200)
# Clean potential markdown wrapping
if response.startswith('```json'):
response = response[7:-3]
elif response.startswith('```'):
response = response[3:-3]
data = json.loads(response.strip())
if data.get('domain') not in DOMAIN_PROFILES:
data['domain'] = detect_data_domain(df)
if not data.get('sections'):
data['sections'] = DOMAIN_PROFILES.get(data['domain'], {}).get('sections', [])
return data
except Exception as e:
logger.warning(f"Domain Agent failed: {e}. Falling back to heuristics.")
domain = detect_data_domain(df)
return {
"domain": domain,
"dashboard_title": f"{domain.title()} Analytics Dashboard",
"sections": DOMAIN_PROFILES.get(domain, {}).get('sections', ["Overview", "Trends", "Segments", "Performance", "Insights"])
}
def analyze_column_relationships(df: pd.DataFrame) -> List[Dict]:
"""Analyze relationships between columns to determine best visualizations"""
relationships = []
profile = dashboard_column_profile(df)
numeric_cols = profile['metrics']
categorical_cols = profile['dimensions']
datetime_cols = profile['dates']
# Numeric vs Categorical (best for bar, pie, donut)
for num in numeric_cols[:3]:
for cat in categorical_cols[:3]:
cardinality = df[cat].nunique()
if 2 <= cardinality <= 15:
relationships.append({
'type': 'num_vs_cat',
'numeric': num,
'categorical': cat,
'cardinality': cardinality,
'best_charts': ['bar', 'horizontal_bar', 'pie', 'donut', 'treemap', 'funnel', 'pareto', 'radar', 'gauge', 'polar_bar'] if cardinality <= 8 else ['bar', 'horizontal_bar', 'treemap', 'funnel', 'pareto']
})
# Numeric vs Numeric (best for scatter, bubble)
for i, num1 in enumerate(numeric_cols[:4]):
for num2 in numeric_cols[i+1:5]:
corr = df[[num1, num2]].corr().iloc[0, 1] if len(df) > 5 else 0
relationships.append({
'type': 'num_vs_num',
'x': num1,
'y': num2,
'correlation': abs(corr) if not pd.isna(corr) else 0,
'best_charts': ['scatter', 'bubble', 'heatmap', 'line', 'area', 'parcoords', 'density_contour', 'hexbin']
})
# Time series (best for line, area)
if datetime_cols and numeric_cols:
for dt_col in datetime_cols[:1]:
for num_col in numeric_cols[:3]:
relationships.append({
'type': 'time_series',
'time': dt_col,
'value': num_col,
'best_charts': ['line', 'area', 'stacked_area', 'bar', 'scatter', 'candlestick', 'waterfall']
})
# Distribution analysis (best for histogram, box)
for num in numeric_cols[:3]:
skewness = df[num].skew() if len(df) > 10 else 0
relationships.append({
'type': 'distribution',
'column': num,
'skewness': skewness if not pd.isna(skewness) else 0,
'best_charts': ['histogram', 'box', 'violin', 'area', 'gauge']
})
# Multi-category (best for stacked, grouped)
if len(categorical_cols) >= 2 and numeric_cols:
relationships.append({
'type': 'multi_category',
'cat1': categorical_cols[0],
'cat2': categorical_cols[1],
'value': numeric_cols[0],
'categorical_cols': categorical_cols[:3],
'best_charts': ['stacked_bar', 'grouped_bar', 'heatmap', 'sunburst', 'multi_sunburst', 'treemap', 'sankey', 'parcoords', 'radar']
})
# Advanced Pattern Detection ------------------------------------------
# 1. Flow / Sankey Detection
# Look for source/target pairs
source_keywords = ['source', 'origin', 'from', 'start']
target_keywords = ['target', 'dest', 'to', 'end']
source_col = next((c for c in categorical_cols if any(k in c.lower() for k in source_keywords)), None)
target_col = next((c for c in categorical_cols if any(k in c.lower() for k in target_keywords)), None)
if source_col and target_col and numeric_cols:
relationships.append({
'type': 'sankey',
'source': source_col,
'target': target_col,
'value': numeric_cols[0],
'best_charts': ['sankey']
})
# 2. Financial OHLC Detection (Candlestick)
ohlc_map = {'open': None, 'high': None, 'low': None, 'close': None}
for col in numeric_cols:
lower = col.lower()
for key in ohlc_map:
if key in lower:
ohlc_map[key] = col
if all(ohlc_map.values()) and datetime_cols:
relationships.append({
'type': 'candlestick',
'date': datetime_cols[0],
**ohlc_map,
'best_charts': ['candlestick']
})
# 3. Target/Goal Detection (Bullet)
target_keywords = ['target', 'goal', 'budget', 'quota', 'plan']
target_col = next((c for c in numeric_cols if any(k in c.lower() for k in target_keywords)), None)
actual_col = next((c for c in numeric_cols if c != target_col and 'id' not in c.lower()), None)
if target_col and actual_col:
relationships.append({
'type': 'bullet',
'column': actual_col,
'target': target_col,
'best_charts': ['bullet']
})
# 4. Multivariate Analysis (Parallel Coordinates)
if len(numeric_cols) >= 4:
relationships.append({
'type': 'multivariate',
'columns': numeric_cols[:5],
'category': categorical_cols[0] if categorical_cols else None,
'best_charts': ['parallel_coords']
})
# 5. Geospatial Detection (Maps) 🌍
geo_keywords = ['country', 'region', 'state', 'city', 'location', 'lat', 'lon', 'postal', 'zip']
geo_col = next((c for c in categorical_cols if any(k in c.lower() for k in geo_keywords)), None)
if geo_col and numeric_cols:
relationships.append({
'type': 'geospatial',
'location': geo_col,
'value': numeric_cols[0],
'best_charts': ['choropleth', 'scatter_geo']
})
# 6. 3D Correlation Detection (3D Scatter) 🧊
if len(numeric_cols) >= 3:
relationships.append({
'type': '3d_correlation',
'x': numeric_cols[0],
'y': numeric_cols[1],
'z': numeric_cols[2],
'color': categorical_cols[0] if categorical_cols else list(numeric_cols)[2],
'best_charts': ['scatter_3d', '3d_surface', '3d_bubble']
})
# 7. Temporal Activity (Calendar Heatmap) πŸ“…
if datetime_cols and len(datetime_cols) > 0:
relationships.append({
'type': 'temporal_heatmap',
'date': datetime_cols[0],
'value': numeric_cols[0] if numeric_cols else None,
'best_charts': ['calendar_heatmap']
})
# Funnel (for sequential/stage data)
if categorical_cols and numeric_cols:
cardinality = df[categorical_cols[0]].nunique()
if 3 <= cardinality <= 8:
relationships.append({
'type': 'funnel',
'category': categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['funnel']
})
# Waterfall (for changes/variance)
if len(numeric_cols) >= 2:
relationships.append({
'type': 'waterfall',
'values': numeric_cols[:5],
'best_charts': ['waterfall']
})
# Radar (for multi-metric comparison)
if len(numeric_cols) >= 3 and categorical_cols:
relationships.append({
'type': 'radar',
'metrics': numeric_cols[:6],
'category': categorical_cols[0],
'best_charts': ['radar']
})
# Gauge (for single metric with target)
if numeric_cols:
relationships.append({
'type': 'gauge',
'metric': numeric_cols[0],
'best_charts': ['gauge']
})
# Violin (for distribution comparison)
if categorical_cols and numeric_cols:
relationships.append({
'type': 'violin',
'category': categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['violin']
})
# ===========================================
# ADDITIONAL CHART RELATIONSHIPS FOR MORE VARIETY
# ===========================================
# NEW PREMIUM CHART RELATIONSHIPS
# Combo Bar-Line Chart (Power BI style dual-axis)
if len(numeric_cols) >= 2 and categorical_cols:
relationships.append({
'type': 'combo_bar_line',
'category': categorical_cols[0],
'bar_metric': numeric_cols[0],
'line_metric': numeric_cols[1],
'best_charts': ['combo_bar_line']
})
# Lollipop Chart (elegant ranking visualization)
if categorical_cols and numeric_cols:
relationships.append({
'type': 'lollipop',
'category': categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['lollipop']
})
# Diverging Bar (positive/negative from center)
if numeric_cols:
col = numeric_cols[0]
if df[col].min() < 0 or df[col].mean() != 0:
relationships.append({
'type': 'diverging_bar',
'category': categorical_cols[0] if categorical_cols else None,
'value': col,
'best_charts': ['diverging_bar']
})
# Slope Chart (period-over-period comparison)
if len(numeric_cols) >= 2:
relationships.append({
'type': 'slope',
'start_value': numeric_cols[0],
'end_value': numeric_cols[1],
'category': categorical_cols[0] if categorical_cols else None,
'best_charts': ['slope']
})
# Dumbbell Chart (before/after or comparison)
if len(numeric_cols) >= 2 and categorical_cols:
relationships.append({
'type': 'dumbbell',
'category': categorical_cols[0],
'value1': numeric_cols[0],
'value2': numeric_cols[1],
'best_charts': ['dumbbell']
})
# Range Plot (min-max visualization)
if numeric_cols and categorical_cols:
relationships.append({
'type': 'range_plot',
'category': categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['range_plot']
})
# Band Chart (with confidence intervals)
if numeric_cols:
relationships.append({
'type': 'band_chart',
'value': numeric_cols[0],
'best_charts': ['band_chart']
})
# Step Line Chart
if numeric_cols:
relationships.append({
'type': 'step_line',
'value': numeric_cols[0],
'best_charts': ['step_line']
})
# Comparison Bar (different from regular bar - shows comparison)
if categorical_cols and numeric_cols:
relationships.append({
'type': 'comparison_bar',
'categorical': categorical_cols[0],
'numeric': numeric_cols[0],
'best_charts': ['grouped_bar']
})
# Stacked Area (for cumulative trends)
if len(numeric_cols) >= 2:
relationships.append({
'type': 'stacked_trend',
'values': numeric_cols[:3],
'best_charts': ['stacked_area']
})
# Donut Breakdown (secondary category)
if len(categorical_cols) >= 2 and numeric_cols:
relationships.append({
'type': 'donut_breakdown',
'category': categorical_cols[1] if len(categorical_cols) > 1 else categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['donut']
})
# Line Trend (for any numeric progression)
if numeric_cols:
relationships.append({
'type': 'line_trend',
'value': numeric_cols[0],
'best_charts': ['line']
})
# Horizontal Ranking (top N items)
if categorical_cols and numeric_cols:
relationships.append({
'type': 'horizontal_ranking',
'category': categorical_cols[0],
'value': numeric_cols[0],
'best_charts': ['horizontal_bar']
})
# Grouped Comparison (if multiple numeric cols)
if len(numeric_cols) >= 2 and categorical_cols:
relationships.append({
'type': 'grouped_comparison',
'category': categorical_cols[0],
'values': numeric_cols[:3],
'best_charts': ['stacked_bar']
})
return relationships
def autonomous_chart_selection(df: pd.DataFrame, domain: str, relationships: List[Dict]) -> List[Dict]:
"""
ENHANCED AUTONOMOUS CHART SELECTION - AI decides everything!
Strategy:
1. GUARANTEE advanced charts (radar, gauge, funnel, heatmap, box) first
2. Score each relationship-chart combo based on data suitability
3. Ensure variety - no repeated chart types
4. Select best 12-15 visualizations
"""
selected_charts = []
used_chart_types = set()
palette = COLOR_PALETTES.get(domain, COLOR_PALETTES['general'])
# Shuffle relationships so every dataset generation feels unique and dynamic!
random.shuffle(relationships)
# Chart complexity tiers - prioritize complex charts for variety
COMPLEX_CHARTS = [
'3d_surface', 'polar_bar', 'density_contour', 'sankey', 'radar', 'sunburst', 'multi_sunburst', 'parcoords', 'bubble', 'violin', 'waterfall', 'candlestick',
'combo_bar_line', 'scatter_matrix', 'marimekko', 'ridgeline', 'band_chart', '3d_bubble', 'hexbin'
]
MEDIUM_CHARTS = [
'heatmap', 'stacked_bar', 'funnel', 'gauge', 'treemap', 'box', 'area', 'pareto',
'lollipop', 'dumbbell', 'slope', 'diverging_bar', 'pyramid', 'range_plot', 'step_line'
]
SIMPLE_CHARTS = ['bar', 'line', 'pie', 'scatter', 'histogram', 'horizontal_bar', 'donut', 'waffle']
# =============================================================
# PHASE 1: GUARANTEE ADVANCED CHARTS (User requested variety)
# Premium enterprise-grade visualizations
# =============================================================
must_have_advanced = [
'radar', 'gauge', 'funnel', 'heatmap', 'box', 'violin',
'combo_bar_line', 'pareto', 'lollipop', 'diverging_bar',
'sankey', 'choropleth', 'sunburst'
]
random.shuffle(must_have_advanced)
for adv_chart in must_have_advanced:
if adv_chart in used_chart_types:
continue
# Find a suitable relationship for this chart type
for rel in relationships:
if adv_chart in rel.get('best_charts', []):
chart = generate_autonomous_chart(df, rel, adv_chart, palette)
if chart:
selected_charts.append(chart)
used_chart_types.add(adv_chart)
break
# If no direct match, try with compatible relationship types
if adv_chart not in used_chart_types:
for rel in relationships:
rel_type = rel.get('type', '')
compatible = False
if adv_chart == 'radar' and rel_type in ['radar', 'multi_category', 'num_vs_cat']:
compatible = True
elif adv_chart == 'gauge' and rel_type in ['gauge', 'distribution']:
compatible = True
elif adv_chart == 'funnel' and rel_type in ['funnel', 'num_vs_cat']:
compatible = True
elif adv_chart == 'heatmap' and rel_type in ['num_vs_num', 'multi_category', 'category_comparison']:
compatible = True
elif adv_chart == 'box' and rel_type in ['distribution', 'violin']:
compatible = True
elif adv_chart == 'violin' and rel_type in ['violin', 'distribution', 'num_vs_cat']:
compatible = True
elif adv_chart == 'sankey' and rel_type in ['sankey', 'flow', 'multi_category']:
compatible = True
elif adv_chart == 'choropleth' and rel_type in ['geo', 'location', 'num_vs_cat']:
compatible = True
elif adv_chart == 'sunburst' and rel_type in ['hierarchy', 'multi_category']:
compatible = True
if compatible:
chart = generate_autonomous_chart(df, rel, adv_chart, palette)
if chart:
selected_charts.append(chart)
used_chart_types.add(adv_chart)
break
logger.info(f"🌟 Phase 1: Guaranteed {len(selected_charts)} advanced charts: {list(used_chart_types)}")
# =============================================================
# PHASE 2: SCORED SELECTION FOR REMAINING CHARTS
# =============================================================
def calculate_chart_score(rel: Dict, chart_type: str) -> float:
"""Score how well a chart type fits the data relationship"""
score = 0.0
rel_type = rel.get('type', '')
# Complexity bonus - prefer complex charts for dashboard variety
if chart_type in COMPLEX_CHARTS:
score += 30
elif chart_type in MEDIUM_CHARTS:
score += 20
else:
score += 10
# Data suitability scoring
if rel_type == 'num_vs_cat':
if chart_type in ['bar', 'horizontal_bar', 'pie', 'donut', 'treemap', 'funnel']:
score += 25
elif rel_type == 'num_vs_num':
if chart_type in ['scatter', 'bubble', 'heatmap', 'parcoords']:
score += 25
elif rel_type == 'time_series':
if chart_type in ['line', 'area', 'candlestick']:
score += 25
elif rel_type == 'multi_category':
if chart_type in ['sunburst', 'treemap', 'sankey', 'stacked_bar']:
score += 25
elif rel_type == 'distribution':
if chart_type in ['histogram', 'box', 'violin']:
score += 25
elif rel_type == 'comparison':
if chart_type in ['radar', 'gauge', 'bullet', 'bar']:
score += 25
# Category count based scoring
if 'categorical_cols' in rel:
cat_count = len(rel.get('categorical_cols', []))
if cat_count >= 2 and chart_type in ['sunburst', 'sankey', 'parcoords']:
score += 15
elif cat_count == 1 and chart_type in ['bar', 'pie', 'treemap']:
score += 10
# Random small factor for diversity
score += random.uniform(0, 5)
return score
# Build scored list of all possible chart options
chart_options = []
for rel in relationships:
for chart_type in rel.get('best_charts', ['bar']):
if chart_type not in used_chart_types:
score = calculate_chart_score(rel, chart_type)
chart_options.append({
'rel': rel,
'chart_type': chart_type,
'score': score
})
# Sort by score descending (highest priority first)
chart_options.sort(key=lambda x: x['score'], reverse=True)
# Select top charts ensuring variety - MAXIMUM 25 UNIQUE CHARTS for dense layout
for option in chart_options:
if len(selected_charts) >= 25: # Dense Power BI-style layout
break
chart_type = option['chart_type']
if chart_type in used_chart_types:
continue # STRICT: Never duplicate chart types
# Generate chart
chart = generate_autonomous_chart(df, option['rel'], chart_type, palette)
if chart:
selected_charts.append(chart)
used_chart_types.add(chart_type)
# If we have less than 15 charts, try adding more from all tiers
if len(selected_charts) < 15:
all_chart_types = COMPLEX_CHARTS + MEDIUM_CHARTS + SIMPLE_CHARTS
for rel in relationships:
if len(selected_charts) >= 20:
break
for chart_type in all_chart_types:
if chart_type not in used_chart_types:
chart = generate_autonomous_chart(df, rel, chart_type, palette)
if chart:
selected_charts.append(chart)
used_chart_types.add(chart_type)
break
logger.info(f"🎨 Generated {len(selected_charts)} UNIQUE charts: {list(used_chart_types)}")
return selected_charts
def generate_autonomous_chart(df: pd.DataFrame, relationship: Dict, chart_type: str, palette: Dict) -> Optional[Dict]:
"""
Generate a chart configuration autonomously based on data relationship.
All data is REAL - calculated with pandas!
"""
try:
rel_type = relationship['type']
colors = palette['chart']
if rel_type == 'num_vs_cat':
cat_col = relationship['categorical']
num_col = relationship['numeric']
grouped = df.groupby(cat_col)[num_col].sum().nlargest(10)
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
if chart_type in ['pie', 'donut']:
return create_pie_chart(labels, values, num_col, cat_col, chart_type, colors)
elif chart_type == 'treemap':
return create_treemap_chart(labels, values, num_col, cat_col, colors)
elif chart_type == 'horizontal_bar':
return create_horizontal_bar(labels, values, num_col, cat_col, colors)
elif chart_type == 'funnel':
# Funnel needs special relationship, fallback to bar for num_vs_cat
return create_bar_chart(labels, values, num_col, cat_col, colors)
elif chart_type == 'pareto':
# Power BI-style Pareto (80/20) chart
return create_pareto_chart(df, {'category': cat_col, 'value': num_col}, colors)
elif chart_type == 'radar':
# Radar needs multi-metrics, fallback to bar for single metric
return create_bar_chart(labels, values, num_col, cat_col, colors)
elif chart_type == 'polar_bar':
return create_polar_bar_chart(df, relationship, colors)
else:
return create_bar_chart(labels, values, num_col, cat_col, colors)
elif rel_type == 'num_vs_num':
x_col = relationship['x']
y_col = relationship['y']
sample = df[[x_col, y_col]].dropna().head(150)
if chart_type == 'bubble':
# SAFER SIZE CALCULATION
y_min = sample[y_col].min()
y_max = sample[y_col].max()
# Avoid division by zero
y_range = y_max - y_min
if y_range == 0:
sizes = np.full(len(sample), 15)
else:
# Normalize to 0-1 then scale to 5-35 range
normalized = (sample[y_col] - y_min) / y_range
sizes = normalized * 30 + 5
# Ensure sizes are JSON serializable (convert to list of floats)
# Handle potential NaNs by filling with default size
sizes = sizes.fillna(15).tolist() if hasattr(sizes, 'fillna') else sizes
return create_bubble_chart(sample[x_col].tolist(), sample[y_col].tolist(), sizes, x_col, y_col, colors)
elif chart_type == 'heatmap':
# Density heatmap
return create_correlation_heatmap(df, colors)
elif chart_type == 'density_contour':
return create_density_contour_chart(df, relationship, colors)
else:
return create_scatter_chart(sample[x_col].tolist(), sample[y_col].tolist(), x_col, y_col, colors)
elif rel_type == 'time_series':
num_col = relationship['value']
time_col = relationship['time']
timed = df[[time_col, num_col]].copy()
timed[time_col] = pd.to_datetime(timed[time_col], errors='coerce')
timed = timed.dropna().sort_values(time_col)
if timed.empty:
return None
span_days = max(1, (timed[time_col].max() - timed[time_col].min()).days)
frequency = 'M' if span_days > 90 else 'W' if span_days > 21 else 'D'
grouped = timed.set_index(time_col)[num_col].resample(frequency).sum().tail(24)
labels = [index.strftime('%b %Y') if frequency == 'M' else index.strftime('%d %b') for index in grouped.index]
values = [float(value) for value in grouped.values]
if chart_type == 'area':
return create_area_chart(labels, values, num_col, colors)
else:
return create_line_chart(labels, values, num_col, colors)
elif rel_type == 'distribution':
col = relationship['column']
if chart_type == 'histogram':
return create_histogram(df[col].dropna().tolist(), col, colors)
elif chart_type == 'box':
return create_box_plot(df, [col], colors)
else:
# Create area from distribution
values = df[col].dropna().head(30).tolist()
labels = [f'{i+1}' for i in range(len(values))]
return create_area_chart(labels, values, col, colors)
elif rel_type == 'multi_category':
if chart_type in ['sunburst', 'treemap']:
# Create sunburst/treemap from multi-category
cat1 = relationship['cat1']
val_col = relationship['value']
grouped = df.groupby(cat1)[val_col].sum().nlargest(10)
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
if chart_type == 'sunburst':
return create_sunburst_chart(labels, values, cat1, colors)
else:
return create_treemap_chart(labels, values, val_col, cat1, colors)
else:
return create_stacked_bar(df, relationship, colors)
elif rel_type == 'sankey':
return create_sankey_chart(df, relationship, colors)
elif rel_type == 'candlestick':
return create_candlestick_chart(df, relationship, colors)
elif rel_type == 'bullet':
return create_bullet_chart(df, relationship, colors)
elif rel_type == 'multivariate':
return create_parallel_coords_chart(df, relationship, colors)
elif rel_type == 'funnel':
return create_funnel_chart(df, relationship, colors)
elif rel_type == 'geospatial':
return create_choropleth_chart(df, relationship, colors)
elif rel_type == '3d_correlation':
if chart_type == '3d_surface':
return create_3d_surface_chart(df, relationship, colors)
elif chart_type == '3d_bubble':
return create_3d_bubble_chart(df, relationship, colors)
else:
return create_scatter3d_chart(df, relationship, colors)
elif rel_type == 'temporal_heatmap':
return create_calendar_heatmap(df, relationship, colors)
elif rel_type == 'waterfall':
return create_waterfall_chart(df, relationship, colors)
elif rel_type == 'radar':
return create_radar_chart(df, relationship, colors)
elif rel_type == 'gauge':
return create_gauge_chart(df, relationship, colors)
elif rel_type == 'violin':
return create_violin_chart(df, relationship, colors)
# ===========================================
# CATEGORICAL-ONLY DATA CHART HANDLERS
# ===========================================
elif rel_type == 'category_distribution':
# Single category frequency distribution
cat_col = relationship['categorical']
counts = df[cat_col].value_counts().head(10)
labels = [str(l)[:20] for l in counts.index]
values = [int(v) for v in counts.values]
if chart_type in ['pie', 'donut']:
return create_pie_chart(labels, values, 'Count', cat_col, chart_type, colors)
elif chart_type == 'horizontal_bar':
return create_horizontal_bar(labels, values, 'Count', cat_col, colors)
elif chart_type == 'treemap':
return create_treemap_chart(labels, values, 'Count', cat_col, colors)
else:
return create_bar_chart(labels, values, 'Count', cat_col, colors)
elif rel_type == 'category_comparison':
# Cross-tabulation visualization
cat1 = relationship['cat1']
cat2 = relationship['cat2']
# SAFE CROSSTAB: Limit to top 20 categories to prevent memory overflow
top_cat1 = df[cat1].value_counts().head(20).index
top_cat2 = df[cat2].value_counts().head(20).index
mask = df[cat1].isin(top_cat1) & df[cat2].isin(top_cat2)
cross = pd.crosstab(df.loc[mask, cat1], df.loc[mask, cat2])
if chart_type in ['stacked_bar', 'grouped_bar']:
# Create stacked/grouped bar from crosstab
data_traces = []
for col in cross.columns[:6]:
data_traces.append({
"type": "bar",
"name": str(col)[:15],
"x": [str(idx)[:15] for idx in cross.index[:10]],
"y": cross[col].head(10).tolist(),
"marker": {"color": colors[len(data_traces) % len(colors)]}
})
return {
"data": data_traces,
"layout": {
"title": f"πŸ“Š {cat1.replace('_', ' ').title()} by {cat2.replace('_', ' ').title()}",
"barmode": "stack" if chart_type == 'stacked_bar' else "group",
"height": 400,
"paper_bgcolor": "rgba(0,0,0,0)",
"plot_bgcolor": "rgba(0,0,0,0)",
"font": {"color": "#e5e7eb"}
}
}
elif chart_type == 'sunburst':
return create_sunburst_chart(
[str(idx)[:15] for idx in cross.index[:8]],
cross.sum(axis=1).head(8).tolist(),
cat1,
colors
)
elif chart_type == 'heatmap':
# Create heatmap from crosstab
return {
"data": [{
"type": "heatmap",
"z": cross.head(10).iloc[:, :10].values.tolist(),
"x": [str(c)[:12] for c in cross.columns[:10]],
"y": [str(idx)[:12] for idx in cross.index[:10]],
"colorscale": "Teal"
}],
"layout": {
"title": f"πŸ”₯ {cat1.replace('_', ' ').title()} vs {cat2.replace('_', ' ').title()}",
"height": 400,
"paper_bgcolor": "rgba(0,0,0,0)",
"plot_bgcolor": "rgba(0,0,0,0)",
"font": {"color": "#e5e7eb"}
}
}
else:
# Fallback to bar
counts = df[cat1].value_counts().head(10)
return create_bar_chart(
[str(l)[:20] for l in counts.index],
counts.values.tolist(),
'Count',
cat1,
colors
)
elif rel_type == 'category_hierarchy':
# Multi-level category visualization
categories = relationship['categories']
if len(categories) >= 2 and chart_type in ['sunburst', 'treemap']:
# Build hierarchical data
cat1, cat2 = categories[0], categories[1]
grouped = df.groupby([cat1, cat2]).size().reset_index(name='count')
grouped = grouped.nlargest(20, 'count')
labels = []
parents = []
values = []
# Add root categories
for cat in grouped[cat1].unique()[:8]:
labels.append(str(cat)[:15])
parents.append("")
values.append(int(grouped[grouped[cat1] == cat]['count'].sum()))
# Add child categories
for _, row in grouped.head(15).iterrows():
labels.append(f"{str(row[cat2])[:12]}")
parents.append(str(row[cat1])[:15])
values.append(int(row['count']))
if chart_type == 'sunburst':
return {
"data": [{
"type": "sunburst",
"labels": labels,
"parents": parents,
"values": values,
"marker": {"colors": colors * 3}
}],
"layout": {
"title": f"🌞 {cat1.replace('_', ' ').title()} β†’ {cat2.replace('_', ' ').title()}",
"height": 400,
"paper_bgcolor": "rgba(0,0,0,0)",
"font": {"color": "#e5e7eb"}
}
}
else: # treemap
return {
"data": [{
"type": "treemap",
"labels": labels,
"parents": parents,
"values": values,
"marker": {"colors": colors * 3}
}],
"layout": {
"title": f"πŸ—‚οΈ {cat1.replace('_', ' ').title()} β†’ {cat2.replace('_', ' ').title()}",
"height": 400,
"paper_bgcolor": "rgba(0,0,0,0)",
"font": {"color": "#e5e7eb"}
}
}
# Fallback for single category
cat = categories[0]
counts = df[cat].value_counts().head(10)
return create_pie_chart(
[str(l)[:20] for l in counts.index],
counts.values.tolist(),
'Count',
cat,
'donut',
colors
)
# ===========================================
# NEW CHART HANDLERS FOR ADDITIONAL VARIETY
# ===========================================
elif rel_type == 'comparison_bar':
cat_col = relationship['categorical']
num_col = relationship['numeric']
grouped = df.groupby(cat_col)[num_col].mean().nlargest(10)
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
return {
'chart_id': f'comparison_{num_col}_{cat_col}',
'title': f'Average {num_col.replace("_", " ").title()} Comparison',
'type': 'grouped_bar',
'plotly_config': {
'data': [{'type': 'bar', 'x': labels, 'y': values, 'marker': {'color': colors[:len(labels)]}}],
'layout': get_layout()
}
}
elif rel_type == 'stacked_trend':
values_cols = relationship['values']
n_points = min(15, len(df))
traces = []
for i, col in enumerate(values_cols[:3]):
segment_size = max(1, len(df) // n_points)
vals = [float(df[col].iloc[j*segment_size:(j+1)*segment_size].sum()) for j in range(n_points)]
traces.append({
'type': 'scatter',
'mode': 'lines',
'fill': 'tonexty' if i > 0 else 'tozeroy',
'name': col.replace('_', ' ').title()[:15],
'x': list(range(1, n_points + 1)),
'y': vals,
'line': {'color': colors[i % len(colors)]}
})
return {
'chart_id': f'stacked_area_trend',
'title': 'Cumulative Trends',
'type': 'stacked_area',
'plotly_config': {'data': traces, 'layout': get_layout()}
}
elif rel_type == 'donut_breakdown':
cat_col = relationship['category']
val_col = relationship['value']
grouped = df.groupby(cat_col)[val_col].sum().nlargest(8)
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
return create_pie_chart(labels, values, val_col, cat_col, 'donut', colors)
elif rel_type == 'line_trend':
val_col = relationship['value']
n_points = min(20, len(df))
segment_size = max(1, len(df) // n_points)
values = [float(df[val_col].iloc[i*segment_size:(i+1)*segment_size].mean()) for i in range(n_points)]
return create_line_chart(
[f'P{i+1}' for i in range(n_points)],
values,
val_col,
colors
)
elif rel_type == 'horizontal_ranking':
cat_col = relationship['category']
val_col = relationship['value']
grouped = df.groupby(cat_col)[val_col].sum().nlargest(10)
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
return create_horizontal_bar(labels, values, val_col, cat_col, colors)
elif rel_type == 'grouped_comparison':
cat_col = relationship['category']
value_cols = relationship['values']
grouped = df.groupby(cat_col)[value_cols].mean().head(8)
traces = []
for i, col in enumerate(value_cols[:3]):
traces.append({
'type': 'bar',
'name': col.replace('_', ' ').title()[:15],
'x': [str(idx)[:12] for idx in grouped.index],
'y': [float(v) for v in grouped[col].values],
'marker': {'color': colors[i % len(colors)]}
})
return {
'chart_id': f'grouped_{cat_col}',
'title': f'Metrics Comparison by {cat_col.replace("_", " ").title()}',
'type': 'stacked_bar',
'plotly_config': {
'data': traces,
'layout': {**get_layout(), 'barmode': 'group'}
}
}
# ===========================================
# NEW PREMIUM CHART TYPE HANDLERS
# ===========================================
elif rel_type == 'combo_bar_line':
return create_combo_bar_line_chart(df, relationship, colors)
elif rel_type == 'lollipop':
return create_lollipop_chart(df, relationship, colors)
elif rel_type == 'diverging_bar':
return create_diverging_bar_chart(df, relationship, colors)
elif rel_type == 'slope':
return create_slope_chart(df, relationship, colors)
elif rel_type == 'dumbbell':
return create_dumbbell_chart(df, relationship, colors)
elif rel_type == 'range_plot':
return create_range_plot(df, relationship, colors)
elif rel_type == 'band_chart':
return create_band_chart(df, relationship, colors)
elif rel_type == 'step_line':
return create_step_line_chart(df, relationship, colors)
except Exception as e:
logger.warning(f"Chart generation error: {e}")
return None
# ================= CHART CREATORS (All Plotly configs) =================
def create_bar_chart(labels, values, num_col, cat_col, colors):
return {
'chart_id': f'bar_{num_col}_{cat_col}',
'title': f'{num_col.replace("_", " ").title()} by {cat_col.replace("_", " ").title()}',
'type': 'bar',
'plotly_config': {
'data': [{'type': 'bar', 'x': labels, 'y': values, 'marker': {'color': colors, 'line': {'width': 0}}}],
'layout': get_layout()
}
}
def create_horizontal_bar(labels, values, num_col, cat_col, colors):
return {
'chart_id': f'hbar_{num_col}_{cat_col}',
'title': f'{cat_col.replace("_", " ").title()} Ranking',
'type': 'horizontal_bar',
'plotly_config': {
'data': [{'type': 'bar', 'y': labels, 'x': values, 'orientation': 'h', 'marker': {'color': colors[:len(values)]}}],
'layout': {**get_layout(), 'yaxis': {'automargin': True}}
}
}
def create_pie_chart(labels, values, num_col, cat_col, chart_type, colors):
return {
'chart_id': f'{chart_type}_{cat_col}',
'title': f'{cat_col.replace("_", " ").title()} Distribution',
'type': chart_type,
'plotly_config': {
'data': [{
'type': 'pie', 'labels': labels, 'values': values,
'hole': 0.5 if chart_type == 'donut' else 0,
'textinfo': 'percent', 'textfont': {'color': '#fff', 'size': 10},
'marker': {'colors': colors[:len(labels)]}
}],
'layout': {**get_layout(), 'showlegend': True, 'legend': {'font': {'size': 9, 'color': '#9ca3af'}, 'orientation': 'h', 'y': -0.1}}
}
}
def create_sunburst_chart(labels, values, cat_col, colors):
"""Create a Sunburst chart for hierarchical category breakdown"""
return {
'chart_id': f'sunburst_{cat_col}',
'title': f'{cat_col.replace("_", " ").title()} Sunburst',
'type': 'sunburst',
'plotly_config': {
'data': [{
'type': 'sunburst',
'labels': labels,
'parents': [''] * len(labels), # Flat sunburst (no hierarchy)
'values': values,
'branchvalues': 'total',
'marker': {'colors': colors[:len(labels)]},
'textinfo': 'label+percent entry'
}],
'layout': {**get_layout(), 'margin': {'l': 10, 'r': 10, 't': 30, 'b': 10}}
}
}
def create_treemap_chart(labels, values, num_col, cat_col, colors):
return {
'chart_id': f'treemap_{cat_col}',
'title': f'{cat_col.replace("_", " ").title()} Breakdown',
'type': 'treemap',
'plotly_config': {
'data': [{
'type': 'treemap',
'labels': labels,
'parents': [''] * len(labels),
'values': values,
'textinfo': 'label+percent entry',
'marker': {'colors': colors[:len(labels)]}
}],
'layout': get_layout()
}
}
def create_scatter_chart(x_vals, y_vals, x_col, y_col, colors):
data = [{
'type': 'scatter', 'mode': 'markers',
'x': x_vals, 'y': y_vals,
'name': 'Data',
'marker': {'size': 8, 'color': colors[0], 'opacity': 0.7, 'line': {'width': 1, 'color': 'rgba(255,255,255,0.3)'}}
}]
# Add Trendline - Silicon Valley Feature
try:
# Simple linear regression
if len(x_vals) > 1:
z = np.polyfit(list(range(len(x_vals))), y_vals, 1)
p = np.poly1d(z)
trend = p(list(range(len(x_vals))))
data.append({
'type': 'scatter', 'mode': 'lines',
'x': x_vals, 'y': trend.tolist(), # Fix: Convert numpy array to list
'name': 'Trend',
'line': {'color': colors[1], 'width': 2, 'dash': 'dash'}
})
except:
pass
return {
'chart_id': f'scatter_{x_col}_{y_col}',
'title': f'{y_col.replace("_", " ")} vs {x_col.replace("_", " ")}',
'type': 'scatter',
'plotly_config': {
'data': data,
'layout': {**get_layout(), 'showlegend': False}
}
}
def create_bubble_chart(x_vals, y_vals, sizes, x_col, y_col, colors):
return {
'chart_id': f'bubble_{x_col}_{y_col}',
'title': f'{y_col.replace("_", " ")} vs {x_col.replace("_", " ")} (Size)',
'type': 'bubble',
'plotly_config': {
'data': [{
'type': 'scatter', 'mode': 'markers',
'x': x_vals, 'y': y_vals,
'marker': {'size': sizes, 'color': y_vals, 'colorscale': [[0, colors[0]], [1, colors[-1]]], 'opacity': 0.6}
}],
'layout': get_layout()
}
}
def create_line_chart(labels, values, col, colors):
# Add simple forecast (3 periods) - Silicon Valley Feature
try:
if len(values) >= 3:
last_3_avg_diff = (values[-1] - values[-4]) / 3
forecast_values = [float(values[-1] + last_3_avg_diff * (i+1)) for i in range(3)] # Ensure floats
forecast_labels = [f'Forecast {i+1}' for i in range(3)]
# Main Line
data = [{
'type': 'scatter', 'mode': 'lines+markers',
'x': labels, 'y': values,
'name': 'Actual',
'line': {'color': colors[0], 'width': 3, 'shape': 'spline'},
'marker': {'size': 6, 'color': colors[0]}
}]
# Forecast Line (Dotted)
data.append({
'type': 'scatter', 'mode': 'lines+markers',
'x': [labels[-1]] + forecast_labels,
'y': [values[-1]] + forecast_values,
'name': 'Forecast',
'line': {'color': colors[0], 'width': 3, 'dash': 'dot'},
'marker': {'size': 6, 'opacity': 0.7}
})
else:
data = [{'type': 'scatter', 'mode': 'lines+markers', 'x': labels, 'y': values, 'line': {'color': colors[0]}, 'marker': {'color': colors[0]}}]
except:
data = [{'type': 'scatter', 'mode': 'lines+markers', 'x': labels, 'y': values, 'line': {'color': colors[0]}, 'marker': {'color': colors[0]}}]
return {
'chart_id': f'line_{col}',
'title': f'{col.replace("_", " ").title()} Trend + Forecast',
'type': 'line',
'plotly_config': {
'data': data,
'layout': {**get_layout(), 'showlegend': True, 'legend': {'orientation': 'h', 'y': 1.1}}
}
}
def create_area_chart(labels, values, col, colors):
return {
'chart_id': f'area_{col}',
'title': f'{col.replace("_", " ").title()} Over Time',
'type': 'area',
'plotly_config': {
'data': [{
'type': 'scatter', 'mode': 'lines',
'x': labels, 'y': values,
'fill': 'tozeroy', 'fillcolor': f'{colors[0]}40',
'line': {'color': colors[0], 'width': 2, 'shape': 'spline'}
}],
'layout': get_layout()
}
}
def create_histogram(values, col, colors):
return {
'chart_id': f'hist_{col}',
'title': f'{col.replace("_", " ").title()} Distribution',
'type': 'histogram',
'plotly_config': {
'data': [{'type': 'histogram', 'x': values, 'marker': {'color': colors[0]}, 'nbinsx': 15}],
'layout': get_layout()
}
}
def create_box_plot(df, columns, colors):
data = []
for i, col in enumerate(columns):
data.append({'type': 'box', 'y': df[col].dropna().tolist()[:200], 'name': col, 'marker': {'color': colors[i % len(colors)]}})
return {
'chart_id': f'box_{columns[0]}',
'title': 'Distribution Analysis',
'type': 'box',
'plotly_config': {'data': data, 'layout': get_layout()}
}
def create_correlation_heatmap(df, colors):
numeric_df = df.select_dtypes(include=['int64', 'float64']).iloc[:, :6]
if len(numeric_df.columns) < 2:
return None
corr = numeric_df.corr()
return {
'chart_id': 'heatmap_corr',
'title': 'Correlation Matrix',
'type': 'heatmap',
'plotly_config': {
'data': [{
'type': 'heatmap',
'z': corr.values.tolist(),
'x': corr.columns.tolist(),
'y': corr.columns.tolist(),
'colorscale': [[0, colors[-1]], [0.5, '#1f2937'], [1, colors[0]]],
'showscale': True
}],
'layout': get_layout()
}
}
def create_stacked_bar(df, rel, colors):
cat1 = rel['cat1']
cat2 = rel['cat2']
# Handle both numeric and categorical-only data
if 'value' in rel and rel['value'] in df.columns:
val = rel['value']
# Safe Pivot: Limit to top categories first
top_cat1 = df[cat1].value_counts().head(20).index
top_cat2 = df[cat2].value_counts().head(20).index
mask = df[cat1].isin(top_cat1) & df[cat2].isin(top_cat2)
pivot = df[mask].groupby([cat1, cat2])[val].sum().unstack(fill_value=0)
title_prefix = val.replace("_", " ")
else:
# Categorical-only: use counts
# Safe Crosstab: Limit to top categories first
top_cat1 = df[cat1].value_counts().head(20).index
top_cat2 = df[cat2].value_counts().head(20).index
mask = df[cat1].isin(top_cat1) & df[cat2].isin(top_cat2)
pivot = pd.crosstab(df.loc[mask, cat1], df.loc[mask, cat2])
title_prefix = "Count"
pivot = pivot.iloc[:8, :5] # Limit size
data = []
for i, col in enumerate(pivot.columns):
y_values = pivot[col].tolist()
# Ensure we have valid numeric data
y_values = [int(v) if pd.notna(v) else 0 for v in y_values]
data.append({
'type': 'bar', 'name': str(col)[:15],
'x': [str(x)[:15] for x in pivot.index], # Truncate long labels
'y': y_values,
'marker': {'color': colors[i % len(colors)]}
})
return {
'chart_id': f'stacked_{cat1}_{cat2}',
'title': f'{title_prefix} by {cat1.replace("_", " ").title()} and {cat2.replace("_", " ").title()}',
'type': 'stacked_bar',
'plotly_config': {'data': data, 'layout': {**get_layout(), 'barmode': 'stack'}}
}
def create_funnel_chart(df, rel, colors):
cat = rel.get('category') or rel.get('categorical')
# Handle both numeric and categorical-only data
if 'value' in rel and rel['value'] in df.columns:
val = rel['value']
grouped = df.groupby(cat)[val].sum().sort_values(ascending=False).head(6)
title_prefix = val.replace("_", " ").title()
else:
# Categorical-only: use counts
grouped = df[cat].value_counts().head(6)
title_prefix = "Count"
return {
'chart_id': f'funnel_{cat}',
'title': f'{title_prefix} Conversion Funnel',
'type': 'funnel',
'plotly_config': {
'data': [{
'type': 'funnel',
'y': [str(idx)[:20] for idx in grouped.index],
'x': [int(v) for v in grouped.values],
'textinfo': 'value+percent initial',
'marker': {'color': colors[:len(grouped)]}
}],
'layout': get_layout()
}
}
def create_pareto_chart(df, rel, colors):
"""
POWER BI PARETO CHART - 80/20 Analysis
Shows bars sorted by value with cumulative % line overlay
"""
cat = rel.get('category') or rel.get('categorical')
val = rel.get('value') or rel.get('numeric')
if not cat or not val or cat not in df.columns or val not in df.columns:
return None
# Group and sort by value descending
grouped = df.groupby(cat)[val].sum().sort_values(ascending=False).head(15)
total = grouped.sum()
if total == 0:
return None
# Calculate cumulative percentage
cumsum = grouped.cumsum()
cum_pct = (cumsum / total * 100).tolist()
labels = [str(l)[:20] for l in grouped.index]
values = [float(v) for v in grouped.values]
return {
'chart_id': f'pareto_{cat}_{val}',
'title': f'Pareto Analysis: {val.replace("_", " ").title()} by {cat.replace("_", " ").title()}',
'type': 'pareto',
'analysis': 'power_bi_80_20',
'plotly_config': {
'data': [
# Bar chart (values)
{
'type': 'bar',
'name': val.replace('_', ' ').title(),
'x': labels,
'y': values,
'marker': {'color': colors[0]},
'yaxis': 'y'
},
# Cumulative line
{
'type': 'scatter',
'mode': 'lines+markers',
'name': 'Cumulative %',
'x': labels,
'y': cum_pct,
'line': {'color': colors[1], 'width': 3},
'marker': {'size': 8, 'color': colors[1]},
'yaxis': 'y2'
},
# 80% threshold line
{
'type': 'scatter',
'mode': 'lines',
'name': '80% Threshold',
'x': [labels[0], labels[-1]],
'y': [80, 80],
'line': {'color': '#ef4444', 'width': 2, 'dash': 'dash'},
'yaxis': 'y2',
'showlegend': True,
'hoverinfo': 'skip'
}
],
'layout': {
**get_layout(),
'yaxis': {'title': val.replace('_', ' ').title(), 'side': 'left'},
'yaxis2': {'title': 'Cumulative %', 'overlaying': 'y', 'side': 'right', 'range': [0, 105]},
'legend': {'orientation': 'h', 'y': -0.2}
}
}
}
def create_waterfall_chart(df, rel, colors):
vals = rel['values']
sums = [df[v].sum() for v in vals]
names = [v.replace('_', ' ').title() for v in vals]
# Calculate deltas for waterfall effect
deltas = [sums[0]]
for i in range(1, len(sums)):
deltas.append(sums[i] - sums[i-1])
return {
'chart_id': 'waterfall_variance',
'title': 'Metric Evolution',
'type': 'waterfall',
'plotly_config': {
'data': [{
'type': 'waterfall',
'measure': ['absolute'] + ['relative'] * (len(sums)-1),
'x': names,
'y': [sums[0]] + [sums[i]-sums[i-1] for i in range(1, len(sums))],
'connector': {'line': {'color': '#6366f1'}},
'decreasing': {'marker': {'color': '#ef4444'}},
'increasing': {'marker': {'color': '#10b981'}},
'totals': {'marker': {'color': '#3b82f6'}}
}],
'layout': get_layout()
}
}
def create_radar_chart(df, rel, colors):
cats = rel['category']
metrics = rel['metrics']
# Normalize metrics to 0-1 scale for fair comparison
top_items = df.groupby(cats)[metrics[0]].sum().nlargest(3).index.tolist()
data = []
for i, item in enumerate(top_items):
item_data = df[df[cats] == item][metrics].sum()
# Simple normalization for visualization
normalized_values = (item_data / df[metrics].sum() * 5).tolist()
data.append({
'type': 'scatterpolar',
'r': normalized_values,
'theta': [m.replace('_', ' ').title() for m in metrics],
'fill': 'toself',
'name': str(item)
})
return {
'chart_id': f'radar_{cats}',
'title': f'Multi-Metric Comparison: {cats}',
'type': 'radar',
'plotly_config': {
'data': data,
'layout': {
**get_layout(),
'polar': {
'radialaxis': {'visible': True, 'range': [0, max([max(d['r']) for d in data]) * 1.1]},
'bgcolor': 'rgba(0,0,0,0)'
}
}
}
}
def create_gauge_chart(df, rel, colors):
metric = rel['metric']
value = df[metric].mean()
target = df[metric].max() * 0.8
return {
'chart_id': f'gauge_{metric}',
'title': f'Average {metric.replace("_", " ").title()}',
'type': 'gauge',
'plotly_config': {
'data': [{
'type': 'indicator',
'mode': 'gauge+number+delta',
'value': value,
'delta': {'reference': target},
'gauge': {
'axis': {'range': [None, df[metric].max()]},
'bar': {'color': colors[0]},
'steps': [
{'range': [0, target*0.5], 'color': f'{colors[0]}40'},
{'range': [target*0.5, target], 'color': f'{colors[0]}80'}
]
}
}],
'layout': {'paper_bgcolor': 'rgba(0,0,0,0)', 'font': {'color': '#9ca3af'}}
}
}
def create_violin_chart(df, rel, colors):
cat = rel['category']
val = rel['value']
top_cats = df[cat].value_counts().head(5).index.tolist()
filtered_df = df[df[cat].isin(top_cats)]
data = []
for i, c in enumerate(top_cats):
data.append({
'type': 'violin',
'x': filtered_df[filtered_df[cat] == c][cat].tolist(),
'y': filtered_df[filtered_df[cat] == c][val].tolist(),
'name': str(c),
'box': {'visible': True},
'meanline': {'visible': True},
'line': {'color': colors[i % len(colors)]}
})
return {
'chart_id': f'violin_{cat}_{val}',
'title': f'{val.replace("_", " ")} Distribution by {cat}',
'type': 'violin',
'plotly_config': {
'data': data,
'layout': get_layout()
}
}
def create_sankey_chart(df, rel, colors):
src = rel['source']
tgt = rel['target']
val = rel['value']
# Aggregation for flow
df_agg = df.groupby([src, tgt])[val].sum().reset_index()
# Limit to top 50 flows for readability
if len(df_agg) > 50:
df_agg = df_agg.nlargest(50, val)
# Create unique node labels
all_nodes = list(pd.concat([df_agg[src], df_agg[tgt]]).unique())
node_map = {node: i for i, node in enumerate(all_nodes)}
return {
'chart_id': f'sankey_{src}_{tgt}',
'title': f'Flow: {src.title()} to {tgt.title()}',
'type': 'sankey',
'plotly_config': {
'data': [{
'type': 'sankey',
'node': {
'pad': 15,
'thickness': 20,
'line': {'color': 'black', 'width': 0.5},
'label': all_nodes,
'color': [colors[i % len(colors)] for i in range(len(all_nodes))]
},
'link': {
'source': df_agg[src].map(node_map).tolist(),
'target': df_agg[tgt].map(node_map).tolist(),
'value': df_agg[val].tolist(),
'color': f'{colors[0]}40'
}
}],
'layout': get_layout()
}
}
def create_candlestick_chart(df, rel, colors):
date = rel['date']
o, h, l, c = rel['open'], rel['high'], rel['low'], rel['close']
df_sorted = df.sort_values(date).tail(50) # Last 50 periods
return {
'chart_id': 'candlestick_financial',
'title': 'Financial Performance (OHLC)',
'type': 'candlestick',
'plotly_config': {
'data': [{
'type': 'candlestick',
'x': df_sorted[date].tolist(),
'open': df_sorted[o].tolist(),
'high': df_sorted[h].tolist(),
'low': df_sorted[l].tolist(),
'close': df_sorted[c].tolist(),
'increasing': {'line': {'color': '#10b981'}},
'decreasing': {'line': {'color': '#ef4444'}}
}],
'layout': {**get_layout(), 'xaxis': {'rangeslider': {'visible': False}}}
}
}
def create_bullet_chart(df, rel, colors):
col = rel['column']
target = rel['target']
current_val = df[col].iloc[-1] if len(df) > 0 else 0
target_val = df[target].iloc[-1] if len(df) > 0 else current_val * 1.1
return {
'chart_id': f'bullet_{col}',
'title': f'{col.title()} vs Target',
'type': 'indicator',
'plotly_config': {
'data': [{
'type': 'indicator',
'mode': 'number+gauge+delta',
'value': current_val,
'delta': {'reference': target_val},
'gauge': {
'shape': 'bullet',
'axis': {'range': [None, target_val * 1.2]},
'bar': {'color': colors[0]},
'steps': [
{'range': [0, target_val * 0.7], 'color': '#374151'},
{'range': [target_val * 0.7, target_val], 'color': '#4b5563'}
],
'threshold': {
'line': {'color': 'white', 'width': 2},
'thickness': 0.75,
'value': target_val
}
}
}],
'layout': {'height': 250, 'margin': {'t': 0, 'b': 0}, 'paper_bgcolor': 'rgba(0,0,0,0)', 'font': {'color': '#9ca3af'}}
}
}
def create_parallel_coords_chart(df, rel, colors):
cols = rel['columns'][:4] # Max 4 dimensions
cat = rel.get('category')
dimensions = []
for col in cols:
dimensions.append({
'range': [df[col].min(), df[col].max()],
'label': col.title(),
'values': df[col].tolist()
})
line_color = (df[cat].astype('category').cat.codes if cat else pd.Series([0]*len(df))).tolist() # Convert to list
return {
'chart_id': 'parallel_multivariate',
'title': 'Multivariate Analysis',
'type': 'parcoords',
'plotly_config': {
'data': [{
'type': 'parcoords',
'line': {
'color': line_color,
'colorscale': [[0, colors[0]], [1, colors[-1]]] if cat else None
},
'dimensions': dimensions
}],
'layout': get_layout()
}
}
def create_choropleth_chart(df, rel, colors):
loc_col = rel['location']
val_col = rel['value']
# Aggregate by location
df_agg = df.groupby(loc_col)[val_col].sum().reset_index()
# HEURISTIC: Check if we can actually map this
# If not a country column and values are not 2-char state codes, it's likely City/Region which Plotly Map can't handle well without Lat/Lon
# In that case, FALLBACK to a nice Bar Chart so user sees data!
sample_val = str(df_agg[loc_col].iloc[0]) if not df_agg.empty else ''
is_likely_state = len(sample_val) == 2 and sample_val.isalpha() and sample_val.isupper()
is_country = 'country' in loc_col.lower()
if not is_country and not is_likely_state:
# Fallback to Bar Chart
return {
'chart_id': f'bar_geo_{loc_col}',
'title': f'Geographic Distribution by {loc_col.title()} (Top 10)',
'type': 'bar',
'plotly_config': {
'data': [{
'type': 'bar',
'x': df_agg[val_col].nlargest(10).tolist(),
'y': df_agg.nlargest(10, val_col)[loc_col].tolist(),
'orientation': 'h',
'marker': {'color': colors[0]}
}],
'layout': {**get_layout(), 'yaxis': {'autorange': 'reversed'}} # Top on top
}
}
return {
'chart_id': f'map_{loc_col}',
'title': f'Geographic Distribution by {loc_col.title()}',
'type': 'choropleth',
'plotly_config': {
'data': [{
'type': 'choropleth',
'locations': df_agg[loc_col].tolist(),
'locationmode': 'country names' if is_country else 'USA-states',
'z': df_agg[val_col].tolist(),
'colorscale': [[0, '#f0f9ff'], [1, colors[0]]],
'autocolorscale': False,
'marker': {'line': {'color': 'rgb(255,255,255)', 'width': 0.5}},
'colorbar': {'title': val_col.title(), 'thickness': 10}
}],
'layout': {
**get_layout(),
'geo': {
'showframe': False,
'showcoastlines': True,
'projection': {'type': 'orthographic'},
'bgcolor': 'rgba(0,0,0,0)',
}
}
}
}
def create_scatter3d_chart(df, rel, colors):
x, y, z = rel['x'], rel['y'], rel['z']
c_col = rel['color']
# Sample if too large
df_sample = df.head(500)
return {
'chart_id': f'3d_{x}_{y}_{z}',
'title': f'3D Correlation Analysis',
'type': 'scatter3d',
'plotly_config': {
'data': [{
'type': 'scatter3d',
'mode': 'markers',
'x': df_sample[x].tolist(),
'y': df_sample[y].tolist(),
'z': df_sample[z].tolist(),
'marker': {
'size': 4,
'color': (df_sample[c_col].astype('category').cat.codes if df[c_col].dtype == 'object' else df_sample[c_col]).tolist(),
'colorscale': 'Viridis',
'opacity': 0.8
}
}],
'layout': {
'paper_bgcolor': 'rgba(0,0,0,0)',
'scene': {
'xaxis': {'title': x, 'backgroundcolor': 'rgba(0,0,0,0)', 'gridcolor': 'rgba(255,255,255,0.1)'},
'yaxis': {'title': y, 'backgroundcolor': 'rgba(0,0,0,0)', 'gridcolor': 'rgba(255,255,255,0.1)'},
'zaxis': {'title': z, 'backgroundcolor': 'rgba(0,0,0,0)', 'gridcolor': 'rgba(255,255,255,0.1)'},
},
'margin': {'l': 0, 'r': 0, 'b': 0, 't': 0},
'height': 400
}
}
}
def create_calendar_heatmap(df, rel, colors):
date_col = rel['date']
val_col = rel['value']
try:
# Convert to datetime safely
df[date_col] = pd.to_datetime(df[date_col], errors='coerce')
df_clean = df.dropna(subset=[date_col])
if df_clean.empty:
return None # Skip if no valid dates
# Group by date
if val_col:
daily = df_clean.groupby(df_clean[date_col].dt.date)[val_col].sum()
else:
daily = df_clean.groupby(df_clean[date_col].dt.date).size()
return {
'chart_id': 'calendar_heatmap',
'title': 'Daily Activity Heatmap',
'type': 'heatmap',
'plotly_config': {
'data': [{
'type': 'heatmap',
'x': [d.strftime('%Y-%m-%d') for d in daily.index],
'y': ['Activity'], # Single row label for 2D array with 1 row
'z': [daily.values.tolist()], # Heatmap expects 2D array
'colorscale': [[0, '#f3f4f6'], [1, colors[0]]],
'showscale': False
}],
'layout': {
**get_layout(),
'height': 150,
'margin': {'l': 20, 'r': 20, 't': 30, 'b': 20}
}
}
}
except:
return None
def get_layout():
return {
'paper_bgcolor': 'rgba(0,0,0,0)',
'plot_bgcolor': 'rgba(0,0,0,0)',
'font': {'color': '#9ca3af', 'size': 10},
'margin': {'l': 45, 'r': 15, 't': 25, 'b': 50},
'xaxis': {'gridcolor': 'rgba(255,255,255,0.05)', 'tickangle': -30},
'yaxis': {'gridcolor': 'rgba(255,255,255,0.05)'}
}
# ================= NEW PREMIUM CHART CREATORS =================
def create_combo_bar_line_chart(df, rel, colors):
"""
POWER BI STYLE COMBO CHART - Bar + Line with Dual Y-Axis
Perfect for showing volume vs rate, sales vs margin, etc.
"""
try:
cat_col = rel['category']
bar_col = rel['bar_metric']
line_col = rel['line_metric']
grouped = df.groupby(cat_col)[[bar_col, line_col]].sum().head(10)
labels = [str(l)[:15] for l in grouped.index]
return {
'chart_id': f'combo_{bar_col}_{line_col}',
'title': f'{bar_col.replace("_", " ").title()} vs {line_col.replace("_", " ").title()}',
'type': 'combo_bar_line',
'analysis': 'dual_axis_comparison',
'plotly_config': {
'data': [
# Bar chart on primary y-axis
{
'type': 'bar',
'name': bar_col.replace('_', ' ').title(),
'x': labels,
'y': grouped[bar_col].tolist(),
'marker': {'color': colors[0], 'opacity': 0.8},
'yaxis': 'y'
},
# Line chart on secondary y-axis
{
'type': 'scatter',
'mode': 'lines+markers',
'name': line_col.replace('_', ' ').title(),
'x': labels,
'y': grouped[line_col].tolist(),
'line': {'color': colors[1], 'width': 3},
'marker': {'size': 8, 'color': colors[1]},
'yaxis': 'y2'
}
],
'layout': {
**get_layout(),
'yaxis': {'title': bar_col.replace('_', ' ').title(), 'side': 'left'},
'yaxis2': {
'title': line_col.replace('_', ' ').title(),
'overlaying': 'y',
'side': 'right',
'showgrid': False
},
'legend': {'orientation': 'h', 'y': -0.15}
}
}
}
except Exception as e:
logger.warning(f"Combo chart error: {e}")
return None
def create_lollipop_chart(df, rel, colors):
"""
LOLLIPOP CHART - Elegant alternative to bar chart for rankings
Shows stem + circle for each data point
"""
try:
cat_col = rel['category']
val_col = rel['value']
grouped = df.groupby(cat_col)[val_col].sum().sort_values(ascending=True).tail(12)
labels = [str(l)[:20] for l in grouped.index]
values = grouped.values.tolist()
# Create stems (lines from 0 to value)
stems = []
for i, (label, val) in enumerate(zip(labels, values)):
stems.append({
'type': 'scatter',
'mode': 'lines',
'x': [0, val],
'y': [label, label],
'line': {'color': colors[i % len(colors)], 'width': 2},
'showlegend': False,
'hoverinfo': 'skip'
})
# Create dots
dots = {
'type': 'scatter',
'mode': 'markers',
'x': values,
'y': labels,
'marker': {
'size': 12,
'color': colors[:len(values)],
'line': {'width': 2, 'color': 'white'}
},
'name': val_col.replace('_', ' ').title(),
'text': [f'{v:,.0f}' for v in values],
'textposition': 'middle right',
'hovertemplate': '%{y}: %{x:,.0f}<extra></extra>'
}
return {
'chart_id': f'lollipop_{cat_col}',
'title': f'{val_col.replace("_", " ").title()} Ranking',
'type': 'lollipop',
'analysis': 'ranking_visualization',
'plotly_config': {
'data': stems + [dots],
'layout': {
**get_layout(),
'yaxis': {'automargin': True, 'gridwidth': 0},
'xaxis': {'zeroline': True, 'zerolinecolor': 'rgba(255,255,255,0.2)'}
}
}
}
except Exception as e:
logger.warning(f"Lollipop chart error: {e}")
return None
def create_diverging_bar_chart(df, rel, colors):
"""
DIVERGING BAR CHART - Shows positive/negative from center
Perfect for profit/loss, growth/decline, sentiment analysis
"""
try:
cat_col = rel.get('category')
val_col = rel['value']
if cat_col and cat_col in df.columns:
grouped = df.groupby(cat_col)[val_col].sum().head(12)
labels = [str(l)[:18] for l in grouped.index]
values = grouped.values.tolist()
else:
# Use index-based if no category
values = df[val_col].head(15).tolist()
labels = [f'Item {i+1}' for i in range(len(values))]
# Center around mean for diverging effect
mean_val = np.mean(values)
diverging_values = [v - mean_val for v in values]
# Color by positive/negative
bar_colors = [colors[0] if v >= 0 else '#ef4444' for v in diverging_values]
return {
'chart_id': f'diverging_{val_col}',
'title': f'{val_col.replace("_", " ").title()} Variance from Average',
'type': 'diverging_bar',
'analysis': 'variance_from_mean',
'plotly_config': {
'data': [{
'type': 'bar',
'orientation': 'h',
'y': labels,
'x': diverging_values,
'marker': {'color': bar_colors},
'text': [f'{v:+,.0f}' for v in diverging_values],
'textposition': 'outside',
'hovertemplate': '%{y}: %{x:+,.0f} vs avg<extra></extra>'
}],
'layout': {
**get_layout(),
'xaxis': {
'zeroline': True,
'zerolinecolor': 'rgba(255,255,255,0.5)',
'zerolinewidth': 2,
'title': f'Difference from Mean ({mean_val:,.0f})'
},
'yaxis': {'automargin': True}
}
}
}
except Exception as e:
logger.warning(f"Diverging bar error: {e}")
return None
def create_slope_chart(df, rel, colors):
"""
SLOPE CHART - Period over period comparison
Shows change between two time periods for multiple categories
"""
try:
start_col = rel['start_value']
end_col = rel['end_value']
cat_col = rel.get('category')
if cat_col and cat_col in df.columns:
grouped = df.groupby(cat_col)[[start_col, end_col]].sum().head(8)
labels = [str(l)[:15] for l in grouped.index]
start_vals = grouped[start_col].tolist()
end_vals = grouped[end_col].tolist()
else:
# Use aggregated totals
n_segments = min(8, len(df) // 2)
segment_size = len(df) // n_segments
start_vals = [df[start_col].iloc[i*segment_size:(i+1)*segment_size//2].sum() for i in range(n_segments)]
end_vals = [df[end_col].iloc[i*segment_size:(i+1)*segment_size//2].sum() for i in range(n_segments)]
labels = [f'Segment {i+1}' for i in range(n_segments)]
data = []
for i, (label, s, e) in enumerate(zip(labels, start_vals, end_vals)):
# Determine color by growth
line_color = colors[0] if e >= s else '#ef4444'
data.append({
'type': 'scatter',
'mode': 'lines+markers+text',
'x': [start_col.replace('_', ' ').title(), end_col.replace('_', ' ').title()],
'y': [s, e],
'name': label,
'line': {'color': line_color, 'width': 2},
'marker': {'size': 10, 'color': line_color},
'text': [f'{s:,.0f}', f'{e:,.0f}'],
'textposition': ['middle left', 'middle right']
})
return {
'chart_id': f'slope_{start_col}_{end_col}',
'title': f'{start_col.replace("_", " ").title()} β†’ {end_col.replace("_", " ").title()}',
'type': 'slope',
'analysis': 'period_comparison',
'plotly_config': {
'data': data,
'layout': {
**get_layout(),
'showlegend': True,
'legend': {'orientation': 'v', 'x': 1.02}
}
}
}
except Exception as e:
logger.warning(f"Slope chart error: {e}")
return None
def create_dumbbell_chart(df, rel, colors):
"""
DUMBBELL CHART - Before/After or Two-metric comparison
Shows range between two values for each category
"""
try:
cat_col = rel['category']
val1_col = rel['value1']
val2_col = rel['value2']
grouped = df.groupby(cat_col)[[val1_col, val2_col]].sum().head(10)
labels = [str(l)[:18] for l in grouped.index]
val1 = grouped[val1_col].tolist()
val2 = grouped[val2_col].tolist()
data = []
# Draw connecting lines first
for i, (label, v1, v2) in enumerate(zip(labels, val1, val2)):
data.append({
'type': 'scatter',
'mode': 'lines',
'x': [v1, v2],
'y': [label, label],
'line': {'color': 'rgba(150,150,150,0.5)', 'width': 4},
'showlegend': False,
'hoverinfo': 'skip'
})
# Add dots for value 1
data.append({
'type': 'scatter',
'mode': 'markers',
'x': val1,
'y': labels,
'marker': {'size': 14, 'color': colors[0], 'line': {'width': 2, 'color': 'white'}},
'name': val1_col.replace('_', ' ').title(),
'hovertemplate': '%{y}: %{x:,.0f}<extra>' + val1_col + '</extra>'
})
# Add dots for value 2
data.append({
'type': 'scatter',
'mode': 'markers',
'x': val2,
'y': labels,
'marker': {'size': 14, 'color': colors[1], 'line': {'width': 2, 'color': 'white'}},
'name': val2_col.replace('_', ' ').title(),
'hovertemplate': '%{y}: %{x:,.0f}<extra>' + val2_col + '</extra>'
})
return {
'chart_id': f'dumbbell_{val1_col}_{val2_col}',
'title': f'{val1_col.replace("_", " ").title()} vs {val2_col.replace("_", " ").title()}',
'type': 'dumbbell',
'analysis': 'comparison_visualization',
'plotly_config': {
'data': data,
'layout': {
**get_layout(),
'yaxis': {'automargin': True},
'legend': {'orientation': 'h', 'y': -0.15}
}
}
}
except Exception as e:
logger.warning(f"Dumbbell chart error: {e}")
return None
def create_range_plot(df, rel, colors):
"""
RANGE PLOT - Min-Max visualization with markers
Shows the spread of values for each category
"""
try:
cat_col = rel['category']
val_col = rel['value']
stats = df.groupby(cat_col)[val_col].agg(['min', 'max', 'mean']).head(10)
labels = [str(l)[:18] for l in stats.index]
data = []
# Draw range lines
for i, (label, row) in enumerate(zip(labels, stats.itertuples())):
data.append({
'type': 'scatter',
'mode': 'lines',
'x': [row.min, row.max],
'y': [label, label],
'line': {'color': colors[i % len(colors)], 'width': 6},
'showlegend': False,
'hoverinfo': 'skip'
})
# Min markers
data.append({
'type': 'scatter',
'mode': 'markers',
'x': stats['min'].tolist(),
'y': labels,
'marker': {'size': 10, 'color': '#ef4444', 'symbol': 'triangle-left'},
'name': 'Min',
'hovertemplate': '%{y}: Min = %{x:,.0f}<extra></extra>'
})
# Mean markers
data.append({
'type': 'scatter',
'mode': 'markers',
'x': stats['mean'].tolist(),
'y': labels,
'marker': {'size': 12, 'color': colors[0], 'symbol': 'diamond'},
'name': 'Mean',
'hovertemplate': '%{y}: Mean = %{x:,.0f}<extra></extra>'
})
# Max markers
data.append({
'type': 'scatter',
'mode': 'markers',
'x': stats['max'].tolist(),
'y': labels,
'marker': {'size': 10, 'color': '#22c55e', 'symbol': 'triangle-right'},
'name': 'Max',
'hovertemplate': '%{y}: Max = %{x:,.0f}<extra></extra>'
})
return {
'chart_id': f'range_{cat_col}_{val_col}',
'title': f'{val_col.replace("_", " ").title()} Range by {cat_col.replace("_", " ").title()}',
'type': 'range_plot',
'analysis': 'min_max_spread',
'plotly_config': {
'data': data,
'layout': {
**get_layout(),
'yaxis': {'automargin': True},
'legend': {'orientation': 'h', 'y': -0.15}
}
}
}
except Exception as e:
logger.warning(f"Range plot error: {e}")
return None
def create_band_chart(df, rel, colors):
"""
BAND CHART - Line with confidence interval bands
Shows trend with uncertainty/variability range
"""
try:
val_col = rel['value']
# Create rolling statistics
values = df[val_col].fillna(0).values
n_points = min(30, len(values))
segment_size = max(1, len(values) // n_points)
means = []
stds = []
for i in range(n_points):
segment = values[i*segment_size:(i+1)*segment_size]
means.append(np.mean(segment))
stds.append(np.std(segment) if len(segment) > 1 else 0)
x_vals = list(range(1, n_points + 1))
upper = [m + s for m, s in zip(means, stds)]
lower = [m - s for m, s in zip(means, stds)]
return {
'chart_id': f'band_{val_col}',
'title': f'{val_col.replace("_", " ").title()} Trend with Confidence Band',
'type': 'band_chart',
'analysis': 'confidence_interval',
'plotly_config': {
'data': [
# Upper bound (invisible, for fill)
{
'type': 'scatter',
'mode': 'lines',
'x': x_vals,
'y': upper,
'line': {'color': 'transparent'},
'showlegend': False,
'name': 'Upper'
},
# Lower bound with fill to upper
{
'type': 'scatter',
'mode': 'lines',
'x': x_vals,
'y': lower,
'fill': 'tonexty',
'fillcolor': f'{colors[0]}30',
'line': {'color': 'transparent'},
'showlegend': False,
'name': 'Β±1 Std Dev'
},
# Mean line
{
'type': 'scatter',
'mode': 'lines+markers',
'x': x_vals,
'y': means,
'line': {'color': colors[0], 'width': 3},
'marker': {'size': 6, 'color': colors[0]},
'name': 'Mean Trend'
}
],
'layout': {
**get_layout(),
'legend': {'orientation': 'h', 'y': -0.15}
}
}
}
except Exception as e:
logger.warning(f"Band chart error: {e}")
return None
def create_step_line_chart(df, rel, colors):
"""
STEP LINE CHART - Step function visualization
Perfect for pricing tiers, discrete changes, thresholds
"""
try:
val_col = rel['value']
n_points = min(25, len(df))
segment_size = max(1, len(df) // n_points)
values = [float(df[val_col].iloc[i*segment_size:(i+1)*segment_size].mean()) for i in range(n_points)]
x_vals = list(range(1, n_points + 1))
return {
'chart_id': f'step_{val_col}',
'title': f'{val_col.replace("_", " ").title()} Step Progression',
'type': 'step_line',
'analysis': 'discrete_changes',
'plotly_config': {
'data': [{
'type': 'scatter',
'mode': 'lines+markers',
'x': x_vals,
'y': values,
'line': {'shape': 'hv', 'color': colors[0], 'width': 2},
'marker': {'size': 8, 'color': colors[0]},
'fill': 'tozeroy',
'fillcolor': f'{colors[0]}20'
}],
'layout': get_layout()
}
}
except Exception as e:
logger.warning(f"Step line error: {e}")
return None
# ================= MAIN DASHBOARD GENERATOR =================
def generate_real_dashboard(df: pd.DataFrame, user_id: str, refresh: bool = False) -> Dict:
"""
Generate a TRULY AUTONOMOUS dashboard using Multi-Agent Architecture.
Agents (all run silently in the background):
1. Data Profiling Agent - scans columns, types, stats
2. Domain & Categorization Agent (LLM) - decides domain, title, sections
3. Visualization Agent - maps data relationships to chart types
ALL calculations are REAL - done with pandas!
"""
global _dashboard_cache
if df is None or df.empty:
return {"error": "No data available. Please upload files first."}
try:
# πŸš€ CACHE CHECK - Return cached dashboard if still fresh (unless refreshing)
cache_key = f"{user_id}_{len(df)}_{len(df.columns)}_{list(df.columns)[:5]}"
cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
current_time = time_module.time()
if not refresh and cache_hash in _dashboard_cache:
cached_data, cached_time = _dashboard_cache[cache_hash]
if current_time - cached_time < _CACHE_TTL:
logger.info(f"⚑ CACHE HIT - Returning cached dashboard (age: {current_time - cached_time:.1f}s)")
return cached_data
logger.info(f"πŸ€– AUTONOMOUS Dashboard generation for {user_id}")
logger.info(f"πŸ“Š Data: {len(df)} rows, {len(df.columns)} cols")
# ============================================================
# AGENT 1: Data Profiling Agent (pandas-powered)
# ============================================================
profile = dashboard_column_profile(df)
numeric_cols = profile['metrics']
categorical_cols = profile['dimensions']
datetime_cols = profile['dates']
logger.info(f"πŸ“‹ Profile: {len(numeric_cols)} numeric, {len(categorical_cols)} categorical, {len(datetime_cols)} datetime")
# ============================================================
# AGENT 2: Domain & Categorization Agent (LLM-powered)
# ============================================================
domain_info = analyze_domain_and_sections(df)
domain = domain_info.get("domain", "general")
if refresh:
# Inject randomness into domain/theme when refreshing to give users variety
available_domains = list(COLOR_PALETTES.keys())
if domain in available_domains and len(available_domains) > 1:
available_domains.remove(domain)
domain = random.choice(available_domains)
ai_title = domain_info.get("dashboard_title", f"{domain.title()} Analytics Dashboard")
ai_sections = domain_info.get("sections", ["Overview", "Distribution Analysis", "Correlation Studio", "Performance Metrics"])
palette = COLOR_PALETTES.get(domain, COLOR_PALETTES['general'])
logger.info(f"🎨 Agent detected domain: {domain}, sections: {ai_sections}")
# ============================================================
# AGENT 3: AI Schema Agent & Premium Builder (LLM-Driven)
# ============================================================
try:
from core.ai_dashboard_agent import build_dashboard_schema_agent
from core.universal_builder import execute_llm_chart_spec
logger.info("πŸ€– Asking LLM to architect dashboard schema and theme...")
schema_output = build_dashboard_schema_agent(df, domain, chat)
ai_theme = schema_output.get("theme", {
"background_color": "#0b1120",
"card_background": "#151e32",
"border_color": "#2a3441",
"text_color": "#f8fafc",
"text_secondary": "#e2e8f0",
"chart_palette": palette['chart']
})
chart_specs = schema_output.get("charts", [])
logger.info(f"πŸ“‹ LLM Architect generated {len(chart_specs)} chart specifications with custom theme")
charts = []
for spec in chart_specs:
# Use AI generated palette if available
chart_palette = ai_theme.get("chart_palette", palette['chart'])
chart = execute_llm_chart_spec(df, spec, chart_palette)
if chart:
charts.append(chart)
# Supplement with heuristics if LLM misses the target density
if len(charts) < 12:
logger.info(f"Supplementing {len(charts)} AI charts with business-aware heuristics.")
relationships = analyze_column_relationships(df)
heuristic_charts = autonomous_chart_selection(df, domain, relationships)
existing_titles = {c.get('title') for c in charts}
for hc in heuristic_charts:
if hc.get('title') not in existing_titles and len(charts) < 14:
charts.append(hc)
existing_titles.add(hc.get('title'))
except Exception as e:
logger.error(f"Error in LLM Agent Builder: {e}. Falling back to pure heuristics.")
relationships = analyze_column_relationships(df)
charts = autonomous_chart_selection(df, domain, relationships)
ai_theme = {
"background_color": "#0b1120",
"card_background": "#151e32",
"border_color": "#2a3441",
"text_color": "#f8fafc",
"text_secondary": "#e2e8f0",
"chart_palette": palette['chart']
}
logger.info(f"πŸ“Š Finalized {len(charts)} premium charts")
# ============================================================
# AGENT 4: Section Assignment Agent
# Distributes charts into the AI-generated sections
# ============================================================
sections = assign_charts_to_sections(charts, ai_sections, numeric_cols, categorical_cols)
logger.info(f"πŸ“‚ Assigned charts to {len(sections)} sections")
# Calculate POWER BI-STYLE ADVANCED KPIs
kpis = calculate_advanced_kpis(df, palette)
# Generate insights from real data
insights = generate_real_insights(df)
# Generate recommendations
recommendations = generate_recommendations(df)
# Generate Interactive Slicers / Filters
active_slicers = []
for col in categorical_cols:
if df[col].nunique() < 25 and df[col].nunique() > 1: # Slicers with 2 to 24 options
active_slicers.append({
"column": col,
"options": sorted([str(x) for x in df[col].dropna().unique().tolist()])
})
if len(active_slicers) >= 4: # Limit to top 4 categorical columns
break
# Generate Executive Summary Ticker
exec_summary = f"Live Data Intelligence: {len(df):,} {domain.title()} records analyzed."
if kpis and len(kpis) > 0:
exec_summary += f" Top Driver: {kpis[0].get('title', 'Metric')} stands at {kpis[0].get('value', 0)}."
if insights and len(insights) > 0:
exec_summary += f" AI Observation: {insights[0]}"
result = sanitize_for_json({
"dashboard_title": ai_title,
"domain": domain,
"theme": ai_theme,
"kpis": kpis,
"sections": sections,
"charts": charts, # Keep flat list for backward compatibility
"insights": insights,
"recommendations": recommendations,
"filters": active_slicers,
"executive_summary": exec_summary,
"data_grid": {
"columns": df.columns.tolist(),
"rows": df.head(100).fillna("").to_dict(orient="records")
},
"generated_at": datetime.now().isoformat(),
"data_source": f"{len(df):,} rows Γ— {len(df.columns)} columns"
})
# πŸš€ CACHE the result
_dashboard_cache[cache_hash] = (result, current_time)
# Clean old cache entries (keep only last 10)
if len(_dashboard_cache) > 10:
oldest_keys = sorted(_dashboard_cache.keys(), key=lambda k: _dashboard_cache[k][1])[:5]
for k in oldest_keys:
del _dashboard_cache[k]
return result
except Exception as e:
logger.error(f"Dashboard error: {e}")
import traceback
traceback.print_exc()
return {"error": str(e)}
def assign_charts_to_sections(charts: List[Dict], section_names: List[str],
numeric_cols: List[str], categorical_cols: List[str]) -> List[Dict]:
"""
SECTION ASSIGNMENT AGENT: Distributes charts evenly across AI-generated sections.
Uses chart type and analysis metadata to intelligently group them.
"""
if not charts or not section_names:
return [{"name": "Overview", "charts": charts}]
# Define chart-to-section affinity rules
section_affinity = {
0: ['bar', 'horizontal_bar', 'pie', 'donut', 'gauge', 'funnel', 'treemap', 'combo_bar_line'], # Overview
1: ['histogram', 'box', 'violin', 'area', 'band_chart', 'step_line'], # Distribution
2: ['scatter', 'bubble', 'heatmap', 'parcoords', 'scatter_3d', 'density_contour'], # Correlation
3: ['line', 'stacked_bar', 'grouped_bar', 'waterfall', 'pareto', 'lollipop', 'slope'], # Performance
4: ['radar', 'sunburst', 'sankey', 'candlestick', 'diverging_bar', 'dumbbell', 'range_plot', 'choropleth', 'calendar_heatmap'], # Advanced
}
# Create section buckets
num_sections = len(section_names)
section_buckets = [[] for _ in range(num_sections)]
assigned = set()
# Phase 1: Assign by affinity
for chart in charts:
chart_type = chart.get('type', 'bar')
chart_id = chart.get('chart_id', '')
for section_idx, affinity_types in section_affinity.items():
if section_idx >= num_sections:
break
if chart_type in affinity_types and chart_id not in assigned:
section_buckets[section_idx].append(chart)
assigned.add(chart_id)
break
# Phase 2: Distribute unassigned charts round-robin
unassigned = [c for c in charts if c.get('chart_id', '') not in assigned]
for i, chart in enumerate(unassigned):
section_buckets[i % num_sections].append(chart)
# Build final sections array
sections = []
for i, name in enumerate(section_names):
if i < len(section_buckets) and section_buckets[i]:
sections.append({
"name": name,
"charts": section_buckets[i]
})
# If any sections are empty, redistribute
non_empty = [s for s in sections if s["charts"]]
if not non_empty:
return [{"name": section_names[0], "charts": charts}]
return non_empty
# ================= POWER BI ADVANCED CALCULATIONS =================
def calculate_cagr(df: pd.DataFrame, value_col: str, periods: int = None) -> Dict:
"""
Calculate Compound Annual Growth Rate (CAGR)
CAGR = (End Value / Start Value)^(1/n) - 1
"""
try:
values = df[value_col].dropna()
if len(values) < 2:
return {'cagr': 0, 'has_data': False}
start_val = values.iloc[0]
end_val = values.iloc[-1]
n = periods if periods else len(values) - 1
if start_val <= 0 or n <= 0:
return {'cagr': 0, 'has_data': False}
cagr = (pow(end_val / start_val, 1/n) - 1) * 100
return {
'cagr': round(cagr, 2),
'start_value': float(start_val),
'end_value': float(end_val),
'periods': n,
'has_data': True
}
except:
return {'cagr': 0, 'has_data': False}
def calculate_z_scores(df: pd.DataFrame, value_col: str) -> Dict:
"""
Calculate Z-scores for anomaly detection
Z = (x - mean) / std
"""
try:
values = df[value_col].dropna()
if len(values) < 3:
return {'anomalies': [], 'max_z': 0, 'anomaly_count': 0}
mean = values.mean()
std = values.std()
if std == 0:
return {'anomalies': [], 'max_z': 0, 'anomaly_count': 0}
z_scores = ((values - mean) / std).abs()
# Find anomalies (|z| > 2.5)
anomaly_mask = z_scores > 2.5
anomaly_indices = values[anomaly_mask].index.tolist()
return {
'max_z': round(float(z_scores.max()), 2),
'anomaly_count': int(anomaly_mask.sum()),
'anomaly_pct': round(anomaly_mask.sum() / len(values) * 100, 1),
'anomaly_indices': anomaly_indices[:10], # First 10
'mean': round(float(mean), 2),
'std': round(float(std), 2)
}
except:
return {'anomalies': [], 'max_z': 0, 'anomaly_count': 0}
def calculate_percentile_rank(df: pd.DataFrame, value_col: str) -> Dict:
"""
Calculate percentile ranks and distribution metrics
"""
try:
values = df[value_col].dropna()
if len(values) < 2:
return {'percentiles': {}, 'has_data': False}
percentiles = {
'p10': round(float(values.quantile(0.10)), 2),
'p25': round(float(values.quantile(0.25)), 2),
'p50': round(float(values.quantile(0.50)), 2),
'p75': round(float(values.quantile(0.75)), 2),
'p90': round(float(values.quantile(0.90)), 2),
'p95': round(float(values.quantile(0.95)), 2),
'p99': round(float(values.quantile(0.99)), 2)
}
# Skewness and Kurtosis
skewness = values.skew()
kurtosis = values.kurtosis()
return {
'percentiles': percentiles,
'skewness': round(float(skewness), 2) if not pd.isna(skewness) else 0,
'kurtosis': round(float(kurtosis), 2) if not pd.isna(kurtosis) else 0,
'distribution': 'normal' if abs(skewness) < 0.5 else ('right_skewed' if skewness > 0 else 'left_skewed'),
'has_data': True
}
except:
return {'percentiles': {}, 'has_data': False}
def calculate_concentration_ratio(df: pd.DataFrame, cat_col: str, value_col: str) -> Dict:
"""
Calculate market concentration ratios (CR4, CR8, HHI)
Used for competitive analysis
"""
try:
grouped = df.groupby(cat_col)[value_col].sum().sort_values(ascending=False)
total = grouped.sum()
if total == 0:
return {'cr4': 0, 'cr8': 0, 'hhi': 0, 'has_data': False}
shares = (grouped / total * 100).values
# Concentration Ratios
cr4 = sum(shares[:4]) if len(shares) >= 4 else sum(shares)
cr8 = sum(shares[:8]) if len(shares) >= 8 else sum(shares)
# Herfindahl-Hirschman Index (HHI)
hhi = sum(s**2 for s in shares[:50]) # Limit to top 50
# Market structure classification
if hhi < 1500:
structure = 'competitive'
elif hhi < 2500:
structure = 'moderately_concentrated'
else:
structure = 'highly_concentrated'
return {
'cr4': round(cr4, 1),
'cr8': round(cr8, 1),
'hhi': round(hhi, 0),
'market_structure': structure,
'leader_share': round(shares[0], 1) if len(shares) > 0 else 0,
'leader_name': str(grouped.index[0]) if len(grouped) > 0 else 'N/A',
'has_data': True
}
except:
return {'cr4': 0, 'cr8': 0, 'hhi': 0, 'has_data': False}
def calculate_seasonality(df: pd.DataFrame, value_col: str, date_col: str = None) -> Dict:
"""
Detect seasonality patterns in data
"""
try:
values = df[value_col].fillna(0).values
n = len(values)
if n < 12:
return {'seasonal': False, 'pattern': None, 'has_data': False}
# Simple autocorrelation for seasonality detection
lags = [7, 12, 30, 52] # Weekly, Monthly, 30-day, Yearly
correlations = {}
for lag in lags:
if n > lag * 2:
v1 = values[:-lag]
v2 = values[lag:]
if np.std(v1) > 0 and np.std(v2) > 0:
corr = np.corrcoef(v1, v2)[0, 1]
if not np.isnan(corr):
correlations[lag] = round(corr, 3)
# Find strongest seasonality
if correlations:
best_lag = max(correlations, key=correlations.get)
best_corr = correlations[best_lag]
lag_names = {7: 'weekly', 12: 'monthly', 30: '30-day', 52: 'yearly'}
return {
'seasonal': best_corr > 0.3,
'pattern': lag_names.get(best_lag, f'{best_lag}-period'),
'correlation': best_corr,
'all_correlations': correlations,
'has_data': True
}
return {'seasonal': False, 'pattern': None, 'has_data': False}
except:
return {'seasonal': False, 'pattern': None, 'has_data': False}
def calculate_momentum(df: pd.DataFrame, value_col: str, window: int = 5) -> Dict:
"""
Calculate momentum indicators (Rate of Change)
ROC = (Current - N periods ago) / N periods ago * 100
"""
try:
values = df[value_col].fillna(0)
n = len(values)
if n < window + 1:
return {'momentum': 0, 'roc': 0, 'has_data': False}
current = values.iloc[-1]
past = values.iloc[-window-1]
if past == 0:
return {'momentum': 0, 'roc': 0, 'has_data': False}
roc = (current - past) / abs(past) * 100
# Momentum direction
if roc > 5:
momentum = 'strong_up'
elif roc > 0:
momentum = 'up'
elif roc > -5:
momentum = 'down'
else:
momentum = 'strong_down'
return {
'momentum': momentum,
'roc': round(roc, 1),
'current': float(current),
'past': float(past),
'window': window,
'has_data': True
}
except:
return {'momentum': 0, 'roc': 0, 'has_data': False}
def calculate_yoy_growth(df: pd.DataFrame, date_col: str, value_col: str) -> Dict:
"""
Calculate Year-over-Year growth - Power BI style
Returns growth %, previous period value, current period value
"""
try:
df_copy = df.copy()
df_copy[date_col] = pd.to_datetime(df_copy[date_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_col])
if len(df_copy) < 2:
return {'growth': 0, 'current': 0, 'previous': 0, 'has_data': False}
# Group by year
df_copy['_year'] = df_copy[date_col].dt.year
yearly = df_copy.groupby('_year')[value_col].sum()
if len(yearly) >= 2:
current_year = yearly.iloc[-1]
previous_year = yearly.iloc[-2]
growth = ((current_year - previous_year) / previous_year * 100) if previous_year != 0 else 0
return {
'growth': round(growth, 1),
'current': float(current_year),
'previous': float(previous_year),
'has_data': True
}
return {'growth': 0, 'current': 0, 'previous': 0, 'has_data': False}
except:
return {'growth': 0, 'current': 0, 'previous': 0, 'has_data': False}
def calculate_moving_average(df: pd.DataFrame, value_col: str, window: int = 7) -> Dict:
"""
Calculate rolling moving average - Power BI style
"""
try:
values = df[value_col].fillna(0)
if len(values) < window:
return {'ma': float(values.mean()), 'current': float(values.iloc[-1]) if len(values) > 0 else 0}
ma = values.rolling(window=window).mean()
current_ma = float(ma.iloc[-1]) if not pd.isna(ma.iloc[-1]) else float(values.mean())
current_val = float(values.iloc[-1])
return {
'ma': round(current_ma, 2),
'current': round(current_val, 2),
'above_ma': current_val > current_ma
}
except:
return {'ma': 0, 'current': 0, 'above_ma': False}
def calculate_pareto_analysis(df: pd.DataFrame, cat_col: str, value_col: str) -> Dict:
"""
Pareto (80/20) analysis - Power BI style
Identifies which categories contribute to 80% of value
"""
try:
grouped = df.groupby(cat_col)[value_col].sum().sort_values(ascending=False)
total = grouped.sum()
if total == 0:
return {'top_contributors': [], 'pareto_count': 0, 'pareto_pct': 0}
cumsum = grouped.cumsum()
cumsum_pct = cumsum / total * 100
# Find categories that make up 80%
pareto_mask = cumsum_pct <= 80
pareto_items = grouped[pareto_mask].index.tolist()
# If no items under 80%, take top item
if not pareto_items:
pareto_items = [grouped.index[0]]
return {
'top_contributors': pareto_items[:5], # Top 5 contributors to 80%
'pareto_count': len(pareto_items),
'total_categories': len(grouped),
'pareto_pct': round(len(pareto_items) / len(grouped) * 100, 1),
'top_value': float(grouped.iloc[0]),
'top_name': str(grouped.index[0])
}
except:
return {'top_contributors': [], 'pareto_count': 0, 'pareto_pct': 0}
def calculate_variance_analysis(df: pd.DataFrame, value_col: str) -> Dict:
"""
Variance and statistical analysis - Power BI style
"""
try:
values = df[value_col].dropna()
if len(values) == 0:
return {'variance': 0, 'std': 0, 'cv': 0, 'range': 0}
mean = values.mean()
std = values.std()
variance = values.var()
cv = (std / mean * 100) if mean != 0 else 0 # Coefficient of variation
q1 = values.quantile(0.25)
q3 = values.quantile(0.75)
iqr = q3 - q1
# Outlier detection
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
outliers = values[(values < lower_bound) | (values > upper_bound)]
return {
'mean': round(float(mean), 2),
'std': round(float(std), 2),
'variance': round(float(variance), 2),
'cv': round(cv, 1), # Coefficient of variation %
'q1': round(float(q1), 2),
'median': round(float(values.median()), 2),
'q3': round(float(q3), 2),
'iqr': round(float(iqr), 2),
'min': round(float(values.min()), 2),
'max': round(float(values.max()), 2),
'range': round(float(values.max() - values.min()), 2),
'outlier_count': len(outliers),
'outlier_pct': round(len(outliers) / len(values) * 100, 1)
}
except:
return {'variance': 0, 'std': 0, 'cv': 0, 'range': 0}
def calculate_contribution_percentage(df: pd.DataFrame, cat_col: str, value_col: str) -> List[Dict]:
"""
Calculate each category's contribution to total - Power BI style
"""
try:
grouped = df.groupby(cat_col)[value_col].sum().sort_values(ascending=False)
total = grouped.sum()
if total == 0:
return []
result = []
cumulative = 0
for name, val in grouped.head(10).items():
pct = val / total * 100
cumulative += pct
result.append({
'name': str(name),
'value': float(val),
'percentage': round(pct, 1),
'cumulative_pct': round(cumulative, 1)
})
return result
except:
return []
def calculate_trend_forecast(df: pd.DataFrame, value_col: str, periods: int = 3) -> Dict:
"""
Linear regression forecast - Power BI style trend analysis
"""
try:
values = df[value_col].fillna(0).values
if len(values) < 5:
return {'forecast': [], 'trend': 'neutral', 'slope': 0}
# Simple linear regression
x = np.arange(len(values))
slope, intercept = np.polyfit(x, values, 1)
# Forecast next periods
forecast_x = np.arange(len(values), len(values) + periods)
forecast_values = slope * forecast_x + intercept
# Determine trend
if slope > 0.01 * np.mean(values):
trend = 'up'
elif slope < -0.01 * np.mean(values):
trend = 'down'
else:
trend = 'neutral'
return {
'forecast': [round(float(v), 2) for v in forecast_values],
'trend': trend,
'slope': round(float(slope), 4),
'r_squared': round(float(np.corrcoef(x, values)[0, 1] ** 2) if len(values) > 2 else 0, 3),
'next_value': round(float(forecast_values[0]), 2),
'growth_rate': round(float(slope / np.mean(values) * 100), 1) if np.mean(values) != 0 else 0
}
except:
return {'forecast': [], 'trend': 'neutral', 'slope': 0}
def calculate_period_comparison(df: pd.DataFrame, value_col: str) -> Dict:
"""
Period-over-period comparison - Power BI style
Compares first half vs second half, quartiles, etc.
"""
try:
values = df[value_col].fillna(0)
n = len(values)
if n < 4:
return {'periods': [], 'best_period': '', 'worst_period': ''}
# Split into 4 quarters
q_size = n // 4
quarters = []
for i in range(4):
start = i * q_size
end = (i + 1) * q_size if i < 3 else n
q_values = values.iloc[start:end]
quarters.append({
'name': f'Q{i+1}',
'sum': round(float(q_values.sum()), 2),
'avg': round(float(q_values.mean()), 2),
'count': len(q_values)
})
# Find best and worst
best = max(quarters, key=lambda x: x['sum'])
worst = min(quarters, key=lambda x: x['sum'])
# Calculate Q-over-Q growth
qoq_growth = []
for i in range(1, len(quarters)):
prev = quarters[i-1]['sum']
curr = quarters[i]['sum']
growth = ((curr - prev) / prev * 100) if prev != 0 else 0
qoq_growth.append(round(growth, 1))
return {
'periods': quarters,
'best_period': best['name'],
'worst_period': worst['name'],
'qoq_growth': qoq_growth,
'avg_qoq_growth': round(np.mean(qoq_growth), 1) if qoq_growth else 0
}
except:
return {'periods': [], 'best_period': '', 'worst_period': ''}
def calculate_advanced_kpis(df: pd.DataFrame, palette: Dict) -> List[Dict]:
"""
ENTERPRISE-GRADE KPIs - Premium Analytics Dashboard
Includes:
- CAGR (Compound Annual Growth Rate)
- Z-Score Anomaly Detection
- Percentile Rankings
- Market Concentration (HHI, CR4)
- Momentum Indicators
- Seasonality Detection
- Statistical Confidence
"""
kpis = []
profile = dashboard_column_profile(df)
numeric_cols = profile['metrics']
categorical_cols = profile['dimensions']
datetime_cols = profile['dates']
# 1. Total Records with completeness score
null_pct = df.isnull().sum().sum() / (len(df) * len(df.columns)) * 100
completeness = round(100 - null_pct, 1)
kpis.append({
'title': 'Total Records',
'value': len(df),
'format': 'number',
'trend': 'neutral',
'comparison': f'{completeness}% data completeness',
'kpi_type': 'count',
'sparkline': df[numeric_cols[0]].head(20).tolist() if numeric_cols else None
})
# Finance KPIs appear only when the data actually supplies the required
# measures. A bank transaction "Amount" is not falsely labelled profit.
revenue_col = find_business_metric(numeric_cols, ('revenue', 'sales', 'income'))
cost_col = find_business_metric(numeric_cols, ('cost', 'expense', 'cogs'))
profit_col = find_business_metric(numeric_cols, ('profit', 'net_income', 'ebitda'))
if revenue_col:
revenue = float(df[revenue_col].sum())
kpis.append({'title': 'Total Revenue', 'value': revenue, 'format': 'currency', 'trend': 'neutral', 'comparison': f'Sourced from {revenue_col}', 'kpi_type': 'revenue', 'sparkline': df[revenue_col].tail(20).tolist()})
if cost_col:
costs = float(df[cost_col].sum())
kpis.append({'title': 'Total Costs', 'value': costs, 'format': 'currency', 'trend': 'down', 'comparison': f'Sourced from {cost_col}', 'kpi_type': 'cost', 'sparkline': df[cost_col].tail(20).tolist()})
if profit_col or (revenue_col and cost_col):
profit = float(df[profit_col].sum()) if profit_col else float(df[revenue_col].sum() - df[cost_col].sum())
revenue = float(df[revenue_col].sum()) if revenue_col else 0
margin = (profit / revenue * 100) if revenue else None
kpis.append({'title': 'Net Profit' if profit_col else 'Calculated Net Profit', 'value': profit, 'format': 'currency', 'trend': 'up' if profit >= 0 else 'down', 'comparison': f'Profit margin: {margin:.1f}%' if margin is not None else f'Sourced from {profit_col}', 'kpi_type': 'profit'})
# 2. Primary Metric with CAGR
if numeric_cols:
try:
col = numeric_cols[0]
total = float(df[col].sum())
cagr_data = calculate_cagr(df, col)
trend_data = calculate_trend_forecast(df, col)
col_lower = col.lower()
is_currency = any(x in col_lower for x in ['price', 'amount', 'revenue', 'cost', 'sales', 'profit', 'budget'])
cagr_display = f"CAGR: {cagr_data.get('cagr', 0):+.1f}%" if cagr_data.get('has_data') else f"Trend: {trend_data.get('growth_rate', 0):+.1f}%"
kpis.append({
'title': f'Total {col.replace("_", " ").title()}',
'value': total,
'format': 'currency' if is_currency else 'number',
'trend': trend_data.get('trend', 'neutral'),
'comparison': cagr_display,
'kpi_type': 'sum_with_cagr',
'cagr': cagr_data.get('cagr', 0),
'sparkline': df[col].tail(20).tolist()
})
except:
pass
# 3. Anomaly Detection KPI
if numeric_cols:
try:
col = numeric_cols[0]
z_data = calculate_z_scores(df, col)
if z_data.get('anomaly_count', 0) > 0:
trend = 'down' if z_data['anomaly_pct'] > 5 else 'neutral'
kpis.append({
'title': f'Anomalies Detected',
'value': z_data['anomaly_count'],
'format': 'number',
'trend': trend,
'comparison': f'{z_data["anomaly_pct"]:.1f}% of data | Max Z: {z_data["max_z"]:.1f}',
'kpi_type': 'anomaly',
'anomaly_pct': z_data['anomaly_pct']
})
except:
pass
# 4. Market Concentration / Dominance
if numeric_cols and categorical_cols:
try:
main_num = numeric_cols[0]
main_cat = categorical_cols[0]
concentration = calculate_concentration_ratio(df, main_cat, main_num)
if concentration.get('has_data'):
kpis.append({
'title': f'Market Leader ({main_cat[:12]})',
'value': concentration.get('leader_name', 'N/A'),
'format': 'text',
'trend': 'up',
'comparison': f'{concentration.get("leader_share", 0):.1f}% share | HHI: {concentration.get("hhi", 0):,.0f}',
'kpi_type': 'concentration',
'market_structure': concentration.get('market_structure', 'unknown'),
'cr4': concentration.get('cr4', 0)
})
except:
pass
# 5. Momentum Indicator
if numeric_cols:
try:
col = numeric_cols[0]
momentum = calculate_momentum(df, col, window=5)
if momentum.get('has_data'):
trend = 'up' if 'up' in momentum['momentum'] else ('down' if 'down' in momentum['momentum'] else 'neutral')
emoji = 'πŸš€' if momentum['momentum'] == 'strong_up' else ('πŸ“ˆ' if momentum['momentum'] == 'up' else ('πŸ“‰' if momentum['momentum'] == 'down' else 'πŸ’₯'))
kpis.append({
'title': f'Momentum ({col[:15]})',
'value': f'{momentum["roc"]:+.1f}%',
'format': 'text',
'trend': trend,
'comparison': f'{emoji} {momentum["momentum"].replace("_", " ").title()}',
'kpi_type': 'momentum',
'roc': momentum['roc']
})
except:
pass
# 6. Percentile Distribution
if numeric_cols:
try:
col = numeric_cols[0]
percentile_data = calculate_percentile_rank(df, col)
if percentile_data.get('has_data'):
p50 = percentile_data['percentiles'].get('p50', 0)
p90 = percentile_data['percentiles'].get('p90', 0)
kpis.append({
'title': f'Median {col[:12]}',
'value': p50,
'format': 'number',
'trend': 'neutral',
'comparison': f'P90: {p90:,.0f} | {percentile_data["distribution"].replace("_", " ").title()}',
'kpi_type': 'percentile',
'distribution': percentile_data['distribution'],
'skewness': percentile_data['skewness']
})
except:
pass
# 7. Seasonality Detection
if numeric_cols and len(df) > 20:
try:
col = numeric_cols[0]
seasonality = calculate_seasonality(df, col)
if seasonality.get('has_data') and seasonality.get('seasonal'):
kpis.append({
'title': 'Seasonality Pattern',
'value': seasonality.get('pattern', 'Unknown').title(),
'format': 'text',
'trend': 'neutral',
'comparison': f'Correlation: {seasonality.get("correlation", 0):.2f}',
'kpi_type': 'seasonality',
'pattern': seasonality.get('pattern')
})
except:
pass
# 8. Variance & Volatility
if numeric_cols:
try:
col = numeric_cols[0]
variance = calculate_variance_analysis(df, col)
cv = variance.get('cv', 0)
if cv > 50:
volatility = 'High'
trend = 'down'
elif cv > 20:
volatility = 'Medium'
trend = 'neutral'
else:
volatility = 'Low'
trend = 'up'
kpis.append({
'title': f'Volatility ({col[:12]})',
'value': f'{volatility}',
'format': 'text',
'trend': trend,
'comparison': f'CV: {cv:.0f}% | {variance.get("outlier_count", 0)} outliers',
'kpi_type': 'volatility',
'cv': cv,
'outlier_pct': variance.get('outlier_pct', 0)
})
except:
pass
# 9. Q-o-Q Growth (if we have enough KPIs)
if len(kpis) < 8 and numeric_cols:
try:
col = numeric_cols[0]
period_data = calculate_period_comparison(df, col)
if period_data.get('periods'):
avg_growth = period_data.get('avg_qoq_growth', 0)
trend = 'up' if avg_growth > 0 else ('down' if avg_growth < 0 else 'neutral')
kpis.append({
'title': 'Avg Period Growth',
'value': avg_growth,
'format': 'percentage',
'trend': trend,
'comparison': f'Best: {period_data.get("best_period", "N/A")} | Worst: {period_data.get("worst_period", "N/A")}',
'kpi_type': 'growth'
})
except:
pass
# 10. Secondary metric (if space available)
if len(kpis) < 8 and len(numeric_cols) >= 2:
try:
col = numeric_cols[1]
total = float(df[col].sum())
mean = float(df[col].mean())
col_lower = col.lower()
is_currency = any(x in col_lower for x in ['price', 'amount', 'revenue', 'cost', 'sales', 'profit', 'budget'])
kpis.append({
'title': f'Total {col.replace("_", " ").title()}',
'value': total,
'format': 'currency' if is_currency else 'number',
'trend': 'neutral',
'comparison': f'Avg: {mean:,.0f}',
'kpi_type': 'sum',
'sparkline': df[col].tail(20).tolist()
})
except:
pass
return kpis[:10] # Return up to 10 advanced KPIs
def calculate_real_kpis(df: pd.DataFrame, palette: Dict) -> List[Dict]:
"""Calculate REAL KPIs with autonomous formatting and Silicon Valley precision"""
kpis = []
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
# Always show record count first
kpis.append({'title': 'Total Records', 'value': len(df), 'format': 'number', 'trend': 'neutral', 'comparison': f'{len(df.columns)} dimensions'})
for col in numeric_cols[:5]:
try:
total = float(df[col].sum())
mean = float(df[col].mean())
std = float(df[col].std()) if len(df) > 1 else 0
col_lower = col.lower()
# Smart Format Detection
is_currency = any(x in col_lower for x in ['price', 'amount', 'revenue', 'cost', 'sales', 'profit', 'budget', 'bill'])
is_percentage = any(x in col_lower for x in ['percent', 'rate', 'margin', 'ratio', 'probability'])
# Trend calculation (Linear Regression Slope for accuracy)
trend = 'neutral'
comparison = ''
if len(df) > 5:
# Use simple regression slope instead of just first/last half
y = df[col].fillna(0).values
mid = len(y) // 2
recent = y[mid:].mean()
past = y[:mid].mean()
if past != 0:
delta = ((recent - past) / past) * 100
if delta > 1: trend = 'up'
elif delta < -1: trend = 'down'
comparison = f"{abs(delta):.1f}% vs past avg"
if is_currency:
kpis.append({
'title': f'Total {col.replace("_", " ").title()}',
'value': total,
'format': 'currency',
'trend': trend,
'comparison': comparison or f'Avg: ${mean:,.0f}'
})
elif is_percentage:
kpis.append({
'title': f'Avg {col.replace("_", " ").title()}',
'value': mean,
'format': 'percentage',
'trend': trend,
'comparison': comparison or f'Οƒ: {std:.1f}'
})
else:
# Decide between Sum and Mean based on variance
if std > mean * 2: # High variance usually means Sum is better (like sales)
kpis.append({'title': f'Total {col}', 'value': total, 'format': 'number', 'trend': trend, 'comparison': comparison})
else:
kpis.append({'title': f'Avg {col}', 'value': mean, 'format': 'number', 'trend': trend, 'comparison': comparison})
except Exception as e:
logger.warning(f"KPI error for {col}: {e}")
pass
return kpis[:6]
def generate_real_insights(df: pd.DataFrame) -> List[str]:
"""
Generate ENTERPRISE-GRADE insights using advanced analytics
Includes: CAGR, Z-scores, Seasonality, Market Concentration, Momentum
"""
insights = []
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
datetime_cols = [c for c in df.columns if 'date' in c.lower() or 'time' in c.lower()]
# Filter out ID columns
numeric_cols = [c for c in numeric_cols if not any(x in c.lower() for x in ['id', 'index', 'key', 'code'])]
# === ADVANCED INSIGHT GENERATION ===
# 1. CAGR Insight
if numeric_cols:
try:
col = numeric_cols[0]
cagr_data = calculate_cagr(df, col)
if cagr_data.get('has_data') and cagr_data.get('cagr', 0) != 0:
trend_emoji = 'πŸ“ˆ' if cagr_data['cagr'] > 0 else 'πŸ“‰'
insights.append(f"{trend_emoji} **Growth Rate**: {col.replace('_', ' ').title()} shows {cagr_data['cagr']:+.1f}% CAGR across the dataset period")
except:
pass
# 2. Anomaly Detection Insight
if numeric_cols:
try:
col = numeric_cols[0]
z_data = calculate_z_scores(df, col)
if z_data.get('anomaly_count', 0) > 0:
insights.append(f"⚠️ **Anomaly Alert**: {z_data['anomaly_count']} outliers detected in {col.replace('_', ' ')} ({z_data['anomaly_pct']:.1f}% of data)")
except:
pass
# 3. Market Concentration Insight
if numeric_cols and categorical_cols:
try:
main_num = numeric_cols[0]
main_cat = categorical_cols[0]
concentration = calculate_concentration_ratio(df, main_cat, main_num)
if concentration.get('has_data'):
structure = concentration.get('market_structure', 'unknown')
if structure == 'highly_concentrated':
insights.append(f"πŸ† **Market Dominance**: {concentration['leader_name']} holds {concentration['leader_share']:.0f}% share (HHI: {concentration['hhi']:,.0f})")
elif concentration['cr4'] > 60:
insights.append(f"πŸ“Š **Concentration**: Top 4 {main_cat}s control {concentration['cr4']:.0f}% of total {main_num.replace('_', ' ')}")
except:
pass
# 4. Momentum Insight
if numeric_cols:
try:
col = numeric_cols[0]
momentum = calculate_momentum(df, col, window=5)
if momentum.get('has_data'):
if momentum['momentum'] == 'strong_up':
insights.append(f"πŸš€ **Strong Momentum**: {col.replace('_', ' ').title()} surging {momentum['roc']:+.1f}% over recent period")
elif momentum['momentum'] == 'strong_down':
insights.append(f"πŸ’₯ **Momentum Warning**: {col.replace('_', ' ').title()} declining {momentum['roc']:.1f}% over recent period")
except:
pass
# 5. Seasonality Insight
if numeric_cols and len(df) > 20:
try:
col = numeric_cols[0]
seasonality = calculate_seasonality(df, col)
if seasonality.get('seasonal') and seasonality.get('correlation', 0) > 0.3:
insights.append(f"πŸ”„ **Seasonality Detected**: {seasonality['pattern'].title()} pattern found in {col.replace('_', ' ')} (r={seasonality['correlation']:.2f})")
except:
pass
# 6. Distribution Insight
if numeric_cols:
try:
col = numeric_cols[0]
percentile_data = calculate_percentile_rank(df, col)
if percentile_data.get('has_data'):
if percentile_data['distribution'] == 'right_skewed':
insights.append(f"πŸ“Š **Distribution**: {col.replace('_', ' ').title()} is right-skewed (skew={percentile_data['skewness']:.1f}) - most values below average")
elif percentile_data['distribution'] == 'left_skewed':
insights.append(f"πŸ“Š **Distribution**: {col.replace('_', ' ').title()} is left-skewed (skew={percentile_data['skewness']:.1f}) - most values above average")
except:
pass
# 7. Correlation Insight
if len(numeric_cols) >= 2:
try:
corr = df[numeric_cols].corr().abs()
mask = np.ones(corr.shape, dtype=bool)
np.fill_diagonal(mask, 0)
max_corr_stack = corr.where(mask).stack()
if not max_corr_stack.empty:
max_idx = max_corr_stack.idxmax()
max_val = max_corr_stack.max()
if max_val > 0.7:
insights.append(f"πŸ”— **Strong Correlation**: {max_idx[0].replace('_', ' ')} and {max_idx[1].replace('_', ' ')} are highly correlated (r={max_val:.2f})")
except:
pass
# 8. Pareto Insight
if categorical_cols and numeric_cols:
try:
cat, num = categorical_cols[0], numeric_cols[0]
pareto = calculate_pareto_analysis(df, cat, num)
if pareto.get('pareto_count', 0) > 0 and pareto.get('pareto_pct', 0) < 30:
insights.append(f"πŸ“Š **Pareto Principle**: {pareto['pareto_count']} of {pareto['total_categories']} {cat.replace('_', ' ')}s drive 80% of {num.replace('_', ' ')}")
except:
pass
# 9. Volatility Insight
if numeric_cols:
try:
col = numeric_cols[0]
variance = calculate_variance_analysis(df, col)
cv = variance.get('cv', 0)
if cv > 100:
insights.append(f"⚑ **High Volatility**: {col.replace('_', ' ').title()} shows extreme variability (CV={cv:.0f}%) - consider risk mitigation")
elif cv < 15:
insights.append(f"βœ… **Stability**: {col.replace('_', ' ').title()} is highly consistent (CV={cv:.0f}%) - reliable metric")
except:
pass
# === FALLBACK: Basic insights if we don't have enough ===
if len(insights) < 3 and numeric_cols:
try:
col = numeric_cols[0]
total = df[col].sum()
mean = df[col].mean()
insights.append(f"πŸ“Š **Summary**: Total {col.replace('_', ' ')} is {total:,.0f} with average {mean:,.0f}")
except:
pass
# Ensure we have at least one insight
if not insights:
insights.append(f"πŸ“Š **Dataset**: {len(df):,} records with {len(df.columns)} columns analyzed")
return sanitize_for_json(insights[:6])
def generate_recommendations(df: pd.DataFrame) -> List[str]:
"""Generate LLM-POWERED STRATEGIC, ACTIONABLE recommendations"""
recs = []
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
# Build data context for LLM
data_context = f"""πŸ“Š DATA PROFILE:
- Records: {len(df):,}
- Numeric metrics: {', '.join(numeric_cols[:5]) if numeric_cols else 'None'}
- Categories: {', '.join(categorical_cols[:3]) if categorical_cols else 'None'}
"""
# Find key patterns for recommendations
if numeric_cols and categorical_cols:
cat, num = categorical_cols[0], numeric_cols[0]
# Top performer
top = df.groupby(cat)[num].sum().idxmax()
top_val = df.groupby(cat)[num].sum().max()
total = df[num].sum()
top_share = (top_val / total * 100) if total > 0 else 0
# Underperformer
grouped = df.groupby(cat)[num].sum()
avg = grouped.mean()
underperformers = grouped[grouped < avg].sort_values(ascending=False)
underperformer = underperformers.index[0] if len(underperformers) > 0 else None
data_context += f"""
πŸ“ˆ KEY FINDINGS:
- Top performer: '{top}' with {top_share:.1f}% share
- Average per category: {avg:,.0f}
- Underperformer to optimize: '{underperformer}' (below average)
"""
# LLM to generate recommendations
try:
prompt = f"""You are a management consultant. Based on this business data, provide exactly 4 STRATEGIC RECOMMENDATIONS.
{data_context}
Rules:
1. Each recommendation must be actionable and specific
2. Reference specific data points where possible
3. Each recommendation should be 1 sentence, max 12 words
4. Focus on: Growth, Optimization, Risk Mitigation, Efficiency
Format: Start each with an action verb and use 'β†’' to show expected impact."""
response = chat(prompt, temperature=0.4, max_tokens=250)
# Parse LLM response
if response and 'error' not in response.lower() and 'rate limit' not in response.lower():
lines = [l.strip() for l in response.split('\n') if l.strip() and len(l) > 10]
# Filter out numbering and keep substantive lines
for line in lines:
clean = line.lstrip('0123456789.-) ').strip()
if clean and len(clean) > 15:
recs.append(clean)
if len(recs) >= 4:
break
except Exception as e:
logger.warning(f"LLM recommendation error: {e}")
# Fallback to rule-based if LLM didn't return enough
if len(recs) < 2:
recs = [] # Reset
try:
# 1. Growth Strategy
if categorical_cols and numeric_cols:
cat, num = categorical_cols[0], numeric_cols[0]
avg = df[num].mean()
underperformers = df[(df[num] < avg) & (df[num] > 0)]
if not underperformers.empty:
target = underperformers.nlargest(1, num)[cat].iloc[0]
recs.append(f"πŸš€ Optimize '{target}' β†’ potential +{int((avg - underperformers[num].mean())):,} increase")
# 2. Risk Mitigation (Outliers)
if numeric_cols:
col = numeric_cols[0]
std = df[col].std()
if std > 0:
z_scores = ((df[col] - df[col].mean()) / std).abs()
outliers = df[z_scores > 3]
if not outliers.empty:
recs.append(f"⚠️ Investigate {len(outliers)} extreme {col} values for anomalies")
# 3. Top Performer Focus
if categorical_cols and numeric_cols:
cat, num = categorical_cols[0], numeric_cols[0]
top = df.groupby(cat)[num].sum().idxmax()
recs.append(f"πŸ“ˆ Double down on '{top}' β†’ current top performer")
# 4. Automation
recs.append("πŸ”” Implement automated alerts for real-time threshold monitoring")
recs.append("πŸ“Š Set up weekly variance analysis on top contributors")
except Exception as e:
logger.warning(f"Recommendation error: {e}")
return sanitize_for_json(recs[:4])
def create_3d_surface_chart(df, rel, colors):
x, y, z = rel['x'], rel['y'], rel['z']
df_sample = df.dropna(subset=[x, y, z]).head(300)
return {
'chart_id': f'surface_{x}_{y}_{z}',
'title': f'3D Surface Mesh of {z} by {x} & {y}',
'type': 'mesh3d',
'plotly_config': {
'data': [{
'type': 'mesh3d',
'x': df_sample[x].tolist(),
'y': df_sample[y].tolist(),
'z': df_sample[z].tolist(),
'opacity': 0.8,
'color': colors[0]
}],
'layout': {
'paper_bgcolor': 'rgba(0,0,0,0)',
'scene': {
'xaxis': {'title': x},
'yaxis': {'title': y},
'zaxis': {'title': z}
}
}
}
}
def create_3d_bubble_chart(df, rel, colors):
x, y, z, c_col = rel['x'], rel['y'], rel['z'], rel['color']
df_sample = df.dropna(subset=[x, y, z]).head(200)
size_col = df_sample[c_col] if df_sample[c_col].dtype in ['int64', 'float64'] else df_sample[z]
s_min = size_col.min()
s_range = size_col.max() - s_min if size_col.max() > s_min else 1
sizes = ((size_col - s_min) / s_range * 20 + 5).tolist()
return {
'chart_id': f'3dbubble_{x}_{y}_{z}',
'title': f'3D Bubble: {x}, {y}, {z}',
'type': 'scatter3d',
'plotly_config': {
'data': [{
'type': 'scatter3d',
'mode': 'markers',
'x': df_sample[x].tolist(),
'y': df_sample[y].tolist(),
'z': df_sample[z].tolist(),
'marker': {
'size': sizes,
'color': (df_sample[c_col].astype('category').cat.codes if df[c_col].dtype == 'object' else df_sample[c_col]).tolist(),
'colorscale': 'Plasma',
'opacity': 0.7,
'line': {'width': 1, 'color': '#ffffff'}
}
}],
'layout': {
'paper_bgcolor': 'rgba(0,0,0,0)',
'scene': {
'xaxis': {'title': x},
'yaxis': {'title': y},
'zaxis': {'title': z}
}
}
}
}
def create_polar_bar_chart(df, rel, colors):
cat, num = rel['categorical'], rel['numeric']
grouped = df.groupby(cat)[num].sum().nlargest(12)
return {
'chart_id': f'polar_{cat}_{num}',
'title': f'Polar Dist: {num} by {cat}',
'type': 'barpolar',
'plotly_config': {
'data': [{
'type': 'barpolar',
'r': [float(v) for v in grouped.values],
'theta': [str(l)[:15] for l in grouped.index],
'marker': {
'color': colors[:len(grouped)] + [colors[0]]*(12-len(grouped)),
'line': {'color': 'white', 'width': 1}
},
'opacity': 0.8
}],
'layout': {
'paper_bgcolor': 'rgba(0,0,0,0)',
'polar': {
'radialaxis': {'visible': True, 'gridcolor': 'rgba(255,255,255,0.1)'},
'angularaxis': {'gridcolor': 'rgba(255,255,255,0.1)'}
},
'showlegend': False
}
}
}
def create_density_contour_chart(df, rel, colors):
x, y = rel['x'], rel['y']
df_sample = df.dropna(subset=[x, y]).head(500)
return {
'chart_id': f'contour_{x}_{y}',
'title': f'Density Contour: {x} vs {y}',
'type': 'histogram2dcontour',
'plotly_config': {
'data': [{
'type': 'histogram2dcontour',
'x': df_sample[x].tolist(),
'y': df_sample[y].tolist(),
'colorscale': 'Viridis',
'reversescale': True,
'contours': {
'showlines': True,
'size': 0.5
}
}],
'layout': {
'paper_bgcolor': 'rgba(0,0,0,0)',
'plot_bgcolor': 'rgba(0,0,0,0)',
'xaxis': {'title': x, 'gridcolor': 'rgba(255,255,255,0.05)'},
'yaxis': {'title': y, 'gridcolor': 'rgba(255,255,255,0.05)'}
}
}
}