NewsLetter / utils /chart_generator.py
SmartHeal's picture
Upload 19 files
a19173c verified
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"""
<div class="chart-container" style="position: relative; height: 400px; margin: 20px 0;">
<canvas id="{chart_id}"></canvas>
</div>
<script>
const chartConfig_{chart_id} = {json.dumps(chart_config)};
const ctx_{chart_id} = document.getElementById('{chart_id}').getContext('2d');
new Chart(ctx_{chart_id}, chartConfig_{chart_id});
</script>
"""