# src/tools/viz_executor.py import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from typing import List, Dict, Any from domain.training.charts import ChartSpec, ChartType from .interfaces import IVisualizationExecutor from src.tools.runner_ai import calculate_pace_s_per_km class TemplateVisualizationExecutor(IVisualizationExecutor): def render_chart(self, chart_spec: ChartSpec, features: List[Dict[str, Any]]) -> plt.Figure: if chart_spec.chart_type == ChartType.PACE: return self._render_pace_chart(features, chart_spec.title or "Pace over Time") elif chart_spec.chart_type == ChartType.HEART_RATE: return self._render_hr_chart(features, chart_spec.title or "Heart Rate over Time") else: return self._empty_chart(f"Unsupported Chart Type: {chart_spec.chart_type}") def _render_pace_chart(self, features: List[Dict[str, Any]], title: str) -> plt.Figure: fig, ax = plt.subplots(figsize=(10, 5)) dates = [] paces = [] for run in features: dt = run.get("start_time") pace_s = calculate_pace_s_per_km( run.get("total_distance_m"), run.get("total_duration_s") ) if dt and pace_s: dates.append(dt) # Convert s/km to min/km (float) paces.append(pace_s / 60.0) if not dates: return self._empty_chart(title) ax.plot(dates, paces, marker="o", linestyle="-", color="blue") ax.set_title(title) ax.set_ylabel("Pace (min/km)") ax.set_xlabel("Date") ax.grid(True) # Invert y-axis for pace (smaller min/km is faster) ax.invert_yaxis() fig.autofmt_xdate() return fig def _render_hr_chart(self, features: List[Dict[str, Any]], title: str) -> plt.Figure: fig, ax = plt.subplots(figsize=(10, 5)) dates = [] hrs = [] for run in features: dt = run.get("start_time") hr = run.get("avg_hr_bpm") if dt and hr: dates.append(dt) hrs.append(hr) if not dates: return self._empty_chart(title) ax.plot(dates, hrs, marker="s", linestyle="--", color="red") ax.set_title(title) ax.set_ylabel("Heart Rate (bpm)") ax.set_xlabel("Date") ax.grid(True) fig.autofmt_xdate() return fig def _empty_chart(self, title: str) -> plt.Figure: fig, ax = plt.subplots(figsize=(6, 3)) ax.text(0.5, 0.5, "No Data Available", ha="center", va="center") ax.set_title(title) ax.axis("off") return fig