feat: implement energy availability warning and enhance nutrition visualization
- Added `get_energy_availability_warning_payload` function to assess energy availability and provide contextual warnings based on multiple health indicators. - Integrated energy availability KPI tile into the nutrition history visualization, enhancing user insights on energy balance. - Updated frontend components to conditionally display the energy availability warning, improving user experience and data interpretation. - Refactored existing logic in `charts.py` to utilize the new energy availability functionality, streamlining data handling.
This commit is contained in:
parent
fc816da335
commit
d7304c1a44
|
|
@ -121,8 +121,7 @@ def build_nutrition_history_kpi_tiles(
|
|||
"status": "bad",
|
||||
"verdict": _verdict("bad"),
|
||||
"hint": (
|
||||
f"Es fehlen rund {miss} g Protein pro Tag – bei Kaloriendefizit "
|
||||
"steigt das Risiko für Muskelerhalt."
|
||||
f"~{miss} g Protein/Tag fehlen – bei Defizit Muskelerhalt gefährdet."
|
||||
),
|
||||
"hoverTop": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||||
"hoverBody": (
|
||||
|
|
@ -159,8 +158,8 @@ def build_nutrition_history_kpi_tiles(
|
|||
"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} %)."
|
||||
f"Protein-Kalorienanteil niedrig (P {prot_pct} % / KH {kh_pct} % / F {fat_pct} %); "
|
||||
"Ziel oft 25–35 %."
|
||||
),
|
||||
"hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
||||
"hoverBody": (
|
||||
|
|
@ -173,6 +172,37 @@ def build_nutrition_history_kpi_tiles(
|
|||
return tiles
|
||||
|
||||
|
||||
def build_energy_availability_kpi_tile(ea: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""E5: nur bei caution/warning — gleiche Daten wie /charts/energy-availability-warning."""
|
||||
level = str(ea.get("warning_level") or "none").strip().lower()
|
||||
if level == "none":
|
||||
return None
|
||||
triggers: List[str] = list(ea.get("triggers") or [])
|
||||
msg = str(ea.get("message") or "").strip()
|
||||
st = "bad" if level == "warning" else "warn"
|
||||
first = triggers[0] if triggers else msg
|
||||
if len(first) > 90:
|
||||
first = first[:87] + "…"
|
||||
meta = ea.get("metadata") if isinstance(ea.get("metadata"), dict) else {}
|
||||
note = str(meta.get("note") or "")
|
||||
hover_lines = [msg] + [f"• {t}" for t in triggers]
|
||||
if note:
|
||||
hover_lines.append(note)
|
||||
return {
|
||||
"key": "energy-availability-e5",
|
||||
"category": "Energieverfügbarkeit",
|
||||
"icon": "⚡",
|
||||
"value": "Achtung" if level == "warning" else "Hinweis",
|
||||
"sublabel": first or "Signale prüfen",
|
||||
"status": st,
|
||||
"verdict": _verdict(st),
|
||||
"hint": msg,
|
||||
"hoverTop": "Energieverfügbarkeit (Heuristik)",
|
||||
"hoverBody": "\n".join(hover_lines),
|
||||
"keys": ["nutrition_score"],
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -699,6 +699,70 @@ def get_weekly_macro_distribution_chart_data(profile_id: str, weeks: int) -> Dic
|
|||
}
|
||||
|
||||
|
||||
def get_energy_availability_warning_payload(profile_id: str, days: int = 14) -> Dict:
|
||||
"""
|
||||
E5 Energieverfügbarkeit — gleiche Heuristik wie GET /charts/energy-availability-warning.
|
||||
"""
|
||||
from data_layer.recovery_metrics import calculate_recovery_score_v2, calculate_sleep_quality_7d
|
||||
from data_layer.body_metrics import calculate_lbm_28d_change
|
||||
|
||||
triggers: List[str] = []
|
||||
warning_level = "none"
|
||||
|
||||
energy_data = get_energy_balance_data(profile_id, days)
|
||||
if energy_data.get("energy_balance", 0) < -500:
|
||||
triggers.append("Großes Energiedefizit (>500 kcal/Tag)")
|
||||
|
||||
try:
|
||||
recovery_score = calculate_recovery_score_v2(profile_id)
|
||||
if recovery_score and recovery_score < 50:
|
||||
triggers.append("Recovery Score niedrig (<50)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
sleep_quality = calculate_sleep_quality_7d(profile_id)
|
||||
if sleep_quality and sleep_quality < 60:
|
||||
triggers.append("Schlafqualität reduziert (<60%)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
lbm_change = calculate_lbm_28d_change(profile_id)
|
||||
if lbm_change and lbm_change < -1.0:
|
||||
triggers.append("Magermasse sinkt (-{:.1f} kg)".format(abs(lbm_change)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(triggers) >= 3:
|
||||
warning_level = "warning"
|
||||
message = (
|
||||
"⚠️ Hinweis auf mögliche Unterversorgung. Mehrere Indikatoren auffällig. "
|
||||
"Erwäge Defizit-Anpassung oder Regenerationswoche."
|
||||
)
|
||||
elif len(triggers) >= 2:
|
||||
warning_level = "caution"
|
||||
message = (
|
||||
"⚡ Beobachte folgende Signale genau. Aktuell noch kein Handlungsbedarf, aber Trend beachten."
|
||||
)
|
||||
elif len(triggers) >= 1:
|
||||
warning_level = "caution"
|
||||
message = "💡 Ein Indikator auffällig. Weiter beobachten."
|
||||
else:
|
||||
message = "✅ Energieverfügbarkeit unauffällig."
|
||||
|
||||
return {
|
||||
"warning_level": warning_level,
|
||||
"triggers": triggers,
|
||||
"message": message,
|
||||
"metadata": {
|
||||
"days_analyzed": days,
|
||||
"trigger_count": len(triggers),
|
||||
"note": "Heuristische Einschätzung, keine medizinische Diagnose",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Calculated Metrics (migrated from calculations/nutrition_metrics.py)
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from data_layer.nutrition_interpretation import (
|
||||
build_energy_availability_kpi_tile,
|
||||
build_macro_donut_from_averages,
|
||||
build_nutrition_history_kpi_tiles,
|
||||
)
|
||||
from data_layer.nutrition_metrics import (
|
||||
estimate_tdee_kcal_from_latest_weight,
|
||||
get_energy_availability_warning_payload,
|
||||
get_energy_balance_data,
|
||||
get_nutrition_average_data,
|
||||
get_protein_targets_data,
|
||||
|
|
@ -184,12 +186,14 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
|||
"tdee_reference_kcal": None,
|
||||
"energy_balance_meta": {},
|
||||
"interpretation_tiles": [],
|
||||
"energy_availability_warning": None,
|
||||
"meta": {"layer_1": "nutrition_metrics", "layer_2b": "nutrition_viz"},
|
||||
}
|
||||
|
||||
all_history = days >= 9999
|
||||
eff_days = 3650 if all_history else max(7, min(int(days), 3650))
|
||||
cutoff = _cutoff_sql(days)
|
||||
chart_days_for_pipeline = 90 if all_history else max(7, min(eff_days, 365))
|
||||
|
||||
navg = get_nutrition_average_data(profile_id, eff_days, all_history=all_history)
|
||||
targets = get_protein_targets_data(profile_id)
|
||||
|
|
@ -223,12 +227,18 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
|||
navg, targets, date_span_label or "—", max(1, n_days)
|
||||
)
|
||||
|
||||
ea_days = min(28, max(7, chart_days_for_pipeline))
|
||||
ea_payload = get_energy_availability_warning_payload(profile_id, ea_days)
|
||||
ea_tile = build_energy_availability_kpi_tile(ea_payload)
|
||||
kpi_tiles_out: List[Dict[str, Any]] = list(kpi_tiles)
|
||||
if ea_tile:
|
||||
kpi_tiles_out.append(ea_tile)
|
||||
|
||||
donut = build_macro_donut_from_averages(navg)
|
||||
|
||||
kw_points = _kcal_weight_points_for_window(profile_id, cutoff)
|
||||
pt_low = round(float(targets.get("protein_target_low") or 0))
|
||||
|
||||
chart_days_for_pipeline = 90 if all_history else max(7, min(eff_days, 365))
|
||||
weeks_for_weekly = max(4, min(52, (chart_days_for_pipeline + 6) // 7))
|
||||
weekly_chart = get_weekly_macro_distribution_chart_data(profile_id, weeks_for_weekly)
|
||||
|
||||
|
|
@ -255,8 +265,9 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
|||
"protein_target_high": targets.get("protein_target_high"),
|
||||
"reference_weight_kg": targets.get("current_weight"),
|
||||
},
|
||||
"kpi_tiles": kpi_tiles,
|
||||
"kpi_tiles": kpi_tiles_out,
|
||||
"interpretation_tiles": [],
|
||||
"energy_availability_warning": ea_payload,
|
||||
"daily_macros": daily_macros,
|
||||
"donut_avg_pct": donut,
|
||||
"protein_reference_line_g": pt_low,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from data_layer.nutrition_metrics import (
|
|||
get_macro_consistency_data,
|
||||
get_energy_balance_data,
|
||||
get_weekly_macro_distribution_chart_data,
|
||||
get_energy_availability_warning_payload,
|
||||
)
|
||||
from data_layer.activity_metrics import (
|
||||
get_activity_summary_data,
|
||||
|
|
@ -1026,87 +1027,10 @@ def get_energy_availability_warning(
|
|||
"""
|
||||
Energy Availability Warning (E5) - Konzept-konform.
|
||||
|
||||
Heuristic warning for potential undernutrition/overtraining.
|
||||
|
||||
Checks:
|
||||
- Persistent large deficit
|
||||
- Recovery score declining
|
||||
- Sleep quality declining
|
||||
- LBM declining
|
||||
|
||||
Args:
|
||||
days: Analysis window (7-28 days, default 14)
|
||||
session: Auth session (injected)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"warning_level": "none" | "caution" | "warning",
|
||||
"triggers": [...],
|
||||
"message": "..."
|
||||
}
|
||||
Datenberechnung: data_layer.nutrition_metrics.get_energy_availability_warning_payload
|
||||
"""
|
||||
profile_id = session['profile_id']
|
||||
|
||||
from db import get_db, get_cursor
|
||||
from data_layer.nutrition_metrics import get_energy_balance_data
|
||||
from data_layer.recovery_metrics import calculate_recovery_score_v2, calculate_sleep_quality_7d
|
||||
from data_layer.body_metrics import calculate_lbm_28d_change
|
||||
|
||||
triggers = []
|
||||
warning_level = "none"
|
||||
|
||||
# Check 1: Large energy deficit
|
||||
energy_data = get_energy_balance_data(profile_id, days)
|
||||
if energy_data.get('energy_balance', 0) < -500:
|
||||
triggers.append("Großes Energiedefizit (>500 kcal/Tag)")
|
||||
|
||||
# Check 2: Recovery declining
|
||||
try:
|
||||
recovery_score = calculate_recovery_score_v2(profile_id)
|
||||
if recovery_score and recovery_score < 50:
|
||||
triggers.append("Recovery Score niedrig (<50)")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check 3: Sleep quality
|
||||
try:
|
||||
sleep_quality = calculate_sleep_quality_7d(profile_id)
|
||||
if sleep_quality and sleep_quality < 60:
|
||||
triggers.append("Schlafqualität reduziert (<60%)")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check 4: LBM declining
|
||||
try:
|
||||
lbm_change = calculate_lbm_28d_change(profile_id)
|
||||
if lbm_change and lbm_change < -1.0:
|
||||
triggers.append("Magermasse sinkt (-{:.1f} kg)".format(abs(lbm_change)))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Determine warning level
|
||||
if len(triggers) >= 3:
|
||||
warning_level = "warning"
|
||||
message = "⚠️ Hinweis auf mögliche Unterversorgung. Mehrere Indikatoren auffällig. Erwäge Defizit-Anpassung oder Regenerationswoche."
|
||||
elif len(triggers) >= 2:
|
||||
warning_level = "caution"
|
||||
message = "⚡ Beobachte folgende Signale genau. Aktuell noch kein Handlungsbedarf, aber Trend beachten."
|
||||
elif len(triggers) >= 1:
|
||||
warning_level = "caution"
|
||||
message = "💡 Ein Indikator auffällig. Weiter beobachten."
|
||||
else:
|
||||
message = "✅ Energieverfügbarkeit unauffällig."
|
||||
|
||||
return {
|
||||
"warning_level": warning_level,
|
||||
"triggers": triggers,
|
||||
"message": message,
|
||||
"metadata": {
|
||||
"days_analyzed": days,
|
||||
"trigger_count": len(triggers),
|
||||
"note": "Heuristische Einschätzung, keine medizinische Diagnose"
|
||||
}
|
||||
}
|
||||
return get_energy_availability_warning_payload(profile_id, days)
|
||||
|
||||
|
||||
@router.get("/training-volume")
|
||||
|
|
|
|||
|
|
@ -350,6 +350,11 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
/* KPI: Kurz-Hinweis max. 2 Zeilen — Details weiter per ℹ */
|
||||
.kpi-tiles-card__hint {
|
||||
max-height: 2.8em;
|
||||
}
|
||||
|
||||
/* Verlauf Ernährung: Donut (Ø-Quote) + wöchentliche Makro-Verteilung (E3) */
|
||||
.nutrition-macro-pair {
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -125,14 +125,17 @@ export default function KpiTilesOverview({
|
|||
<div
|
||||
className="kpi-tiles-card__hint"
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
fontSize: 10,
|
||||
lineHeight: 1.45,
|
||||
marginTop: 6,
|
||||
paddingLeft: 8,
|
||||
borderLeft: `2px solid ${accent}`,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.35,
|
||||
color: 'var(--text2)',
|
||||
background: 'var(--surface2)',
|
||||
borderLeft: `3px solid ${accent}`,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{cardHint}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,12 @@ export function WeeklyMacroDistributionPanel({ macroWeeklyData, loading, error }
|
|||
/**
|
||||
* Nutrition Charts (E1–E5). Verlauf: `showWeeklyMacroDistribution={false}` wenn E3 separat (z. B. neben Donut) gerendert wird.
|
||||
*/
|
||||
export default function NutritionCharts({ days = 28, showWeeklyMacroDistribution = true }) {
|
||||
export default function NutritionCharts({
|
||||
days = 28,
|
||||
showWeeklyMacroDistribution = true,
|
||||
/** Verlauf: E5-Kachel liegt in nutrition-history-viz KPIs — doppelte Karte ausblenden */
|
||||
hideEnergyAvailabilityCard = false,
|
||||
}) {
|
||||
const [energyData, setEnergyData] = useState(null)
|
||||
const [proteinData, setProteinData] = useState(null)
|
||||
const [macroWeeklyData, setMacroWeeklyData] = useState(null)
|
||||
|
|
@ -221,15 +226,17 @@ export default function NutritionCharts({ days = 28, showWeeklyMacroDistribution
|
|||
|
||||
useEffect(() => {
|
||||
loadCharts()
|
||||
}, [days, showWeeklyMacroDistribution])
|
||||
}, [days, showWeeklyMacroDistribution, hideEnergyAvailabilityCard])
|
||||
|
||||
const loadCharts = async () => {
|
||||
const tasks = [
|
||||
loadEnergyBalance(),
|
||||
loadProteinAdequacy(),
|
||||
loadAdherence(),
|
||||
loadWarning(),
|
||||
]
|
||||
if (!hideEnergyAvailabilityCard) {
|
||||
tasks.push(loadWarning())
|
||||
}
|
||||
if (showWeeklyMacroDistribution) {
|
||||
tasks.splice(2, 0, loadMacroWeekly())
|
||||
}
|
||||
|
|
@ -474,7 +481,7 @@ export default function NutritionCharts({ days = 28, showWeeklyMacroDistribution
|
|||
)}
|
||||
|
||||
{!loading.adherence && !errors.adherence && renderAdherence()}
|
||||
{!loading.warning && !errors.warning && renderWarning()}
|
||||
{!hideEnergyAvailabilityCard && !loading.warning && !errors.warning && renderWarning()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -700,6 +700,26 @@ function BodySection({ profile, insights, onRequest, loadingSlug, filterActiveSl
|
|||
)
|
||||
}
|
||||
|
||||
/** TDEE-Linie muss in der kcal-Y-Domain liegen (sonst unsichtbar trotz Legende). */
|
||||
function kcalVsWeightKcalDomain(points, tdeeRef) {
|
||||
const vals = (points || [])
|
||||
.map(d => Number(d.kcal_avg))
|
||||
.filter(v => !Number.isNaN(v))
|
||||
if (!vals.length) return ['auto', 'auto']
|
||||
let lo = Math.min(...vals)
|
||||
let hi = Math.max(...vals)
|
||||
const t = tdeeRef != null ? Number(tdeeRef) : NaN
|
||||
if (!Number.isNaN(t)) {
|
||||
lo = Math.min(lo, t)
|
||||
hi = Math.max(hi, t)
|
||||
}
|
||||
const span = hi - lo || 400
|
||||
const pad = Math.max(100, span * 0.1)
|
||||
return [Math.max(0, Math.floor(lo - pad)), Math.ceil(hi + pad)]
|
||||
}
|
||||
|
||||
const TDEE_REF_LINE_COLOR = '#475569'
|
||||
|
||||
/** Legende unter dem Chart: Linien + ggf. TDEE-Referenz (gestrichelt). */
|
||||
function KcalVsWeightLegend({ showTdee }) {
|
||||
const line = (color) => ({
|
||||
|
|
@ -753,7 +773,7 @@ function KcalVsWeightLegend({ showTdee }) {
|
|||
height: 0,
|
||||
verticalAlign: 'middle',
|
||||
marginRight: 6,
|
||||
borderTop: '2px dashed #EA580C',
|
||||
borderTop: `2px dashed ${TDEE_REF_LINE_COLOR}`,
|
||||
opacity: 0.95,
|
||||
}}
|
||||
/>
|
||||
|
|
@ -774,6 +794,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
|||
}))
|
||||
const n = vizKcalWeight.common_days_count ?? kcalVsW.length
|
||||
const tdeeLabel = tdee != null && tdee > 0 ? Math.round(tdee) : null
|
||||
const kcalDomain = kcalVsWeightKcalDomain(kcalVsW, tdeeLabel)
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>
|
||||
|
|
@ -786,14 +807,21 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
|||
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} interval={Math.max(0, Math.floor(kcalVsW.length / 6) - 1)} />
|
||||
<YAxis yAxisId="kcal" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={['auto', 'auto']} />
|
||||
<YAxis yAxisId="kcal" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={kcalDomain} />
|
||||
<YAxis yAxisId="weight" orientation="right" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={['auto', 'auto']} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 11 }}
|
||||
formatter={(v, n) => [`${Math.round(v)} ${n === 'weight' ? 'kg' : 'kcal'}`, n === 'kcal_avg' ? 'Ø Kalorien' : 'Gewicht']}
|
||||
/>
|
||||
{tdeeLabel != null && (
|
||||
<ReferenceLine yAxisId="kcal" y={tdeeLabel} stroke="#EA580C" strokeDasharray="6 4" strokeWidth={1.2} />
|
||||
<ReferenceLine
|
||||
yAxisId="kcal"
|
||||
y={tdeeLabel}
|
||||
stroke={TDEE_REF_LINE_COLOR}
|
||||
strokeDasharray="6 5"
|
||||
strokeWidth={2}
|
||||
isFront
|
||||
/>
|
||||
)}
|
||||
<Line yAxisId="kcal" type="monotone" dataKey="kcal_avg" stroke="#EA580C" strokeWidth={2.5} dot={false} name="kcal_avg" />
|
||||
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
|
||||
|
|
@ -823,6 +851,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
|||
const bmr = sex === 'm' ? 10 * latestW + 6.25 * height - 5 * age + 5 : 10 * latestW + 6.25 * height - 5 * age - 161
|
||||
const tdee = Math.round(bmr * 1.4)
|
||||
const kcalVsW = rollingAvg(raw.map(d => ({ ...d, date: fmtDate(d.date) })), 'kcal')
|
||||
const kcalDomainFb = kcalVsWeightKcalDomain(kcalVsW, tdee)
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
|
|
@ -836,13 +865,20 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
|||
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
||||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} interval={Math.max(0, Math.floor(kcalVsW.length / 6) - 1)} />
|
||||
<YAxis yAxisId="kcal" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={['auto', 'auto']} />
|
||||
<YAxis yAxisId="kcal" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={kcalDomainFb} />
|
||||
<YAxis yAxisId="weight" orientation="right" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={['auto', 'auto']} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 11 }}
|
||||
formatter={(v, n) => [`${Math.round(v)} ${n === 'weight' ? 'kg' : 'kcal'}`, n === 'kcal_avg' ? 'Ø Kalorien' : 'Gewicht']}
|
||||
/>
|
||||
<ReferenceLine yAxisId="kcal" y={tdee} stroke="#EA580C" strokeDasharray="6 4" strokeWidth={1.2} />
|
||||
<ReferenceLine
|
||||
yAxisId="kcal"
|
||||
y={tdee}
|
||||
stroke={TDEE_REF_LINE_COLOR}
|
||||
strokeDasharray="6 5"
|
||||
strokeWidth={2}
|
||||
isFront
|
||||
/>
|
||||
<Line yAxisId="kcal" type="monotone" dataKey="kcal_avg" stroke="#EA580C" strokeWidth={2.5} dot={false} name="kcal_avg" />
|
||||
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
|
||||
</LineChart>
|
||||
|
|
@ -1054,7 +1090,7 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct
|
|||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8, marginTop: 4 }}>
|
||||
Zeitverläufe (Energie & Protein)
|
||||
</div>
|
||||
<NutritionCharts days={chartDays} showWeeklyMacroDistribution={false} />
|
||||
<NutritionCharts days={chartDays} showWeeklyMacroDistribution={false} hideEnergyAvailabilityCard />
|
||||
|
||||
<InsightBox insights={insights} slugs={filterActiveSlugs(['ernaehrung'])} onRequest={onRequest} loading={loadingSlug}/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user