Download tests/scripts/generate_cross_platform_dashboard.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 23.2 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/generate_cross_platform_dashboard.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/scripts/generate_cross_platform_dashboard.py
-
curl -L -o generate_cross_platform_dashboard.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/generate_cross_platform_dashboard.py
23.2 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Cross-Platform Coverage Dashboard Generator | |
| Purpose: Generate HTML dashboard with matplotlib charts visualizing 30-day coverage trends | |
| for each platform (backend, frontend, mobile, desktop) and overall coverage. Creates | |
| self-contained HTML with embedded base64 images. | |
| Usage: | |
| python generate_cross_platform_dashboard.py [options] | |
| Options: | |
| --trending-file PATH Path to cross_platform_trend.json (default: relative path) | |
| --output PATH Output HTML file path (default: coverage_trend_30d.html) | |
| --days INT Number of days to include in chart (default: 30) | |
| --width INT Chart width in pixels (default: 1200) | |
| --height INT Chart height in pixels (default: 600) | |
| Example: | |
| python generate_cross_platform_dashboard.py --days 30 --output coverage_dashboard.html | |
| """ | |
| import argparse | |
| import base64 | |
| import io | |
| import json | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| # Matplotlib imports | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-interactive backend for CI/CD | |
| import matplotlib.dates as mdates | |
| import matplotlib.pyplot as plt | |
| # Configure logging | |
| from logging import basicConfig, getLogger, INFO | |
| basicConfig( | |
| level=INFO, | |
| format='%(levelname)s: %(message)s' | |
| ) | |
| logger = getLogger(__name__) | |
| # Default paths | |
| TREND_FILE = Path("tests/coverage_reports/metrics/cross_platform_trend.json") | |
| OUTPUT_DIR = Path("tests/coverage_reports/dashboards") | |
| # Chart colors for platforms | |
| CHART_COLORS = { | |
| "backend": "#3B82F6", # Blue | |
| "frontend": "#10B981", # Green | |
| "mobile": "#F59E0B", # Orange | |
| "desktop": "#8B5CF6", # Purple | |
| "overall": "#111827" # Dark gray | |
| } | |
| # Default configuration | |
| DEFAULT_DAYS = 30 | |
| DEFAULT_WIDTH = 1200 | |
| DEFAULT_HEIGHT = 600 | |
| DPI = 100 | |
| def load_trending_data(trend_file: Path) -> Dict: | |
| """ | |
| Load trending data from cross_platform_trend.json. | |
| Reuses load_trending_data() from update_cross_platform_trending.py. | |
| Args: | |
| trend_file: Path to cross_platform_trend.json | |
| Returns: | |
| Dict with history list and latest entry | |
| """ | |
| # Import from update_cross_platform_trending.py | |
| try: | |
| # Add scripts directory to path | |
| script_dir = Path(__file__).parent | |
| sys.path.insert(0, str(script_dir)) | |
| from update_cross_platform_trending import load_trending_data as load_trend | |
| return load_trend(trend_file) | |
| except ImportError: | |
| # Fallback to simple implementation | |
| default_structure = { | |
| "history": [], | |
| "latest": {}, | |
| "platform_trends": {}, | |
| "computed_weights": { | |
| "backend": 0.35, | |
| "frontend": 0.40, | |
| "mobile": 0.15, | |
| "desktop": 0.10 | |
| } | |
| } | |
| if not trend_file.exists(): | |
| logger.warning(f"Trend file not found: {trend_file}, using empty structure") | |
| return default_structure | |
| try: | |
| with open(trend_file, 'r') as f: | |
| trending_data = json.load(f) | |
| # Validate structure | |
| required_keys = ["history", "latest", "platform_trends"] | |
| for key in required_keys: | |
| if key not in trending_data: | |
| logger.warning(f"Missing key '{key}', using default") | |
| trending_data[key] = default_structure[key] | |
| return trending_data | |
| except (json.JSONDecodeError, IOError) as e: | |
| logger.error(f"Error loading trending data: {e}") | |
| return default_structure | |
| def prepare_chart_data(trending_data: Dict, days: int = 30) -> Dict: | |
| """ | |
| Prepare chart data by filtering history to last N days. | |
| Args: | |
| trending_data: Trending data dict with history | |
| days: Number of days to include (default: 30) | |
| Returns: | |
| Dict with timestamps, platforms dict, and overall list | |
| """ | |
| history = trending_data.get("history", []) | |
| if not history: | |
| logger.warning("No history data available") | |
| return { | |
| "timestamps": [], | |
| "platforms": {}, | |
| "overall": [] | |
| } | |
| # Filter to last N entries (days parameter interpreted as entries for simplicity) | |
| filtered_history = history[-days:] if len(history) > days else history | |
| # Extract timestamps | |
| timestamps = [] | |
| for entry in filtered_history: | |
| try: | |
| # Parse timestamp | |
| ts = entry.get("timestamp", "") | |
| ts_clean = ts.replace("Z", "").replace("+00:00", "") | |
| dt = datetime.fromisoformat(ts_clean) | |
| timestamps.append(dt) | |
| except (ValueError, KeyError): | |
| # Use current time if parsing fails | |
| timestamps.append(datetime.now()) | |
| # Extract platform coverage values | |
| platforms_data = { | |
| "backend": [], | |
| "frontend": [], | |
| "mobile": [], | |
| "desktop": [] | |
| } | |
| overall_data = [] | |
| for entry in filtered_history: | |
| platforms = entry.get("platforms", {}) | |
| for platform in platforms_data.keys(): | |
| platforms_data[platform].append(platforms.get(platform, 0.0)) | |
| overall_data.append(entry.get("overall_coverage", 0.0)) | |
| return { | |
| "timestamps": timestamps, | |
| "platforms": platforms_data, | |
| "overall": overall_data | |
| } | |
| def create_line_chart( | |
| data: Dict, | |
| title: str = "Coverage Trend (30 Days)", | |
| width: int = DEFAULT_WIDTH, | |
| height: int = DEFAULT_HEIGHT | |
| ) -> bytes: | |
| """ | |
| Create line chart with all platforms and overall coverage. | |
| Args: | |
| data: Chart data dict with timestamps, platforms, overall | |
| title: Chart title | |
| width: Chart width in pixels | |
| height: Chart height in pixels | |
| Returns: | |
| Base64-encoded PNG image bytes | |
| """ | |
| timestamps = data.get("timestamps", []) | |
| platforms = data.get("platforms", {}) | |
| overall = data.get("overall", []) | |
| if not timestamps: | |
| logger.warning("No data available for chart") | |
| return b"" | |
| # Create figure | |
| figsize = (width / DPI, height / DPI) | |
| fig, ax = plt.subplots(figsize=figsize, dpi=DPI) | |
| # Plot overall coverage (thick, dark line) | |
| if overall: | |
| ax.plot(timestamps, overall, color=CHART_COLORS["overall"], | |
| linewidth=3, label="Overall", alpha=0.8) | |
| # Plot each platform (thinner, colored lines) | |
| for platform_name, coverage_values in platforms.items(): | |
| if coverage_values: | |
| ax.plot(timestamps, coverage_values, | |
| color=CHART_COLORS.get(platform_name, "#000000"), | |
| linewidth=1.5, label=platform_name.capitalize(), | |
| alpha=0.7) | |
| # Add legend | |
| ax.legend(loc='best', framealpha=0.9) | |
| # Add grid | |
| ax.grid(True, linestyle='--', alpha=0.7) | |
| # Format x-axis with dates | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) | |
| ax.xaxis.set_major_locator(mdates.DayLocator(interval=max(1, len(timestamps) // 10))) | |
| plt.xticks(rotation=45, ha='right') | |
| # Set y-axis range and format | |
| ax.set_ylim(0, 100) | |
| ax.set_ylabel('Coverage %', fontsize=11) | |
| ax.set_xlabel('Date', fontsize=11) | |
| # Set title | |
| ax.set_title(title, fontsize=14, fontweight='bold', pad=20) | |
| # Use tight layout to prevent clipping | |
| plt.tight_layout() | |
| # Save to BytesIO buffer | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=DPI, bbox_inches='tight') | |
| buf.seek(0) | |
| # Get base64 encoded bytes | |
| image_bytes = buf.getvalue() | |
| base64_bytes = base64.b64encode(image_bytes) | |
| # Close figure to prevent memory leak | |
| plt.close(fig) | |
| return base64_bytes | |
| def create_platform_charts(data: Dict) -> Dict[str, bytes]: | |
| """ | |
| Create individual platform charts in 2x2 grid. | |
| Args: | |
| data: Chart data dict with timestamps, platforms, overall | |
| Returns: | |
| Dict mapping platform name to base64 image bytes | |
| """ | |
| timestamps = data.get("timestamps", []) | |
| platforms = data.get("platforms", {}) | |
| if not timestamps: | |
| logger.warning("No data available for platform charts") | |
| return {} | |
| # Create 2x2 subplot figure | |
| fig, axes = plt.subplots(2, 2, figsize=(14, 10), dpi=DPI) | |
| axes = axes.flatten() | |
| for idx, (platform_name, coverage_values) in enumerate(platforms.items()): | |
| if idx >= len(axes): | |
| break | |
| ax = axes[idx] | |
| if not coverage_values: | |
| continue | |
| # Plot platform coverage | |
| ax.plot(timestamps, coverage_values, | |
| color=CHART_COLORS.get(platform_name, "#000000"), | |
| linewidth=2, label=platform_name.capitalize(), | |
| marker='o', markersize=4, alpha=0.8) | |
| # Add threshold line (70% as example) | |
| threshold = 70.0 | |
| ax.axhline(y=threshold, color='red', linestyle='--', | |
| linewidth=1, alpha=0.5, label=f'Threshold ({threshold}%)') | |
| # Color code: above threshold green, below red | |
| if coverage_values and coverage_values[-1] >= threshold: | |
| title_color = 'green' | |
| else: | |
| title_color = 'red' | |
| # Format subplot | |
| ax.set_title(f"{platform_name.capitalize()} Coverage", | |
| fontsize=12, fontweight='bold', color=title_color) | |
| ax.set_ylim(0, 100) | |
| ax.set_ylabel('Coverage %', fontsize=10) | |
| ax.grid(True, linestyle='--', alpha=0.5) | |
| ax.legend(loc='best', fontsize=9) | |
| # Format x-axis | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) | |
| ax.xaxis.set_major_locator(mdates.DayLocator(interval=max(1, len(timestamps) // 5))) | |
| plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right', fontsize=8) | |
| # Adjust layout | |
| plt.tight_layout() | |
| # Save to BytesIO buffer | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=DPI, bbox_inches='tight') | |
| buf.seek(0) | |
| # Get base64 encoded bytes | |
| image_bytes = buf.getvalue() | |
| base64_bytes = base64.b64encode(image_bytes) | |
| # Close figure to prevent memory leak | |
| plt.close(fig) | |
| return {"platforms": base64_bytes} | |
| def calculate_statistics(data: Dict) -> Dict: | |
| """ | |
| Calculate summary statistics for each platform. | |
| Args: | |
| data: Chart data dict with timestamps, platforms, overall | |
| Returns: | |
| Dict with statistics for each platform and overall | |
| """ | |
| platforms = data.get("platforms", {}) | |
| overall = data.get("overall", []) | |
| stats = {} | |
| # Calculate overall statistics | |
| if overall: | |
| stats["overall"] = { | |
| "current": round(overall[-1], 2) if overall else 0.0, | |
| "min": round(min(overall), 2) if overall else 0.0, | |
| "max": round(max(overall), 2) if overall else 0.0, | |
| "avg": round(sum(overall) / len(overall), 2) if overall else 0.0, | |
| "trend": _calculate_trend(overall) | |
| } | |
| # Calculate platform statistics | |
| for platform_name, coverage_values in platforms.items(): | |
| if coverage_values: | |
| stats[platform_name] = { | |
| "current": round(coverage_values[-1], 2), | |
| "min": round(min(coverage_values), 2), | |
| "max": round(max(coverage_values), 2), | |
| "avg": round(sum(coverage_values) / len(coverage_values), 2), | |
| "trend": _calculate_trend(coverage_values) | |
| } | |
| return stats | |
| def _calculate_trend(values: List[float]) -> str: | |
| """ | |
| Calculate trend direction (up/down/stable). | |
| Args: | |
| values: List of coverage values | |
| Returns: | |
| "up", "down", or "stable" | |
| """ | |
| if len(values) < 2: | |
| return "stable" | |
| first = values[0] | |
| last = values[-1] | |
| delta = last - first | |
| if delta > 1.0: | |
| return "up" | |
| elif delta < -1.0: | |
| return "down" | |
| else: | |
| return "stable" | |
| def _get_trend_indicator(trend: str) -> str: | |
| """Get trend indicator symbol.""" | |
| if trend == "up": | |
| return "↑" | |
| elif trend == "down": | |
| return "↓" | |
| else: | |
| return "→" | |
| def generate_html_template( | |
| chart_base64: str, | |
| platform_charts: Dict[str, str], | |
| data: Dict, | |
| statistics: Dict | |
| ) -> str: | |
| """ | |
| Generate self-contained HTML dashboard. | |
| Args: | |
| chart_base64: Base64-encoded main chart image | |
| platform_charts: Dict with platform chart base64 images | |
| data: Chart data dict | |
| statistics: Statistics dict | |
| Returns: | |
| Complete HTML string | |
| """ | |
| # Convert base64 bytes to string | |
| main_chart_src = f"data:image/png;base64,{chart_base64.decode('utf-8')}" if isinstance(chart_base64, bytes) else chart_base64 | |
| platforms_chart_src = "" | |
| if "platforms" in platform_charts: | |
| platforms_chart_bytes = platform_charts["platforms"] | |
| if isinstance(platforms_chart_bytes, bytes): | |
| platforms_chart_src = f"data:image/png;base64,{platforms_chart_bytes.decode('utf-8')}" | |
| else: | |
| platforms_chart_src = platforms_chart_bytes | |
| # Get generation time | |
| generation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") | |
| # Generate statistics table rows | |
| stats_rows = "" | |
| for name, stats_data in statistics.items(): | |
| trend_indicator = _get_trend_indicator(stats_data.get("trend", "stable")) | |
| trend_color = "green" if stats_data.get("trend") == "up" else "red" if stats_data.get("trend") == "down" else "gray" | |
| stats_rows += f""" | |
| <tr> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb; font-weight: 600;">{name.capitalize()}</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb;">{stats_data.get('current', 0):.2f}%</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb;">{stats_data.get('min', 0):.2f}%</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb;">{stats_data.get('max', 0):.2f}%</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb;">{stats_data.get('avg', 0):.2f}%</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #e5e7eb; color: {trend_color}; font-weight: bold;">{trend_indicator}</td> | |
| </tr> | |
| """ | |
| html_template = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Coverage Trend Dashboard (30 Days)</title> | |
| <style> | |
| * {{ | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| }} | |
| body {{ | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| color: #1f2937; | |
| background: #f9fafb; | |
| padding: 20px; | |
| }} | |
| .container {{ | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| overflow: hidden; | |
| }} | |
| .header {{ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| }} | |
| .header h1 {{ | |
| font-size: 32px; | |
| margin-bottom: 10px; | |
| font-weight: 700; | |
| }} | |
| .header p {{ | |
| font-size: 14px; | |
| opacity: 0.9; | |
| }} | |
| .content {{ | |
| padding: 30px; | |
| }} | |
| .chart-section {{ | |
| margin-bottom: 40px; | |
| }} | |
| .chart-section h2 {{ | |
| font-size: 24px; | |
| margin-bottom: 20px; | |
| color: #111827; | |
| border-bottom: 2px solid #e5e7eb; | |
| padding-bottom: 10px; | |
| }} | |
| .chart-container {{ | |
| text-align: center; | |
| margin: 20px 0; | |
| }} | |
| .chart-container img {{ | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| }} | |
| .chart-container img:hover {{ | |
| transform: scale(1.02); | |
| }} | |
| .stats-table {{ | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 20px 0; | |
| background: white; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| }} | |
| .stats-table th {{ | |
| background: #374151; | |
| color: white; | |
| padding: 12px; | |
| text-align: left; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| }} | |
| .stats-table td {{ | |
| padding: 12px; | |
| border-bottom: 1px solid #e5e7eb; | |
| }} | |
| .stats-table tr:hover {{ | |
| background: #f9fafb; | |
| }} | |
| .legend {{ | |
| background: #f3f4f6; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin: 20px 0; | |
| font-size: 14px; | |
| }} | |
| .legend h3 {{ | |
| margin-bottom: 10px; | |
| color: #374151; | |
| }} | |
| .legend ul {{ | |
| list-style: none; | |
| padding-left: 0; | |
| }} | |
| .legend li {{ | |
| margin: 5px 0; | |
| padding-left: 20px; | |
| position: relative; | |
| }} | |
| .legend li::before {{ | |
| content: "•"; | |
| position: absolute; | |
| left: 0; | |
| color: #6b7280; | |
| font-weight: bold; | |
| }} | |
| .footer {{ | |
| background: #f3f4f6; | |
| padding: 20px; | |
| text-align: center; | |
| font-size: 12px; | |
| color: #6b7280; | |
| border-top: 1px solid #e5e7eb; | |
| }} | |
| @media (max-width: 768px) {{ | |
| .content {{ | |
| padding: 15px; | |
| }} | |
| .header h1 {{ | |
| font-size: 24px; | |
| }} | |
| .stats-table {{ | |
| font-size: 12px; | |
| }} | |
| .stats-table th, .stats-table td {{ | |
| padding: 8px; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>Cross-Platform Coverage Trend Dashboard</h1> | |
| <p>Last 30 days of coverage metrics across all platforms</p> | |
| </div> | |
| <div class="content"> | |
| <div class="chart-section"> | |
| <h2>Overall Coverage Trend</h2> | |
| <div class="chart-container"> | |
| <img src="{main_chart_src}" alt="Overall Coverage Trend Chart" title="Click to zoom"> | |
| </div> | |
| </div> | |
| <div class="chart-section"> | |
| <h2>Platform-Specific Trends</h2> | |
| <div class="chart-container"> | |
| <img src="{platforms_chart_src}" alt="Platform-Specific Coverage Charts" title="Click to zoom"> | |
| </div> | |
| </div> | |
| <div class="chart-section"> | |
| <h2>Summary Statistics</h2> | |
| <table class="stats-table"> | |
| <thead> | |
| <tr> | |
| <th>Platform</th> | |
| <th>Current (%)</th> | |
| <th>Min (%)</th> | |
| <th>Max (%)</th> | |
| <th>Average (%)</th> | |
| <th>Trend</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {stats_rows} | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="legend"> | |
| <h3>Trend Indicators</h3> | |
| <ul> | |
| <li>↑ Improved (>1% increase from start of period)</li> | |
| <li>↓ Regressed (>1% decrease from start of period)</li> | |
| <li>→ Stable (within ±1% from start of period)</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <p>Generated on {generation_time}</p> | |
| <p>Coverage Dashboard Generator | Atom Quality Infrastructure</p> | |
| </div> | |
| </div> | |
| </body> | |
| </html>""" | |
| return html_template | |
| def main(): | |
| """Main execution function.""" | |
| parser = argparse.ArgumentParser( | |
| description="Generate HTML cross-platform coverage dashboard with matplotlib charts" | |
| ) | |
| parser.add_argument( | |
| "--trending-file", | |
| type=Path, | |
| default=TREND_FILE, | |
| help="Path to cross_platform_trend.json" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| type=Path, | |
| default=OUTPUT_DIR / "coverage_trend_30d.html", | |
| help="Output HTML file path" | |
| ) | |
| parser.add_argument( | |
| "--days", | |
| type=int, | |
| default=DEFAULT_DAYS, | |
| help=f"Number of days to include in chart (default: {DEFAULT_DAYS})" | |
| ) | |
| parser.add_argument( | |
| "--width", | |
| type=int, | |
| default=DEFAULT_WIDTH, | |
| help=f"Chart width in pixels (default: {DEFAULT_WIDTH})" | |
| ) | |
| parser.add_argument( | |
| "--height", | |
| type=int, | |
| default=DEFAULT_HEIGHT, | |
| help=f"Chart height in pixels (default: {DEFAULT_HEIGHT})" | |
| ) | |
| args = parser.parse_args() | |
| # Load trend data | |
| logger.info(f"Loading trend data from: {args.trending_file}") | |
| trending_data = load_trending_data(args.trending_file) | |
| # Prepare chart data | |
| logger.info(f"Preparing chart data (last {args.days} entries)") | |
| chart_data = prepare_chart_data(trending_data, days=args.days) | |
| if not chart_data.get("timestamps"): | |
| logger.error("No data available for chart generation") | |
| sys.exit(1) | |
| # Generate main chart | |
| logger.info("Generating main coverage trend chart") | |
| main_chart_base64 = create_line_chart( | |
| chart_data, | |
| title=f"Coverage Trend (Last {len(chart_data['timestamps'])} Entries)", | |
| width=args.width, | |
| height=args.height | |
| ) | |
| # Generate platform charts | |
| logger.info("Generating platform-specific charts") | |
| platform_charts = create_platform_charts(chart_data) | |
| # Calculate statistics | |
| logger.info("Calculating summary statistics") | |
| statistics = calculate_statistics(chart_data) | |
| # Generate HTML | |
| logger.info("Generating HTML dashboard") | |
| html_content = generate_html_template( | |
| main_chart_base64, | |
| platform_charts, | |
| chart_data, | |
| statistics | |
| ) | |
| # Write output file | |
| output_path = args.output | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(output_path, 'w') as f: | |
| f.write(html_content) | |
| logger.info(f"Dashboard generated: {output_path}") | |
| logger.info(f" File size: {output_path.stat().st_size / 1024:.2f} KB") | |
| logger.info(f" Data points: {len(chart_data['timestamps'])}") | |
| logger.info(f" Platforms: {', '.join(chart_data['platforms'].keys())}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |