# Chart Generator MCP Service """ Enterprise Chart Payload Generator Generates frontend-compatible chart payloads for: - Forecast line charts with confidence bands - Scenario comparison bar charts - Churn distribution charts - Risk heatmaps - Driver waterfall charts - Multi-dataset overlay charts Usage: from mcp.chart_generator import ChartGenerator generator = ChartGenerator() payload = generator.forecast_chart(prediction_result) """ from typing import Dict, List, Any, Optional from dataclasses import dataclass, asdict from datetime import datetime import json @dataclass class ChartPayload: """Base chart payload for frontend rendering""" chart_type: str title: str x: List[str] series: List[Dict[str, Any]] options: Dict[str, Any] class ChartGenerator: """ Enterprise Chart Payload Generator. Generates structured payloads compatible with: - Recharts (React) - Chart.js - Custom chart components """ # Chart type constants FORECAST_LINE = 'forecast_line' SCENARIO_BAR = 'scenario_bar' CHURN_DIST = 'churn_distribution' RISK_HEATMAP = 'risk_heatmap' DRIVER_WATERFALL = 'driver_waterfall' COMPARISON_OVERLAY = 'comparison_overlay' PROFIT_LOSS = 'profit_loss_curve' def __init__(self): self.default_colors = { 'primary': '#f97316', # Orange 'secondary': '#3b82f6', # Blue 'success': '#22c55e', # Green 'danger': '#ef4444', # Red 'warning': '#eab308', # Yellow 'muted': '#6b7280', # Gray 'confidence': 'rgba(249, 115, 22, 0.2)', # Orange transparent } def forecast_chart( self, historical: List[Dict], forecast: List[Dict], title: str = "Forecast", currency: str = "₹", show_confidence: bool = True ) -> Dict[str, Any]: """ Generate forecast line chart with confidence bands. Args: historical: List of {date, value} for historical data forecast: List of {date, value, lower, upper} for forecast title: Chart title currency: Currency symbol show_confidence: Whether to show confidence bands Returns: Chart payload for frontend """ # Build x-axis labels x_labels = [] for h in historical: date = h.get('date', '') if hasattr(date, 'strftime'): x_labels.append(date.strftime('%b %Y')) else: x_labels.append(str(date)[:10]) for f in forecast: date = f.get('date', '') if hasattr(date, 'strftime'): x_labels.append(date.strftime('%b %Y')) else: x_labels.append(str(date)[:10]) # Build series data n_historical = len(historical) n_total = n_historical + len(forecast) # Historical series (actual values) y_actual = [h.get('value') for h in historical] + [None] * len(forecast) # Forecast series (with connection to last historical point) y_forecast = [None] * (n_historical - 1) if historical: y_forecast.append(historical[-1].get('value')) # Connect point y_forecast.extend([f.get('value') for f in forecast]) # Confidence bands y_upper = [None] * n_historical y_lower = [None] * n_historical if show_confidence: for f in forecast: y_upper.append(f.get('upper', f.get('value') * 1.1)) y_lower.append(f.get('lower', f.get('value') * 0.9)) return { 'chart_type': self.FORECAST_LINE, 'title': title, 'x': x_labels, 'y_actual': y_actual, 'y_forecast': y_forecast, 'y_upper': y_upper if show_confidence else [], 'y_lower': y_lower if show_confidence else [], 'confidence_band': show_confidence, 'currency': currency, 'series': [ { 'name': 'Historical', 'data': y_actual, 'type': 'line', 'color': self.default_colors['secondary'], 'strokeWidth': 3, 'dot': True }, { 'name': 'Forecast', 'data': y_forecast, 'type': 'line', 'color': self.default_colors['primary'], 'strokeWidth': 3, 'strokeDasharray': '8 4', 'dot': True } ], 'options': { 'animationDuration': 1000, 'showGrid': True, 'showLegend': True, 'yAxisFormatter': f'{currency}{{value}}', 'tooltipFormatter': f'{currency}{{value:,.0f}}' } } def scenario_chart( self, scenarios: List[Dict], metric: str = 'profit', title: str = "Scenario Comparison", currency: str = "₹", best_scenario: Optional[str] = None ) -> Dict[str, Any]: """ Generate scenario comparison bar chart. Args: scenarios: List of scenario dicts with name, revenue, profit, risk metric: Which metric to compare ('revenue', 'profit') title: Chart title currency: Currency symbol best_scenario: Name of recommended scenario Returns: Chart payload for frontend """ x_labels = [s.get('name', f'Scenario {i+1}') for i, s in enumerate(scenarios)] values = [s.get(metric, 0) for s in scenarios] changes = [s.get('change_pct', 0) for s in scenarios] risks = [s.get('risk', 'medium') for s in scenarios] # Determine bar colors colors = [] for i, s in enumerate(scenarios): name = s.get('name', '') if name == best_scenario: colors.append(self.default_colors['success']) elif name == 'Current State': colors.append(self.default_colors['secondary']) elif s.get('risk') == 'high': colors.append(self.default_colors['danger']) else: colors.append(self.default_colors['primary']) return { 'chart_type': self.SCENARIO_BAR, 'title': title, 'x': x_labels, 'values': values, 'changes': changes, 'risks': risks, 'colors': colors, 'best_scenario': best_scenario, 'currency': currency, 'series': [ { 'name': metric.capitalize(), 'data': values, 'type': 'bar', 'colors': colors } ], 'options': { 'animationDuration': 800, 'barRadius': 8, 'showLabels': True, 'labelPosition': 'top', 'highlightBest': True } } def churn_chart( self, churn_data: List[Dict], title: str = "Churn Prediction" ) -> Dict[str, Any]: """ Generate churn probability distribution chart. Args: churn_data: List of {segment, probability, count} title: Chart title Returns: Chart payload for frontend """ segments = [d.get('segment', f'Segment {i+1}') for i, d in enumerate(churn_data)] probabilities = [d.get('probability', 0) for d in churn_data] counts = [d.get('count', 0) for d in churn_data] # Color by risk level colors = [] for prob in probabilities: if prob > 0.3: colors.append(self.default_colors['danger']) elif prob > 0.15: colors.append(self.default_colors['warning']) else: colors.append(self.default_colors['success']) return { 'chart_type': self.CHURN_DIST, 'title': title, 'x': segments, 'probabilities': probabilities, 'counts': counts, 'colors': colors, 'series': [ { 'name': 'Churn Risk', 'data': [p * 100 for p in probabilities], # Convert to percentage 'type': 'bar', 'colors': colors }, { 'name': 'Customer Count', 'data': counts, 'type': 'line', 'color': self.default_colors['muted'], 'yAxisId': 'right' } ], 'options': { 'showDualAxis': True, 'leftAxisLabel': 'Churn Risk (%)', 'rightAxisLabel': 'Customer Count', 'highlightHighRisk': True } } def profit_loss_chart( self, data: List[Dict], title: str = "Profit/Loss Analysis", currency: str = "₹" ) -> Dict[str, Any]: """ Generate profit vs loss curve chart. Args: data: List of {period, revenue, cost, profit} title: Chart title currency: Currency symbol Returns: Chart payload for frontend """ periods = [d.get('period', f'P{i+1}') for i, d in enumerate(data)] revenues = [d.get('revenue', 0) for d in data] costs = [d.get('cost', 0) for d in data] profits = [d.get('profit', r - c) for d, r, c in zip(data, revenues, costs)] # Calculate cumulative profit cumulative = [] total = 0 for p in profits: total += p cumulative.append(total) # Determine profit/loss colors for each point profit_colors = [ self.default_colors['success'] if p >= 0 else self.default_colors['danger'] for p in profits ] return { 'chart_type': self.PROFIT_LOSS, 'title': title, 'x': periods, 'revenues': revenues, 'costs': costs, 'profits': profits, 'cumulative': cumulative, 'currency': currency, 'series': [ { 'name': 'Revenue', 'data': revenues, 'type': 'area', 'color': self.default_colors['secondary'], 'fillOpacity': 0.3 }, { 'name': 'Cost', 'data': costs, 'type': 'area', 'color': self.default_colors['danger'], 'fillOpacity': 0.3 }, { 'name': 'Profit', 'data': profits, 'type': 'bar', 'colors': profit_colors } ], 'options': { 'showBreakeven': True, 'breakEvenLine': 0, 'highlightProfit': True } } def comparison_overlay_chart( self, datasets: List[Dict], title: str = "Multi-Dataset Comparison" ) -> Dict[str, Any]: """ Generate multi-dataset overlay chart. Args: datasets: List of {name, data, color} where data is [{x, y}] title: Chart title Returns: Chart payload for frontend """ # Find all unique x values all_x = set() for ds in datasets: for point in ds.get('data', []): all_x.add(point.get('x', '')) x_labels = sorted(list(all_x)) # Build series series = [] colors = [ self.default_colors['primary'], self.default_colors['secondary'], self.default_colors['success'], self.default_colors['warning'], self.default_colors['danger'] ] for i, ds in enumerate(datasets): name = ds.get('name', f'Dataset {i+1}') color = ds.get('color', colors[i % len(colors)]) data_dict = {p.get('x'): p.get('y') for p in ds.get('data', [])} values = [data_dict.get(x) for x in x_labels] series.append({ 'name': name, 'data': values, 'type': 'line', 'color': color, 'strokeWidth': 2 }) return { 'chart_type': self.COMPARISON_OVERLAY, 'title': title, 'x': x_labels, 'series': series, 'options': { 'showLegend': True, 'legendPosition': 'top', 'enableHover': True, 'showDots': True } } def driver_waterfall_chart( self, drivers: List[Dict], title: str = "Driver Analysis", currency: str = "₹" ) -> Dict[str, Any]: """ Generate driver waterfall chart (what caused the change). Args: drivers: List of {name, impact, type} where type is 'increase' or 'decrease' title: Chart title currency: Currency symbol Returns: Chart payload for frontend """ names = [d.get('name', f'Driver {i+1}') for i, d in enumerate(drivers)] impacts = [d.get('impact', 0) for d in drivers] types = [d.get('type', 'increase' if d.get('impact', 0) >= 0 else 'decrease') for d in drivers] # Calculate running total for waterfall running = [0] for impact in impacts: running.append(running[-1] + impact) colors = [ self.default_colors['success'] if t == 'increase' else self.default_colors['danger'] for t in types ] return { 'chart_type': self.DRIVER_WATERFALL, 'title': title, 'x': names, 'impacts': impacts, 'running_total': running[1:], 'types': types, 'colors': colors, 'currency': currency, 'series': [ { 'name': 'Impact', 'data': impacts, 'type': 'waterfall', 'colors': colors } ], 'options': { 'showConnectors': True, 'showTotalBar': True, 'startLabel': 'Start', 'endLabel': 'End' } } def risk_heatmap( self, risk_matrix: List[List[float]], x_labels: List[str], y_labels: List[str], title: str = "Risk Assessment" ) -> Dict[str, Any]: """ Generate risk heatmap. Args: risk_matrix: 2D array of risk scores (0-1) x_labels: Column labels y_labels: Row labels title: Chart title Returns: Chart payload for frontend """ # Flatten matrix to points points = [] for i, row in enumerate(risk_matrix): for j, val in enumerate(row): # Determine color based on risk level if val > 0.7: color = self.default_colors['danger'] elif val > 0.4: color = self.default_colors['warning'] else: color = self.default_colors['success'] points.append({ 'x': j, 'y': i, 'value': val, 'color': color, 'label': f'{val:.1%}' }) return { 'chart_type': self.RISK_HEATMAP, 'title': title, 'x_labels': x_labels, 'y_labels': y_labels, 'points': points, 'matrix': risk_matrix, 'options': { 'showLabels': True, 'colorScale': { 'low': self.default_colors['success'], 'medium': self.default_colors['warning'], 'high': self.default_colors['danger'] }, 'cellRadius': 4 } } # Convenience functions def generate_forecast_chart(prediction_result: Dict) -> Dict[str, Any]: """Generate forecast chart from prediction result.""" generator = ChartGenerator() return generator.forecast_chart( historical=prediction_result.get('historical_points', []), forecast=prediction_result.get('forecast_points', []), title=prediction_result.get('title') or f"Revenue Forecast ({len(prediction_result.get('forecast_points', []))} Periods)" ) def generate_scenario_chart(simulation_result: Dict) -> Dict[str, Any]: """Generate scenario chart from simulation result.""" generator = ChartGenerator() return generator.scenario_chart( scenarios=simulation_result.get('scenarios', []), best_scenario=simulation_result.get('best_scenario') ) # Quick test if __name__ == "__main__": generator = ChartGenerator() # Test forecast chart historical = [ {'date': '2024-01', 'value': 10000}, {'date': '2024-02', 'value': 10500}, {'date': '2024-03', 'value': 11000}, ] forecast = [ {'date': '2024-04', 'value': 11500, 'lower': 10500, 'upper': 12500}, {'date': '2024-05', 'value': 12000, 'lower': 10800, 'upper': 13200}, ] chart = generator.forecast_chart(historical, forecast, "Test Forecast") print("Forecast Chart:") print(f" Type: {chart['chart_type']}") print(f" X Labels: {chart['x']}") print(f" Series: {len(chart['series'])}")