""" Chart-Daten für Berichts-PDF: dieselbe Logik wie /api/charts/* (Data Layer), ohne HTTP. """ from __future__ import annotations from typing import Any, Callable from data_layer.activity_metrics import ( build_training_type_distribution_chart_payload, build_training_volume_chart_payload, ) from data_layer.body_metrics import get_weight_trend_data from data_layer.nutrition_chart_payloads import build_energy_balance_chart_payload from data_layer.nutrition_metrics import get_nutrition_average_data from data_layer.utils import serialize_dates def _weight_trend_payload(profile_id: str, days: int) -> dict[str, Any]: d = min(max(days, 7), 365) trend_data = get_weight_trend_data(profile_id, d) if trend_data["confidence"] == "insufficient": return { "chart_type": "line", "data": {"labels": [], "datasets": []}, "metadata": { "confidence": "insufficient", "data_points": 0, "message": "Nicht genug Daten für Trend-Analyse", }, } series = trend_data.get("series") or [] labels = [ pt["date"].isoformat() if hasattr(pt["date"], "isoformat") else str(pt["date"]) for pt in series ] values = [pt["weight"] for pt in series] return { "chart_type": "line", "data": { "labels": labels, "datasets": [ { "label": "Gewicht", "data": values, "borderColor": "#1D9E75", "backgroundColor": "rgba(29, 158, 117, 0.1)", "borderWidth": 2, "tension": 0.4, "fill": True, "pointRadius": 2, } ], }, "metadata": serialize_dates( { "confidence": trend_data["confidence"], "data_points": trend_data["data_points"], "first_value": trend_data["first_value"], "last_value": trend_data["last_value"], "delta": trend_data["delta"], "direction": trend_data["direction"], } ), } def _macro_distribution_payload(profile_id: str, days: int) -> dict[str, Any]: d = min(max(days, 7), 90) macro_data = get_nutrition_average_data(profile_id, d) if macro_data["confidence"] == "insufficient": return { "chart_type": "pie", "data": {"labels": [], "datasets": []}, "metadata": {"confidence": "insufficient", "message": "Keine Ernährungsdaten vorhanden"}, } protein_kcal = macro_data["protein_avg"] * 4 carbs_kcal = macro_data["carbs_avg"] * 4 fat_kcal = macro_data["fat_avg"] * 9 total_kcal = protein_kcal + carbs_kcal + fat_kcal if total_kcal == 0: return { "chart_type": "pie", "data": {"labels": [], "datasets": []}, "metadata": {"confidence": "insufficient", "message": "Keine Makronährstoff-Daten"}, } protein_pct = protein_kcal / total_kcal * 100 carbs_pct = carbs_kcal / total_kcal * 100 fat_pct = fat_kcal / total_kcal * 100 return { "chart_type": "pie", "data": { "labels": ["Protein", "Kohlenhydrate", "Fett"], "datasets": [ { "data": [round(protein_pct, 1), round(carbs_pct, 1), round(fat_pct, 1)], "backgroundColor": ["#1D9E75", "#F59E0B", "#EF4444"], "borderWidth": 2, "borderColor": "#fff", } ], }, "metadata": {"confidence": macro_data.get("confidence", "high")}, } def _training_volume_payload(profile_id: str, window_days: int) -> dict[str, Any]: w = max(4, min(52, window_days // 7)) return build_training_volume_chart_payload(profile_id, w) _CHART_FETCHERS: dict[str, Callable[[str, int], dict[str, Any]]] = { "weight_trend": _weight_trend_payload, "energy_balance": lambda pid, d: build_energy_balance_chart_payload(pid, min(max(d, 7), 90)), "macro_distribution": _macro_distribution_payload, "training_volume": _training_volume_payload, "training_type_distribution": lambda pid, d: build_training_type_distribution_chart_payload( pid, min(max(d, 7), 90) ), } def fetch_chart_payload(chart_id: str, profile_id: str, window_days: int) -> dict[str, Any]: fn = _CHART_FETCHERS.get(chart_id) if not fn: raise ValueError(f"Unbekanntes chart_id: {chart_id}") return fn(profile_id, window_days) CHART_CATALOG_FOR_API: list[dict[str, Any]] = [ {"id": "weight_trend", "title": "Gewichtstrend", "default_window_days": 90, "window_max": 365}, {"id": "energy_balance", "title": "Energiebilanz", "default_window_days": 28, "window_max": 90}, {"id": "macro_distribution", "title": "Makroverteilung (Ø)", "default_window_days": 28, "window_max": 90}, {"id": "training_volume", "title": "Trainingsvolumen (Wochen)", "default_window_days": 84, "window_max": 365}, { "id": "training_type_distribution", "title": "Trainingsart-Verteilung", "default_window_days": 28, "window_max": 90, }, ]