import json import logging from typing import Dict, List, Optional, Any class ChartGenerator: """Generate Chart.js configurations for data visualization""" def __init__(self): self.chart_colors = [ '#007bff', '#28a745', '#ffc107', '#dc3545', '#6f42c1', '#fd7e14', '#20c997', '#6c757d', '#343a40', '#007bff' ] def create_chart_config(self, metrics: List[Dict], section_title: str) -> Optional[Dict]: """Create Chart.js configuration based on metrics data""" if not metrics: return None # Analyze metrics to determine best chart type chart_type = self._determine_chart_type(metrics) if chart_type == 'line': return self._create_line_chart(metrics, section_title) elif chart_type == 'bar': return self._create_bar_chart(metrics, section_title) elif chart_type == 'pie': return self._create_pie_chart(metrics, section_title) elif chart_type == 'doughnut': return self._create_doughnut_chart(metrics, section_title) else: return self._create_default_chart(metrics, section_title) def _determine_chart_type(self, metrics: List[Dict]) -> str: """Determine the most appropriate chart type for the data""" # Analyze metric types has_percentages = any('%' in str(metric.get('metric', '')) for metric in metrics) has_time_series = any('year' in str(metric.get('context', '')).lower() for metric in metrics) has_categories = len(metrics) <= 6 # Good for pie/doughnut charts if has_time_series and len(metrics) > 2: return 'line' elif has_percentages and has_categories: return 'doughnut' elif len(metrics) <= 5: return 'bar' else: return 'line' def _create_line_chart(self, metrics: List[Dict], title: str) -> Dict: """Create line chart configuration""" labels = [] data_points = [] for i, metric in enumerate(metrics[:10]): # Limit to 10 points labels.append(f"Point {i+1}") data_points.append(self._extract_numeric_value(metric.get('metric', '0'))) return { 'type': 'line', 'data': { 'labels': labels, 'datasets': [{ 'label': f'{title} Trend', 'data': data_points, 'borderColor': self.chart_colors[0], 'backgroundColor': self.chart_colors[0] + '20', 'tension': 0.4, 'fill': True }] }, 'options': { 'responsive': True, 'plugins': { 'title': { 'display': True, 'text': f'{title} - Data Analysis' }, 'legend': { 'position': 'top' } }, 'scales': { 'y': { 'beginAtZero': True, 'title': { 'display': True, 'text': 'Value' } } } } } def _create_bar_chart(self, metrics: List[Dict], title: str) -> Dict: """Create bar chart configuration""" labels = [] data_points = [] for metric in metrics[:8]: # Limit to 8 bars for readability context = metric.get('context', '') # Extract meaningful label from context label = self._extract_label_from_context(context) or f"Metric {len(labels)+1}" labels.append(label) data_points.append(self._extract_numeric_value(metric.get('metric', '0'))) return { 'type': 'bar', 'data': { 'labels': labels, 'datasets': [{ 'label': title, 'data': data_points, 'backgroundColor': self.chart_colors[:len(data_points)], 'borderColor': self.chart_colors[:len(data_points)], 'borderWidth': 1 }] }, 'options': { 'responsive': True, 'plugins': { 'title': { 'display': True, 'text': f'{title} - Comparative Analysis' }, 'legend': { 'display': False } }, 'scales': { 'y': { 'beginAtZero': True, 'title': { 'display': True, 'text': 'Value' } }, 'x': { 'title': { 'display': True, 'text': 'Categories' } } } } } def _create_pie_chart(self, metrics: List[Dict], title: str) -> Dict: """Create pie chart configuration""" labels = [] data_points = [] for metric in metrics[:6]: # Limit to 6 slices for readability context = metric.get('context', '') label = self._extract_label_from_context(context) or f"Category {len(labels)+1}" labels.append(label) data_points.append(self._extract_numeric_value(metric.get('metric', '0'))) return { 'type': 'pie', 'data': { 'labels': labels, 'datasets': [{ 'data': data_points, 'backgroundColor': self.chart_colors[:len(data_points)], 'borderColor': '#ffffff', 'borderWidth': 2 }] }, 'options': { 'responsive': True, 'plugins': { 'title': { 'display': True, 'text': f'{title} - Distribution Analysis' }, 'legend': { 'position': 'right' } } } } def _create_doughnut_chart(self, metrics: List[Dict], title: str) -> Dict: """Create doughnut chart configuration""" config = self._create_pie_chart(metrics, title) config['type'] = 'doughnut' config['options']['plugins']['title']['text'] = f'{title} - Key Metrics Overview' return config def _create_default_chart(self, metrics: List[Dict], title: str) -> Dict: """Create default chart when type cannot be determined""" return self._create_bar_chart(metrics, title) def _extract_numeric_value(self, metric_str: str) -> float: """Extract numeric value from metric string""" import re if not metric_str: return 0.0 # Remove common non-numeric characters cleaned = re.sub(r'[^0-9.,\-+]', '', str(metric_str)) # Handle percentages if '%' in str(metric_str): cleaned = cleaned.replace('%', '') # Handle currency if '$' in str(metric_str): cleaned = cleaned.replace('$', '') # Handle billions, millions, etc. if 'billion' in str(metric_str).lower(): try: return float(cleaned.replace(',', '')) * 1000000000 except: return 0.0 elif 'million' in str(metric_str).lower(): try: return float(cleaned.replace(',', '')) * 1000000 except: return 0.0 # Try to convert to float try: return float(cleaned.replace(',', '')) except: return 0.0 def _extract_label_from_context(self, context: str) -> Optional[str]: """Extract meaningful label from context""" if not context: return None # Simple extraction of first few words words = context.split()[:3] return ' '.join(words) if words else None def create_multi_series_chart(self, data_series: List[Dict], title: str) -> Dict: """Create chart with multiple data series""" datasets = [] labels = [] for i, series in enumerate(data_series): series_data = series.get('data', []) datasets.append({ 'label': series.get('name', f'Series {i+1}'), 'data': [self._extract_numeric_value(str(val)) for val in series_data], 'borderColor': self.chart_colors[i % len(self.chart_colors)], 'backgroundColor': self.chart_colors[i % len(self.chart_colors)] + '20', 'tension': 0.4 }) if not labels and series.get('labels'): labels = series.get('labels', []) if not labels: labels = [f"Point {i+1}" for i in range(max(len(ds['data']) for ds in datasets) if datasets else 0)] return { 'type': 'line', 'data': { 'labels': labels, 'datasets': datasets }, 'options': { 'responsive': True, 'plugins': { 'title': { 'display': True, 'text': f'{title} - Multi-Series Analysis' }, 'legend': { 'position': 'top' } }, 'scales': { 'y': { 'beginAtZero': True, 'title': { 'display': True, 'text': 'Value' } } } } } def generate_chart_html(self, chart_config: Dict, chart_id: str) -> str: """Generate HTML for embedding chart""" return f"""
"""