"""Plotting and visualization for paw burn risk assessment.""" import matplotlib import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime from typing import List, Optional, Tuple import numpy as np import warnings import os import shutil from models import WeatherHour, RiskScore # Suppress matplotlib UserWarnings and macOS GUI warnings warnings.filterwarnings('ignore', category=UserWarning, module='matplotlib') warnings.filterwarnings('ignore', message='.*NSSavePanel.*') class RiskPlotter: """Handles plotting and visualization of risk data.""" def __init__(self, figure_size: Tuple[int, int] = (12, 8)): self.figure_size = figure_size self.plots_dir = "plots" self._plots_dir_setup = False # Set up matplotlib style and suppress warnings plt.style.use('default') plt.rcParams['figure.figsize'] = figure_size plt.rcParams['font.size'] = 10 def _setup_plots_directory(self): """Create and clear plots directory.""" if os.path.exists(self.plots_dir): # Clear existing plots shutil.rmtree(self.plots_dir) os.makedirs(self.plots_dir, exist_ok=True) print(f"📁 Plots directory ready: {self.plots_dir}/") def _safe_save_plot(self, filename: str, dpi: int = 300): """Safely save plot to plots directory.""" if filename: # Setup plots directory on first save if not self._plots_dir_setup: self._setup_plots_directory() self._plots_dir_setup = True # Create full path in plots directory save_path = os.path.join(self.plots_dir, filename) # Save with current backend (don't switch backends as it causes blank files) with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) plt.savefig(save_path, dpi=dpi, bbox_inches='tight', facecolor='white') print(f"📊 Plot saved: {save_path}") def plot_risk_timeline(self, risk_scores: List[RiskScore], weather_hours: List[WeatherHour], threshold: float = 6.0, save_path: Optional[str] = None, show: bool = True) -> None: """Plot risk scores over time with recommendation threshold.""" if not risk_scores: print("No risk data to plot") return fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=self.figure_size, sharex=True, gridspec_kw={'height_ratios': [2, 1, 1]}) # Extract data times = [score.datetime for score in risk_scores] total_scores = [score.total_score for score in risk_scores] temperatures = [hour.temperature_f for hour in weather_hours] uv_indices = [hour.uv_index or 0 for hour in weather_hours] # Main risk score plot ax1.plot(times, total_scores, 'b-', linewidth=2, label='Risk Score') ax1.axhline(y=threshold, color='red', linestyle='--', alpha=0.7, label=f'Shoe Threshold ({threshold})') # Highlight high-risk periods high_risk_times = [] high_risk_scores = [] for score in risk_scores: if score.recommend_shoes: high_risk_times.append(score.datetime) high_risk_scores.append(score.total_score) if high_risk_times: ax1.scatter(high_risk_times, high_risk_scores, color='red', s=50, alpha=0.7, zorder=5, label='Shoes Recommended') ax1.set_ylabel('Risk Score') ax1.set_title('Paw Burn Risk Assessment Timeline') ax1.legend() ax1.grid(True, alpha=0.3) ax1.set_ylim(0, 10) # Temperature subplot ax2.plot(times, temperatures, 'orange', linewidth=2, label='Temperature (°F)') ax2.axhline(y=80, color='yellow', linestyle=':', alpha=0.7, label='80°F') ax2.axhline(y=90, color='orange', linestyle=':', alpha=0.7, label='90°F') ax2.axhline(y=100, color='red', linestyle=':', alpha=0.7, label='100°F') ax2.set_ylabel('Temperature (°F)') ax2.legend(loc='upper right') ax2.grid(True, alpha=0.3) # UV Index subplot ax3.plot(times, uv_indices, 'purple', linewidth=2, label='UV Index') ax3.axhline(y=6, color='yellow', linestyle=':', alpha=0.7, label='UV 6') ax3.axhline(y=8, color='orange', linestyle=':', alpha=0.7, label='UV 8') ax3.axhline(y=10, color='red', linestyle=':', alpha=0.7, label='UV 10') ax3.set_ylabel('UV Index') ax3.set_xlabel('Time') ax3.legend(loc='upper right') ax3.grid(True, alpha=0.3) # Format x-axis for ax in [ax1, ax2, ax3]: ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) try: plt.tight_layout() except: # If tight_layout fails, adjust manually plt.subplots_adjust(hspace=0.4, bottom=0.15) if save_path: self._safe_save_plot(save_path) if show: plt.show() def plot_risk_components(self, risk_scores: List[RiskScore], save_path: Optional[str] = None, show: bool = True) -> None: """Plot breakdown of risk score components.""" if not risk_scores: print("No risk data to plot") return fig, ax = plt.subplots(figsize=self.figure_size) # Extract data times = [score.datetime for score in risk_scores] temp_scores = [score.temperature_score for score in risk_scores] uv_scores = [score.uv_score for score in risk_scores] condition_scores = [score.condition_score for score in risk_scores] accumulated_scores = [score.accumulated_heat_score for score in risk_scores] recovery_scores = [score.surface_recovery_score for score in risk_scores] # Create stacked area plot ax.stackplot(times, temp_scores, uv_scores, condition_scores, accumulated_scores, recovery_scores, labels=['Temperature', 'UV Index', 'Condition', 'Accumulated Heat', 'Surface Recovery'], alpha=0.7) ax.set_ylabel('Risk Score Components') ax.set_xlabel('Time') ax.set_title('Risk Score Component Breakdown') ax.legend(loc='upper left', bbox_to_anchor=(1, 1)) ax.grid(True, alpha=0.3) # Format x-axis ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) try: plt.tight_layout() except: # If tight_layout fails, adjust manually plt.subplots_adjust(right=0.75, bottom=0.15) if save_path: self._safe_save_plot(save_path) print(f"Component plot saved") if show: plt.show() def plot_risk_heatmap(self, risk_scores: List[RiskScore], save_path: Optional[str] = None, show: bool = True) -> None: """Create a heatmap visualization of risk throughout the day.""" if not risk_scores: print("No risk data to plot") return fig, ax = plt.subplots(figsize=(12, 3)) # Create time vs risk matrix hours = [score.datetime.hour for score in risk_scores] scores = [score.total_score for score in risk_scores] # Create a matrix for the heatmap hour_range = list(range(24)) risk_matrix = np.zeros((1, 24)) for hour, score in zip(hours, scores): risk_matrix[0, hour] = score # Create heatmap im = ax.imshow(risk_matrix, cmap='RdYlBu_r', aspect='auto', vmin=0, vmax=10) # Customize the plot ax.set_xticks(range(24)) ax.set_xticklabels([f'{h:02d}:00' for h in range(24)]) ax.set_yticks([]) ax.set_xlabel('Hour of Day') ax.set_title('Paw Burn Risk Heatmap') # Add colorbar cbar = plt.colorbar(im, ax=ax, orientation='horizontal', pad=0.1) cbar.set_label('Risk Score') # Add threshold line threshold_line = 6.0 / 10.0 # Normalize to colormap scale with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) try: plt.tight_layout() except: # If tight_layout fails, adjust manually plt.subplots_adjust(bottom=0.2) if save_path: self._safe_save_plot(save_path) print(f"Heatmap saved") if show: plt.show() def create_summary_dashboard(self, risk_scores: List[RiskScore], weather_hours: List[WeatherHour], recommendations: dict, save_path: Optional[str] = None, show: bool = True) -> None: """Create a comprehensive dashboard with all visualizations.""" if not risk_scores or not weather_hours: print("Insufficient data for dashboard") return fig = plt.figure(figsize=(16, 12)) # Create subplots gs = fig.add_gridspec(3, 2, height_ratios=[2, 1, 1], hspace=0.3, wspace=0.3) # Main timeline plot ax1 = fig.add_subplot(gs[0, :]) times = [score.datetime for score in risk_scores] total_scores = [score.total_score for score in risk_scores] ax1.plot(times, total_scores, 'b-', linewidth=3, label='Risk Score') ax1.axhline(y=6, color='red', linestyle='--', alpha=0.7, label='Shoe Threshold') # Highlight high-risk periods high_risk_periods = [] current_start = None for i, score in enumerate(risk_scores): if score.recommend_shoes and current_start is None: current_start = i elif not score.recommend_shoes and current_start is not None: high_risk_periods.append((current_start, i)) current_start = None if current_start is not None: high_risk_periods.append((current_start, len(risk_scores))) for start_idx, end_idx in high_risk_periods: ax1.axvspan(times[start_idx], times[end_idx-1], alpha=0.3, color='red') ax1.set_ylabel('Risk Score') ax1.set_title('Paw Burn Risk Assessment - Daily Overview') ax1.legend() ax1.grid(True, alpha=0.3) ax1.set_ylim(0, 10) # Component breakdown ax2 = fig.add_subplot(gs[1, 0]) component_names = ['Temp', 'UV', 'Condition', 'Heat Accum', 'Recovery'] avg_components = [ np.mean([s.temperature_score for s in risk_scores]), np.mean([s.uv_score for s in risk_scores]), np.mean([s.condition_score for s in risk_scores]), np.mean([s.accumulated_heat_score for s in risk_scores]), np.mean([s.surface_recovery_score for s in risk_scores]) ] bars = ax2.bar(component_names, avg_components, color=['orange', 'purple', 'lightblue', 'yellow', 'green']) ax2.set_ylabel('Average Score') ax2.set_title('Average Risk Components') ax2.tick_params(axis='x', rotation=45) # Statistics ax3 = fig.add_subplot(gs[1, 1]) ax3.axis('off') stats_text = f""" DAILY SUMMARY Total Hours Analyzed: {recommendations['summary']['total_hours_analyzed']} High Risk Hours: {recommendations['summary']['high_risk_hours']} Max Risk Score: {recommendations['summary']['max_risk_score']} Average Risk Score: {recommendations['summary']['average_risk_score']} Peak Risk Time: {recommendations['summary']['peak_risk_time']} Risk Periods: {recommendations['summary']['continuous_risk_blocks']} """ ax3.text(0.1, 0.9, stats_text, transform=ax3.transAxes, fontsize=10, verticalalignment='top', fontfamily='monospace') # Weather conditions ax4 = fig.add_subplot(gs[2, :]) temperatures = [hour.temperature_f for hour in weather_hours] uv_indices = [hour.uv_index or 0 for hour in weather_hours] ax4_twin = ax4.twinx() line1 = ax4.plot(times, temperatures, 'orange', linewidth=2, label='Temperature (°F)') line2 = ax4_twin.plot(times, uv_indices, 'purple', linewidth=2, label='UV Index') ax4.set_ylabel('Temperature (°F)', color='orange') ax4_twin.set_ylabel('UV Index', color='purple') ax4.set_xlabel('Time') ax4.set_title('Weather Conditions') # Format x-axis for all subplots for ax in [ax1, ax4]: ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) # Use constrained layout or manual adjustment instead of tight_layout with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) try: plt.tight_layout() except: # If tight_layout fails with complex layouts, adjust manually plt.subplots_adjust(hspace=0.4, wspace=0.4, bottom=0.1, top=0.95) if save_path: self._safe_save_plot(save_path) print(f"Dashboard saved") if show: plt.show() def create_plotter(figure_size: Tuple[int, int] = (12, 8)) -> RiskPlotter: """Create a risk plotter with specified figure size.""" return RiskPlotter(figure_size)