#!/usr/bin/env python3 """ Enhanced Devour Scorecard Generator - Extended version with more visual elements. Enhanced data visualization with additional metrics and improved layout. """ from __future__ import annotations import json import logging import math import os import sys from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Tuple, Any, Optional try: from PIL import Image, ImageDraw, ImageFont except ImportError: print("Error: PIL/Pillow required. Install with: pip install Pillow") sys.exit(1) logger = logging.getLogger(__name__) # Enhanced visual constants SCALE = 2 BG = (248, 248, 246) FRAME = (222, 222, 220) BORDER = (200, 200, 198) ACCENT = (88, 166, 255) TEXT = (40, 44, 52) DIM = (140, 140, 140) BG_SCORE = (255, 255, 255) BG_TABLE = (255, 255, 255) BG_ROW_ALT = (250, 250, 248) BG_GRADIENT_START = (240, 240, 238) BG_GRADIENT_END = (248, 248, 246) # Extended color palette COLORS = { 'excellent': (68, 120, 68), # deep sage 'good': (120, 140, 72), # olive green 'moderate': (145, 155, 80), # yellow-green 'poor': (255, 193, 7), # orange 'critical': (220, 38, 127), # red 'info': (88, 166, 255), # blue accent 'warning': (255, 152, 0), # orange accent } @dataclass class EnhancedScorecardData: """Enhanced data structure for comprehensive scorecard.""" project_name: str version: str main_score: float strict_score: float dimensions: List[Tuple[str, Dict[str, Any]]] trends: Dict[str, List[float]] recommendations: List[str] metadata: Dict[str, Any] def draw_mini_sparkline( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, height: int, values: List[float], color: Tuple[int, int, int] ) -> None: """Draw a mini sparkline chart.""" if len(values) < 2: return # Draw background draw.rectangle([x, y, x + width, y + height], fill=BG_TABLE, outline=BORDER) # Calculate points padding = 3 chart_width = width - 2 * padding chart_height = height - 2 * padding step_x = chart_width / (len(values) - 1) points = [] for i, value in enumerate(values): px = x + padding + i * step_x py = y + padding + chart_height - (value / 100) * chart_height points.append((px, py)) # Draw line if len(points) > 1: draw.line(points, fill=color, width=2) # Draw points for px, py in points: draw.ellipse([px - 2, py - 2, px + 2, py + 2], fill=color, outline=TEXT) def draw_progress_ring( draw: ImageDraw.ImageDraw, cx: int, cy: int, outer_radius: int, progress: float, color: Tuple[int, int, int], label: str = "" ) -> None: """Draw a circular progress ring.""" # Background ring draw.ellipse([cx - outer_radius, cy - outer_radius, cx + outer_radius, cy + outer_radius], fill=BG_TABLE, outline=BORDER, width=2) # Progress arc if progress > 0: inner_radius = outer_radius - 8 start_angle = 270 end_angle = start_angle - (progress / 100) * 360 # Draw multiple arcs for thickness effect for i in range(3): radius_offset = i * 2 draw.arc([cx - outer_radius + radius_offset, cy - outer_radius + radius_offset, cx + outer_radius + radius_offset, cy + outer_radius + radius_offset], start=start_angle, end=end_angle, fill=color, width=6 - i * 2) # Center text if label: font = load_font(10, bold=True) bbox = draw.textbbox((0, 0), label, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] draw.text((cx - text_width // 2, cy - text_height // 2 + bbox[1]), label, fill=TEXT, font=font) def draw_trend_indicator( draw: ImageDraw.ImageDraw, x: int, y: int, size: int, trend: str, value: float ) -> None: """Draw a trend indicator with arrow.""" # Background circle draw.ellipse([x, y, x + size, y + size], fill=BG_TABLE, outline=BORDER) # Trend arrow cx, cy = x + size // 2, y + size // 2 arrow_size = size // 4 color = COLORS['good'] if trend == 'up' else COLORS['poor'] if trend == 'down' else COLORS['moderate'] if trend == 'up': # Up arrow points = [ (cx, cy + arrow_size), (cx - arrow_size // 2, cy), (cx + arrow_size // 2, cy) ] elif trend == 'down': # Down arrow points = [ (cx, cy - arrow_size), (cx - arrow_size // 2, cy), (cx + arrow_size // 2, cy) ] else: # Circle for stable draw.ellipse([cx - arrow_size // 2, cy - arrow_size // 2, cx + arrow_size // 2, cy + arrow_size // 2], fill=color, outline=TEXT) return draw.polygon(points, fill=color) # Value text font = load_font(8) text = f"{value:.0f}%" bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] draw.text((cx - text_width // 2, y + size + 5), text, fill=TEXT, font=font) def generate_enhanced_scorecard(data: EnhancedScorecardData, output_path: str | Path) -> Path: """Generate enhanced scorecard with additional visual elements.""" output_path = Path(output_path) # Layout - larger canvas for more elements width = scale(900) height = scale(700) margin = scale(20) # Create image with gradient background img = Image.new("RGB", (width, height), BG) draw = ImageDraw.Draw(img) # Add subtle gradient overlay for i in range(height): alpha = i / height color = tuple( int(BG_GRADIENT_START[j] + alpha * (BG_GRADIENT_END[j] - BG_GRADIENT_START[j])) for j in range(3) ) draw.line([(0, i), (width, i)], fill=color) # Header section with enhanced styling header_height = scale(100) draw_header_section(draw, margin, data, header_height) # Main content area content_y = margin + header_height + scale(20) # Left panel - enhanced score display left_panel_width = scale(280) draw_enhanced_left_panel(draw, margin, content_y, left_panel_width, data) # Right panel - dimensions with trends right_panel_x = margin + left_panel_width + scale(20) right_panel_width = width - right_panel_x - margin draw_enhanced_right_panel(draw, right_panel_x, content_y, right_panel_width, data) # Bottom recommendations recommendations_y = content_y + scale(200) draw_recommendations_section(draw, margin, recommendations_y, width - 2 * margin, data.recommendations) # Footer with metadata footer_y = height - scale(40) draw_footer_section(draw, margin, footer_y, width - 2 * margin, data.metadata) # Save image output_path.parent.mkdir(parents=True, exist_ok=True) img.save(str(output_path), "PNG", optimize=True) return output_path def draw_header_section( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, data: EnhancedScorecardData ) -> None: """Draw enhanced header section.""" # Project title font_title = load_font(18, bold=True) title = f"{data.project_name} Code Health" title_bbox = draw.textbbox((0, 0), title, font=font_title) title_width = title_bbox[2] - title_bbox[0] # Background panel for title panel_height = scale(40) draw.rectangle([x, y, x + width, y + panel_height], fill=BG_SCORE, outline=ACCENT, width=2) draw.text((x + (width - title_width) // 2, y + scale(12)), title, fill=TEXT, font=font_title) # Version and timestamp font_info = load_font(10) info_text = f"v{data.version} - Generated {data.metadata.get('timestamp', '')}" draw.text((x, y + panel_height + scale(5), info_text, fill=DIM, font=font_info) def draw_enhanced_left_panel( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, data: EnhancedScorecardData ) -> None: """Draw enhanced left panel with multiple visual elements.""" # Main score with large display score_y = y + scale(20) score_size = scale(80) # Circular progress indicator draw_progress_ring(draw, x + width // 2 - score_size // 2, score_y, score_size // 2, progress=data.main_score, color=score_to_color(data.main_score)) # Score text font_score = load_font(36, bold=True) score_text = f"{int(data.main_score)}" score_bbox = draw.textbbox((0, 0), score_text, font=font_score) score_width = score_bbox[2] - score_bbox[0] score_height = score_bbox[3] - score_bbox[1] score_bg_y = score_y + score_size + scale(10) score_bg_height = scale(50) # Background for score text draw.rectangle([x, score_bg_y, x + width, score_bg_y + score_bg_height], fill=BG_SCORE, outline=BORDER, width=1) draw.text((x + (width - score_width) // 2, score_bg_y + score_bg_height // 2 - score_height // 2 + score_bbox[1]), score_text, fill=TEXT, font=font_score) # Strict score strict_y = score_bg_y + score_bg_height + scale(15) font_strict = load_font(14) strict_text = f"Strict: {int(data.strict_score)}" strict_bbox = draw.textbbox((0, 0), strict_text, font=font_strict) strict_width = strict_bbox[2] - strict_bbox[0] draw.text((x + (width - strict_width) // 2, strict_y - strict_bbox[1]), strict_text, fill=score_to_color(data.strict_score, muted=True), font=font_strict) # Grade indicator grade_y = strict_y + scale(30) grade_size = scale(40) grade = "A" if data.main_score >= 90 else "B" if data.main_score >= 70 else "C" if data.main_score >= 50 else "D" if data.main_score >= 30 else "F" draw_progress_ring(draw, x + width // 2 - grade_size // 2, grade_y, grade_size // 2, progress=data.main_score, color=score_to_color(data.main_score)) font_grade = load_font(24, bold=True) grade_bbox = draw.textbbox((0, 0), grade, font=font_grade) grade_width = grade_bbox[2] - grade_bbox[0] draw.text((x + (width - grade_width) // 2, grade_y + grade_size + scale(10) - grade_bbox[1]), grade, fill=TEXT, font=font_grade) def draw_enhanced_right_panel( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, data: EnhancedScorecardData ) -> None: """Draw enhanced right panel with dimensions and trends.""" # Dimensions table table_y = y + scale(20) table_height = scale(120) draw_enhanced_dimensions_table(draw, x, table_y, width, table_height, data.dimensions) # Trends section trends_y = table_y + table_height + scale(20) draw_trends_section(draw, x, trends_y, width, data.trends) def draw_enhanced_dimensions_table( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, height: int, dimensions: List[Tuple[str, Dict[str, Any]]] ) -> None: """Draw enhanced dimensions table with sparklines.""" font_header = load_font(11, bold=True) font_row = load_font(9) # Table header header_y = y draw.text((x, header_y), "CATEGORY", fill=TEXT, font=font_header) draw.text((x + scale(120), header_y), "SCORE", fill=TEXT, font=font_header) draw.text((x + scale(200), header_y), "TREND", fill=TEXT, font=font_header) # Table rows row_y = header_y + scale(20) row_height = scale(25) for i, (name, data) in enumerate(dimensions[:4]): # Limit to 4 rows # Background if i % 2 == 1: draw.rectangle([x, row_y, x + width, row_y + row_height], fill=BG_ROW_ALT) # Category name draw.text((x + scale(5), row_y + scale(5), name, fill=TEXT, font=font_row) # Score bar score = data.get('score', 0) bar_x = x + scale(120) bar_width = scale(60) bar_height = scale(10) draw_metric_bar(draw, bar_x, row_y + scale(5), bar_width, bar_height, score, 100, "", score_to_color(score)) # Trend indicator trend_x = x + scale(200) trend_data = data.get('trend', [50, 55, 60]) # Sample trend data if len(trend_data) >= 2: trend = 'up' if trend_data[-1] > trend_data[-2] else 'down' if trend_data[-1] < trend_data[-2] else 'stable' draw_trend_indicator(draw, trend_x, row_y + scale(5), scale(20), trend, trend_data[-1]) row_y += row_height def draw_trends_section( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, trends: Dict[str, List[float]] ) -> None: """Draw trends section with sparklines.""" font_header = load_font(11, bold=True) # Section title draw.text((x, y), "Recent Trends", fill=TEXT, font=font_header) # Trend charts chart_y = y + scale(30) chart_width = scale(80) chart_height = scale(40) for i, (category, values) in enumerate(list(trends.items())[:2]): # 2 charts chart_x = x + i * (chart_width + scale(20)) # Category label font_label = load_font(9) draw.text((chart_x, chart_y - scale(15), category, fill=TEXT, font=font_label) # Sparkline draw_mini_sparkline(draw, chart_x, chart_y, chart_width, chart_height, values, COLORS['info']) def draw_recommendations_section( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, recommendations: List[str] ) -> None: """Draw recommendations section.""" font_header = load_font(12, bold=True) font_rec = load_font(9) # Section title draw.text((x, y), "🎯 Priority Actions", fill=TEXT, font=font_header) # Recommendations list rec_y = y + scale(25) for i, rec in enumerate(recommendations[:4]): # Limit to 4 recommendations # Numbered bullet bullet = f"{i + 1}." draw.text((x + scale(10), rec_y, bullet, fill=ACCENT, font=font_rec) # Recommendation text (with word wrap) text_x = x + scale(30) max_width = width - scale(40) # Simple word wrap words = rec.split() line = "" for word in words: test_line = line + " " + word if line else word bbox = draw.textbbox((0, 0), test_line, font=font_rec) text_width = bbox[2] - bbox[0] if text_width > max_width or line: draw.text((text_x, rec_y), line, fill=TEXT, font=font_rec) rec_y += scale(12) line = word if line else "" text_x = x + scale(30) else: line = test_line rec_y += scale(15) def draw_footer_section( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, metadata: Dict[str, Any] ) -> None: """Draw footer with metadata.""" font_footer = load_font(8) # Footer line draw.line([(x, y), (x + width, y)], fill=BORDER, width=1) # Metadata text info_text = f"Generated by Devour v{metadata.get('version', '1.0.0')} • {metadata.get('files_analyzed', 0)} files analyzed • {metadata.get('timestamp', '')}" draw.text((x, y + scale(5), info_text, fill=DIM, font=font_footer) def draw_metric_bar( draw: ImageDraw.ImageDraw, x: int, y: int, width: int, height: int, value: float, max_value: float, label: str, color: Tuple[int, int, int] ) -> None: """Draw a horizontal metric bar.""" # Background draw.rectangle([x, y, x + width, y + height], fill=BG_TABLE, outline=BORDER) # Fill bar if max_value > 0: fill_width = int((value / max_value) * width) draw.rectangle([x, y, x + fill_width, y + height], fill=color) def score_to_color(score: float) -> Tuple[int, int, int]: """Convert score to color.""" if score >= 90: return COLORS['excellent'] elif score >= 70: return COLORS['good'] elif score >= 50: return COLORS['moderate'] elif score >= 30: return COLORS['poor'] else: return COLORS['critical'] def load_font(size: int, *, bold: bool = False) -> ImageFont.ImageFont: """Load font with fallback.""" size = size * SCALE candidates = [ "/System/Library/Fonts/SFCompact.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "arial.ttf" ] if bold: candidates = [ "/System/Library/Fonts/SFCompact.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "arialbd.ttf" ] for path in candidates: try: if os.path.exists(path): return ImageFont.truetype(path, size) except OSError: continue return ImageFont.load_default() def load_enhanced_devour_data(json_path: str) -> EnhancedScorecardData: """Load Devour data and convert to enhanced format.""" with open(json_path, 'r') as f: data = json.load(f) findings = data.get('findings', []) # Calculate scores total_score = sum(f.get('score', 0) * int(f.get('severity', 1)) for f in findings) main_score = max(0, 100 - (total_score / 1000 * 100)) strict_score = main_score * 0.8 # Strict is lower # Group by type type_counts = {} type_scores = {} for finding in findings: ftype = finding.get('type', 'unknown') type_counts[ftype] = type_counts.get(ftype, 0) + 1 type_scores[ftype] = type_scores.get(ftype, 0) + finding.get('score', 0) # Create dimensions with trend data dimensions = [] for ftype, count in type_counts.items(): avg_score = 100 - (type_scores[ftype] / max(1, count) / 10 * 100) # Generate sample trend data trend_data = [avg_score - 10, avg_score - 5, avg_score, avg_score + 5, avg_score + 10] dimensions.append(( ftype.replace('_', ' ').title(), { 'score': max(0, min(100, avg_score)), 'strict': max(0, min(100, avg_score * 0.8)), 'count': count, 'trend': trend_data } )) # Sort by score (lowest first) dimensions.sort(key=lambda x: x[1]['score']) # Generate recommendations recommendations = [ "🔴 Address critical T4 issues immediately for maximum impact", "🟡 Focus on reducing high-severity findings (T2-T3)", "🟢 Improve test coverage to reduce technical debt", "📊 Regular code reviews to maintain quality standards" ] # Metadata metadata = { 'version': '1.0.0', 'files_analyzed': len(set(f.get('file', '') for f in findings)), 'timestamp': data.get('timestamp', '') } return EnhancedScorecardData( project_name="Devour", version="1.0.0", main_score=main_score, strict_score=strict_score, dimensions=dimensions[:6], # Limit to 6 dimensions trends={'overall': [main_score - 5, main_score], 'performance': [85, 88, 92, main_score]}, recommendations=recommendations, metadata=metadata ) def main(): """Main entry point.""" if len(sys.argv) != 3: print("Usage: python devour_enhanced.py ") sys.exit(1) json_path = sys.argv[1] output_path = sys.argv[2] if not os.path.exists(json_path): print(f"Error: Input file {json_path} not found") sys.exit(1) try: data = load_enhanced_devour_data(json_path) result_path = generate_enhanced_scorecard(data, output_path) print(f"Enhanced scorecard generated: {result_path}") except Exception as e: print(f"Error generating enhanced scorecard: {e}") sys.exit(1) if __name__ == "__main__": main()