Spaces:
Runtime error
Runtime error
Download pdf_chart_generator.py from happynickamazon/zenvolt-api: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/happynickamazon/zenvolt-api/resolve/main/pdf_chart_generator.py
- Command line
-
hf download hf://spaces/happynickamazon/zenvolt-api/pdf_chart_generator.py
-
curl -L -o pdf_chart_generator.py https://huggingface.co/spaces/happynickamazon/zenvolt-api/resolve/main/pdf_chart_generator.py
10.9 kB
| """ | |
| PDF Chart Generator | |
| Generates matplotlib charts as base64 images for embedding in PDF reports. | |
| MIT/Stanford Quality: | |
| - Publication-quality charts | |
| - Optimized for print (300 DPI) | |
| - Professional styling | |
| - Fast rendering | |
| """ | |
| import matplotlib | |
| matplotlib.use('Agg') # Non-interactive backend | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| from io import BytesIO | |
| import base64 | |
| from typing import List, Dict, Optional | |
| import seaborn as sns | |
| # Set style | |
| plt.style.use('seaborn-v0_8-darkgrid') | |
| sns.set_palette("husl") | |
| def generate_health_timeline_chart( | |
| history_data: List[Dict], | |
| asset_name: str, | |
| width: int = 8, | |
| height: int = 4, | |
| dpi: int = 150 | |
| ) -> str: | |
| """ | |
| Generate health score timeline chart. | |
| Parameters: | |
| ----------- | |
| history_data : list | |
| List of prediction dictionaries with health_score and prediction_date | |
| asset_name : str | |
| Asset name for title | |
| width, height : int | |
| Figure dimensions in inches | |
| dpi : int | |
| Resolution (150 for screen, 300 for print) | |
| Returns: | |
| -------- | |
| str: Base64-encoded PNG image | |
| """ | |
| fig, ax = plt.subplots(figsize=(width, height), dpi=dpi) | |
| # Extract data | |
| dates = [pd.to_datetime(p['prediction_date']) for p in history_data] | |
| scores = [p['health_score'] for p in history_data] | |
| lower_bounds = [p.get('soh_lower_95', 0) * 100 for p in history_data] | |
| upper_bounds = [p.get('soh_upper_95', 1) * 100 for p in history_data] | |
| # Plot | |
| ax.plot(dates, scores, 'o-', linewidth=2.5, markersize=8, label='Health Score', color='#22c55e') | |
| ax.fill_between(dates, lower_bounds, upper_bounds, alpha=0.2, color='#22c55e', label='95% Confidence') | |
| # Styling | |
| ax.set_xlabel('Date', fontsize=11, fontweight='bold') | |
| ax.set_ylabel('Health Score', fontsize=11, fontweight='bold') | |
| ax.set_title(f'Health Timeline - {asset_name}', fontsize=13, fontweight='bold', pad=15) | |
| ax.grid(True, alpha=0.3, linestyle='--') | |
| ax.legend(loc='best', frameon=True, shadow=True) | |
| ax.set_ylim([0, 105]) | |
| # Add threshold line | |
| ax.axhline(y=80, color='#ef4444', linestyle='--', linewidth=1.5, alpha=0.7, label='EOL Threshold (80%)') | |
| plt.tight_layout() | |
| # Convert to base64 | |
| buffer = BytesIO() | |
| plt.savefig(buffer, format='png', dpi=dpi, bbox_inches='tight') | |
| plt.close(fig) | |
| buffer.seek(0) | |
| img_base64 = base64.b64encode(buffer.read()).decode('utf-8') | |
| return f"data:image/png;base64,{img_base64}" | |
| def generate_degradation_curve_chart( | |
| history_data: List[Dict], | |
| asset_name: str, | |
| width: int = 8, | |
| height: int = 4, | |
| dpi: int = 150 | |
| ) -> str: | |
| """Generate capacity degradation curve chart.""" | |
| fig, ax = plt.subplots(figsize=(width, height), dpi=dpi) | |
| # Extract data | |
| cycles = [p['cycle_index'] for p in history_data] | |
| soh_values = [p['current_soh'] for p in history_data] | |
| # Plot actual | |
| ax.plot(cycles, soh_values, 'o-', linewidth=2.5, markersize=8, color='#3b82f6', label='Actual SOH') | |
| # Fit trend line | |
| if len(cycles) > 2: | |
| z = np.polyfit(cycles, soh_values, 2) # Quadratic fit | |
| p = np.poly1d(z) | |
| cycle_range = np.linspace(min(cycles), max(cycles) * 1.5, 100) | |
| ax.plot(cycle_range, p(cycle_range), '--', linewidth=2, alpha=0.7, color='#ef4444', label='Projected Trend') | |
| # Styling | |
| ax.set_xlabel('Cycle Index', fontsize=11, fontweight='bold') | |
| ax.set_ylabel('State of Health (SOH)', fontsize=11, fontweight='bold') | |
| ax.set_title(f'Degradation Curve - {asset_name}', fontsize=13, fontweight='bold', pad=15) | |
| ax.grid(True, alpha=0.3, linestyle='--') | |
| ax.legend(loc='best', frameon=True, shadow=True) | |
| ax.set_ylim([0, 1.05]) | |
| # Add EOL threshold | |
| ax.axhline(y=0.8, color='#f59e0b', linestyle='--', linewidth=1.5, alpha=0.7, label='EOL (80%)') | |
| plt.tight_layout() | |
| buffer = BytesIO() | |
| plt.savefig(buffer, format='png', dpi=dpi, bbox_inches='tight') | |
| plt.close(fig) | |
| buffer.seek(0) | |
| return f"data:image/png;base64,{base64.b64encode(buffer.read()).decode('utf-8')}" | |
| def generate_voltage_temperature_chart( | |
| voltage_data: List[float], | |
| temperature_data: List[float], | |
| asset_name: str, | |
| width: int = 8, | |
| height: int = 4, | |
| dpi: int = 150 | |
| ) -> str: | |
| """Generate voltage and temperature dual-axis chart.""" | |
| fig, ax1 = plt.subplots(figsize=(width, height), dpi=dpi) | |
| time_points = list(range(len(voltage_data))) | |
| # Voltage on left axis | |
| color = '#3b82f6' | |
| ax1.set_xlabel('Time Point', fontsize=11, fontweight='bold') | |
| ax1.set_ylabel('Voltage (V)', fontsize=11, fontweight='bold', color=color) | |
| ax1.plot(time_points, voltage_data, 'o-', color=color, linewidth=2, markersize=6, label='Voltage') | |
| ax1.tick_params(axis='y', labelcolor=color) | |
| ax1.grid(True, alpha=0.3, linestyle='--') | |
| # Temperature on right axis | |
| ax2 = ax1.twinx() | |
| color = '#ef4444' | |
| ax2.set_ylabel('Temperature (°C)', fontsize=11, fontweight='bold', color=color) | |
| ax2.plot(time_points, temperature_data, 's-', color=color, linewidth=2, markersize=6, label='Temperature') | |
| ax2.tick_params(axis='y', labelcolor=color) | |
| # Title | |
| ax1.set_title(f'Voltage & Temperature Trends - {asset_name}', fontsize=13, fontweight='bold', pad=15) | |
| # Combined legend | |
| lines1, labels1 = ax1.get_legend_handles_labels() | |
| lines2, labels2 = ax2.get_legend_handles_labels() | |
| ax1.legend(lines1 + lines2, labels1 + labels2, loc='best', frameon=True, shadow=True) | |
| plt.tight_layout() | |
| buffer = BytesIO() | |
| plt.savefig(buffer, format='png', dpi=dpi, bbox_inches='tight') | |
| plt.close(fig) | |
| buffer.seek(0) | |
| return f"data:image/png;base64,{base64.b64encode(buffer.read()).decode('utf-8')}" | |
| def generate_feature_importance_chart( | |
| features: List[Dict], | |
| asset_name: str, | |
| width: int = 8, | |
| height: int = 5, | |
| dpi: int = 150 | |
| ) -> str: | |
| """Generate feature importance bar chart.""" | |
| fig, ax = plt.subplots(figsize=(width, height), dpi=dpi) | |
| # Extract top features | |
| names = [f['name'][:30] for f in features[:10]] # Truncate long names | |
| importances = [f['importance'] for f in features[:10]] | |
| # Create horizontal bar chart | |
| y_pos = np.arange(len(names)) | |
| colors = plt.cm.viridis(np.linspace(0.3, 0.9, len(names))) | |
| bars = ax.barh(y_pos, importances, color=colors, edgecolor='black', linewidth=0.5) | |
| # Styling | |
| ax.set_yticks(y_pos) | |
| ax.set_yticklabels(names, fontsize=9) | |
| ax.invert_yaxis() # Top features at top | |
| ax.set_xlabel('Feature Importance', fontsize=11, fontweight='bold') | |
| ax.set_title(f'Top Predictive Features - {asset_name}', fontsize=13, fontweight='bold', pad=15) | |
| ax.grid(True, alpha=0.3, axis='x', linestyle='--') | |
| # Add value labels on bars | |
| for i, (bar, val) in enumerate(zip(bars, importances)): | |
| ax.text(val, bar.get_y() + bar.get_height()/2, f' {val:.3f}', | |
| va='center', fontsize=8, fontweight='bold') | |
| plt.tight_layout() | |
| buffer = BytesIO() | |
| plt.savefig(buffer, format='png', dpi=dpi, bbox_inches='tight') | |
| plt.close(fig) | |
| buffer.seek(0) | |
| return f"data:image/png;base64,{base64.b64encode(buffer.read()).decode('utf-8')}" | |
| def generate_all_charts_for_asset( | |
| asset: Dict, | |
| history: List[Dict] | |
| ) -> Dict[str, str]: | |
| """ | |
| Generate all charts for an asset. | |
| Parameters: | |
| ----------- | |
| asset : dict | |
| Asset data | |
| history : list | |
| Historical predictions | |
| Returns: | |
| -------- | |
| dict: {chart_name: base64_image} | |
| """ | |
| charts = {} | |
| # 1. Health timeline | |
| if history and len(history) > 0: | |
| charts['health_timeline'] = generate_health_timeline_chart( | |
| history, | |
| asset.get('projectName', asset.get('project_name', 'Unknown')) | |
| ) | |
| # 2. Degradation curve | |
| if history and len(history) > 0: | |
| charts['degradation_curve'] = generate_degradation_curve_chart( | |
| history, | |
| asset.get('projectName', asset.get('project_name', 'Unknown')) | |
| ) | |
| # 3. Voltage/Temperature (if available) | |
| if 'voltageData' in asset or 'voltage' in asset: | |
| voltage_data = asset.get('voltageData', [asset.get('voltage', 700)] * 8) | |
| temp_data = asset.get('temperatureData', [asset.get('temperature', 25)] * 8) | |
| if len(voltage_data) > 0 and len(temp_data) > 0: | |
| charts['voltage_temperature'] = generate_voltage_temperature_chart( | |
| voltage_data, | |
| temp_data, | |
| asset.get('projectName', asset.get('project_name', 'Unknown')) | |
| ) | |
| # 4. Feature importance (if available) | |
| if history and len(history) > 0 and 'shap_top_features' in history[-1]: | |
| features_str = history[-1]['shap_top_features'] | |
| if features_str: | |
| import json | |
| features = json.loads(features_str) if isinstance(features_str, str) else features_str | |
| if features and len(features) > 0: | |
| charts['feature_importance'] = generate_feature_importance_chart( | |
| features, | |
| asset.get('projectName', asset.get('project_name', 'Unknown')) | |
| ) | |
| return charts | |
| if __name__ == "__main__": | |
| # Test chart generation | |
| print("Testing PDF chart generation...") | |
| # Mock data | |
| history = [ | |
| {'prediction_date': '2025-10-01', 'health_score': 95, 'current_soh': 0.95, 'soh_lower_95': 0.93, 'soh_upper_95': 0.97, 'cycle_index': 40}, | |
| {'prediction_date': '2025-11-01', 'health_score': 90, 'current_soh': 0.90, 'soh_lower_95': 0.88, 'soh_upper_95': 0.92, 'cycle_index': 80}, | |
| {'prediction_date': '2025-12-01', 'health_score': 85, 'current_soh': 0.85, 'soh_lower_95': 0.83, 'soh_upper_95': 0.87, 'cycle_index': 120}, | |
| {'prediction_date': '2026-01-01', 'health_score': 80, 'current_soh': 0.80, 'soh_lower_95': 0.78, 'soh_upper_95': 0.82, 'cycle_index': 160}, | |
| ] | |
| asset = {'projectName': 'Test Battery', 'voltage': 700, 'temperature': 25} | |
| # Generate charts | |
| chart1 = generate_health_timeline_chart(history, "Test Battery") | |
| print(f"✓ Health timeline: {len(chart1)} chars") | |
| chart2 = generate_degradation_curve_chart(history, "Test Battery") | |
| print(f"✓ Degradation curve: {len(chart2)} chars") | |
| chart3 = generate_voltage_temperature_chart([700, 705, 710, 708, 710, 712, 710, 710], | |
| [25, 26, 27, 27, 26, 28, 27, 26], | |
| "Test Battery") | |
| print(f"✓ Voltage/Temperature: {len(chart3)} chars") | |
| print("\n✅ Chart generation test complete!") | |
| print(" All charts ready for PDF embedding") | |