- Added contextual hints to KPI tiles in the nutrition interpretation to provide users with actionable insights regarding protein intake and weight assessment. - Updated the KpiTilesOverview component to display these hints, improving user understanding of nutrition metrics. - Introduced a new KcalVsWeightLegend component to clarify chart data representation, enhancing the overall user experience in the history visualization.
190 lines
6.9 KiB
Python
190 lines
6.9 KiB
Python
"""
|
||
Interpretation + KPI-Kacheln für Layer 2b Ernährungs-Verlauf.
|
||
|
||
Gleiche Schwellen wie zuvor im Frontend (History.jsx); Ausgabe strukturiert
|
||
für KpiTilesOverview (keys = related_placeholder_keys).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
|
||
def _verdict(status: str) -> str:
|
||
if status == "good":
|
||
return "Gut"
|
||
if status == "warn":
|
||
return "Hinweis"
|
||
return "Achtung"
|
||
|
||
|
||
def build_nutrition_history_kpi_tiles(
|
||
navg: Dict[str, Any],
|
||
targets: Dict[str, Any],
|
||
date_span_label: str,
|
||
n_days_with_entries: int,
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
KPI-Kacheln wie buildNutritionKpiTiles im Frontend (Kalorien/KH/Fett + Regeln).
|
||
"""
|
||
kcal_avg = round(float(navg.get("kcal_avg") or 0))
|
||
avg_carbs = round(float(navg.get("carbs_avg") or 0) * 10) / 10
|
||
avg_fat = round(float(navg.get("fat_avg") or 0) * 10) / 10
|
||
avg_protein = round(float(navg.get("protein_avg") or 0) * 10) / 10
|
||
|
||
pt_low = round(float(targets.get("protein_target_low") or 0))
|
||
pt_high = round(float(targets.get("protein_target_high") or 0))
|
||
targets_ok = targets.get("confidence") != "insufficient" and pt_low > 0
|
||
protein_ok = targets_ok and avg_protein >= pt_low
|
||
|
||
total_macro_kcal = avg_protein * 4 + avg_carbs * 4 + avg_fat * 9
|
||
prot_pct = (
|
||
round(avg_protein * 4 / total_macro_kcal * 100)
|
||
if total_macro_kcal > 0
|
||
else 0
|
||
)
|
||
kh_pct = (
|
||
round(avg_carbs * 4 / total_macro_kcal * 100)
|
||
if total_macro_kcal > 0
|
||
else 0
|
||
)
|
||
fat_pct = (
|
||
round(avg_fat * 9 / total_macro_kcal * 100)
|
||
if total_macro_kcal > 0
|
||
else 0
|
||
)
|
||
|
||
tiles: List[Dict[str, Any]] = [
|
||
{
|
||
"key": "kcal",
|
||
"category": "Kalorien (Ø)",
|
||
"icon": "🔥",
|
||
"value": f"{kcal_avg} kcal",
|
||
"sublabel": date_span_label,
|
||
"status": "good",
|
||
"verdict": "Gut",
|
||
"hoverTop": "Durchschnittliche tägliche Energie",
|
||
"hoverBody": f"Mittel über {n_days_with_entries} Tage mit Ernährungseinträgen im gewählten Zeitraum.",
|
||
"keys": ["nutrition_score"],
|
||
},
|
||
{
|
||
"key": "carbs",
|
||
"category": "KH (Ø)",
|
||
"icon": "🌾",
|
||
"value": f"{avg_carbs} g",
|
||
"sublabel": "Kohlenhydrate / Tag",
|
||
"status": "good",
|
||
"verdict": "Gut",
|
||
"hoverTop": "Durchschnittliche Kohlenhydrate",
|
||
"hoverBody": "Summe der täglichen Werte im Zeitraum, gemittelt.",
|
||
"keys": ["nutrition_summary"],
|
||
},
|
||
{
|
||
"key": "fat",
|
||
"category": "Fett (Ø)",
|
||
"icon": "🧈",
|
||
"value": f"{avg_fat} g",
|
||
"sublabel": "Fett / Tag",
|
||
"status": "good",
|
||
"verdict": "Gut",
|
||
"hoverTop": "Durchschnittliches Fett",
|
||
"hoverBody": "Summe der täglichen Werte im Zeitraum, gemittelt.",
|
||
"keys": ["nutrition_summary"],
|
||
},
|
||
]
|
||
|
||
if not targets_ok:
|
||
tiles.append(
|
||
{
|
||
"key": "eval-protein",
|
||
"category": "Protein",
|
||
"icon": "🥩",
|
||
"value": f"{avg_protein}g",
|
||
"sublabel": "Referenzgewicht fehlt",
|
||
"status": "warn",
|
||
"verdict": _verdict("warn"),
|
||
"hint": "Ohne aktuelles Körpergewicht lässt sich das Protein-Ziel (g/kg) nicht bewerten.",
|
||
"hoverTop": "Protein-Ziel nicht berechenbar",
|
||
"hoverBody": "Für 1,6–2,2 g/kg wird ein aktuelles Körpergewicht benötigt.",
|
||
"keys": ["protein_adequacy"],
|
||
}
|
||
)
|
||
elif not protein_ok:
|
||
miss = max(0, pt_low - round(avg_protein))
|
||
tiles.append(
|
||
{
|
||
"key": "eval-protein",
|
||
"category": "Protein",
|
||
"icon": "🥩",
|
||
"value": f"{avg_protein}g",
|
||
"sublabel": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||
"status": "bad",
|
||
"verdict": _verdict("bad"),
|
||
"hint": (
|
||
f"Es fehlen rund {miss} g Protein pro Tag – bei Kaloriendefizit "
|
||
"steigt das Risiko für Muskelerhalt."
|
||
),
|
||
"hoverTop": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||
"hoverBody": (
|
||
f"1,6–2,2g/kg KG. Fehlend: ~{miss}g täglich. "
|
||
"Konsequenz: Muskelverlust bei Defizit."
|
||
),
|
||
"keys": ["protein_adequacy", "nutrition_score"],
|
||
}
|
||
)
|
||
else:
|
||
tiles.append(
|
||
{
|
||
"key": "eval-protein",
|
||
"category": "Protein",
|
||
"icon": "🥩",
|
||
"value": f"{avg_protein}g",
|
||
"sublabel": f"Gut: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||
"status": "good",
|
||
"verdict": _verdict("good"),
|
||
"hoverTop": f"Gut: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||
"hoverBody": "Ausreichend für Muskelerhalt und -aufbau.",
|
||
"keys": ["protein_adequacy", "nutrition_score"],
|
||
}
|
||
)
|
||
|
||
if prot_pct < 20 and total_macro_kcal > 0:
|
||
tiles.append(
|
||
{
|
||
"key": "eval-macro-pct",
|
||
"category": "Makro-Anteil",
|
||
"icon": "📊",
|
||
"value": f"{prot_pct}%",
|
||
"sublabel": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
||
"status": "warn",
|
||
"verdict": _verdict("warn"),
|
||
"hint": (
|
||
f"Viele Kalorien kommen aus KH/Fett; Proteinanteil oft sinnvoll bei 25–35 % "
|
||
f"(aktuell P {prot_pct} % / KH {kh_pct} % / F {fat_pct} %)."
|
||
),
|
||
"hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
||
"hoverBody": (
|
||
f"Empfehlung oft 25–35%. Aktuell: {prot_pct}% P / {kh_pct}% KH / {fat_pct}% F"
|
||
),
|
||
"keys": ["nutrition_summary"],
|
||
}
|
||
)
|
||
|
||
return tiles
|
||
|
||
|
||
def build_macro_donut_from_averages(navg: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
|
||
"""Anteile in % der Makro-kcal + Gramm für Legende."""
|
||
p = float(navg.get("protein_avg") or 0)
|
||
c = float(navg.get("carbs_avg") or 0)
|
||
f = float(navg.get("fat_avg") or 0)
|
||
pkcal, ckcal, fkcal = p * 4, c * 4, f * 9
|
||
tot = pkcal + ckcal + fkcal
|
||
if tot <= 0:
|
||
return None
|
||
return [
|
||
{"name": "Protein", "value": round(pkcal / tot * 100), "color": "#4a8f72", "grams": round(p, 1)},
|
||
{"name": "KH", "value": round(ckcal / tot * 100), "color": "#c17d45", "grams": round(c, 1)},
|
||
{"name": "Fett", "value": round(fkcal / tot * 100), "color": "#6e8eb8", "grams": round(f, 1)},
|
||
]
|