diff --git a/frontend/src/app.css b/frontend/src/app.css index 8a2c520..c6d23c7 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -350,6 +350,24 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we font-style: italic; } +/* Verlauf Ernährung: Donut (Ø-Quote) + wöchentliche Makro-Verteilung (E3) */ +.nutrition-macro-pair { + display: grid; + gap: 12px; + margin-bottom: 12px; + align-items: stretch; +} + +@media (min-width: 780px) { + .nutrition-macro-pair { + grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.25fr); + } +} + +.nutrition-macro-pair__weekly { + min-width: 0; +} + .history-page__title { margin-bottom: 12px; } diff --git a/frontend/src/components/NutritionCharts.jsx b/frontend/src/components/NutritionCharts.jsx index 9a03343..9d8d312 100644 --- a/frontend/src/components/NutritionCharts.jsx +++ b/frontend/src/components/NutritionCharts.jsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import { LineChart, Line, BarChart, Bar, - XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend + XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, + ComposedChart, ReferenceArea, } from 'recharts' import { api } from '../utils/api' import dayjs from 'dayjs' @@ -135,16 +136,74 @@ function WarningCard({ title, warning_level, triggers, message }) { ) } +/** Wöchentliche Makro-Verteilung (E3) — für Verlauf neben Donut nutzbar. */ +export function WeeklyMacroDistributionPanel({ macroWeeklyData, loading, error }) { + if (loading) { + return ( +
+
+
+ ) + } + if (error) { + return ( +
{error}
+ ) + } + if (!macroWeeklyData || macroWeeklyData.metadata?.confidence === 'insufficient') { + const msg = macroWeeklyData?.metadata?.message || 'Nicht genug Daten für Wochen-Analyse (min. 7 Tage)' + return ( +
{msg}
+ ) + } + + const chartData = macroWeeklyData.data.labels.map((label, i) => ({ + week: label, + protein: macroWeeklyData.data.datasets[0]?.data[i], + carbs: macroWeeklyData.data.datasets[1]?.data[i], + fat: macroWeeklyData.data.datasets[2]?.data[i], + })) + + const meta = macroWeeklyData.metadata + + return ( + <> +
+ Anteil der Kalorien aus jedem Makronährstoff pro Kalenderwoche (100 % gestapelt). Gut vergleichbar mit der + Donut-Übersicht links. +
+ + + + + + [`${v}%`, name]} + /> + + + + + + +
+ Ø Verteilung: P {meta.avg_protein_pct}% · KH {meta.avg_carbs_pct}% · F {meta.avg_fat_pct}% · Variabilität (CV): P{' '} + {meta.protein_cv}% · KH {meta.carbs_cv}% · F {meta.fat_cv}% +
+ + ) +} + /** - * Nutrition Charts Component (E1-E5) - Konzept-konform v2.0 - * - * E1: Energy Balance (mit 7d/14d Durchschnitten) - * E2: Protein Adequacy (mit 7d/28d Durchschnitten) - * E3: Weekly Macro Distribution (100% gestapelte Balken) - * E4: Nutrition Adherence Score (0-100, goal-aware) - * E5: Energy Availability Warning (Ampel-System) + * Nutrition Charts (E1–E5). Verlauf: `showWeeklyMacroDistribution={false}` wenn E3 separat (z. B. neben Donut) gerendert wird. */ -export default function NutritionCharts({ days = 28 }) { +export default function NutritionCharts({ days = 28, showWeeklyMacroDistribution = true }) { const [energyData, setEnergyData] = useState(null) const [proteinData, setProteinData] = useState(null) const [macroWeeklyData, setMacroWeeklyData] = useState(null) @@ -159,16 +218,19 @@ export default function NutritionCharts({ days = 28 }) { useEffect(() => { loadCharts() - }, [days]) + }, [days, showWeeklyMacroDistribution]) const loadCharts = async () => { - await Promise.all([ + const tasks = [ loadEnergyBalance(), loadProteinAdequacy(), - loadMacroWeekly(), loadAdherence(), - loadWarning() - ]) + loadWarning(), + ] + if (showWeeklyMacroDistribution) { + tasks.splice(2, 0, loadMacroWeekly()) + } + await Promise.all(tasks) } const loadEnergyBalance = async () => { @@ -236,12 +298,13 @@ export default function NutritionCharts({ days = 28 }) { } } - // E1: Energy Balance Timeline (mit 7d/14d Durchschnitten) + // E1: Energy Balance — klare Farben (kein hellgraues Gewirr) const renderEnergyBalance = () => { if (!energyData || energyData.metadata?.confidence === 'insufficient') { - return
- Nicht genug Ernährungsdaten (min. 7 Tage) -
+ const msg = energyData?.metadata?.message || 'Nicht genug Ernährungsdaten für die Energiebilanz.' + return ( +
{msg}
+ ) } const chartData = energyData.data.labels.map((label, i) => ({ @@ -249,7 +312,7 @@ export default function NutritionCharts({ days = 28 }) { täglich: energyData.data.datasets[0]?.data[i], avg7d: energyData.data.datasets[1]?.data[i], avg14d: energyData.data.datasets[2]?.data[i], - tdee: energyData.data.datasets[3]?.data[i] + tdee: energyData.data.datasets[3]?.data[i], })) const balance = energyData.metadata?.energy_balance || 0 @@ -257,111 +320,90 @@ export default function NutritionCharts({ days = 28 }) { return ( <> - - - - - - - - - - - +
+ Tägliche Aufnahme, gleitende Mittel und geschätzter TDEE — Linien sind farblich getrennt (Legende unten). +
+ + + + + + + + + + + -
- - Ø {energyData.metadata.avg_kcal} kcal/Tag · - - - Balance: {balance > 0 ? '+' : ''}{balance} kcal/Tag - - - · {energyData.metadata.data_points} Tage +
+ Ø {energyData.metadata.avg_kcal} kcal/Tag · + + Balance: {balance > 0 ? '+' : ''} + {balance} kcal/Tag + · {energyData.metadata.data_points} Tage
) } - // E2: Protein Adequacy Timeline (mit 7d/28d Durchschnitten) + // E2: Protein — Zielzone als Fläche, Linien klar von E1 abgrenzbar const renderProteinAdequacy = () => { if (!proteinData || proteinData.metadata?.confidence === 'insufficient') { - return
- Nicht genug Protein-Daten (min. 7 Tage) -
+ const msg = proteinData?.metadata?.message || 'Nicht genug Protein-Daten für dieses Diagramm.' + return ( +
{msg}
+ ) } + const tl = proteinData.metadata.target_low + const th = proteinData.metadata.target_high + const chartData = proteinData.data.labels.map((label, i) => ({ date: fmtDate(label), täglich: proteinData.data.datasets[0]?.data[i], avg7d: proteinData.data.datasets[1]?.data[i], avg28d: proteinData.data.datasets[2]?.data[i], - targetLow: proteinData.data.datasets[3]?.data[i], - targetHigh: proteinData.data.datasets[4]?.data[i] })) return ( <> - - - - - - - - - - - - - - -
- {proteinData.metadata.days_in_target}/{proteinData.metadata.data_points} Tage im Zielbereich ({proteinData.metadata.target_compliance_pct}%) +
+ Grüne Zone = empfohlenes Protein-Ziel (g/Tag). Tägliche Werte und Mittel — andere Farben als Energiebilanz oben.
- - ) - } - - // E3: Weekly Macro Distribution (100% gestapelte Balken) - const renderMacroWeekly = () => { - if (!macroWeeklyData || macroWeeklyData.metadata?.confidence === 'insufficient') { - return
- Nicht genug Daten für Wochen-Analyse (min. 7 Tage) -
- } - - const chartData = macroWeeklyData.data.labels.map((label, i) => ({ - week: label, - protein: macroWeeklyData.data.datasets[0]?.data[i], - carbs: macroWeeklyData.data.datasets[1]?.data[i], - fat: macroWeeklyData.data.datasets[2]?.data[i] - })) - - const meta = macroWeeklyData.metadata - - return ( - <> - - - - - - - - - - - + + + + + + + {tl != null && th != null && ( + + )} + + + + + -
- Ø Verteilung: P {meta.avg_protein_pct}% · C {meta.avg_carbs_pct}% · F {meta.avg_fat_pct}% · - Konsistenz (CV): P {meta.protein_cv}% · C {meta.carbs_cv}% · F {meta.fat_cv}% +
+ Ziel {tl}–{th} g/Tag · {proteinData.metadata.days_in_target}/{proteinData.metadata.data_points} Tage im Zielbereich ( + {proteinData.metadata.target_compliance_pct}%)
) @@ -414,17 +456,19 @@ export default function NutritionCharts({ days = 28 }) { return (
- + {renderEnergyBalance()} - + {renderProteinAdequacy()} - - {renderMacroWeekly()} - + {showWeeklyMacroDistribution && ( + + + + )} {!loading.adherence && !errors.adherence && renderAdherence()} {!loading.warning && !errors.warning && renderWarning()} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 587a45f..9e18919 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -13,7 +13,7 @@ import { getBfCategory } from '../utils/calc' import { getStatusColor, getStatusBg } from '../utils/interpret' import Markdown from '../utils/Markdown' import TrainingTypeDistribution from '../components/TrainingTypeDistribution' -import NutritionCharts from '../components/NutritionCharts' +import NutritionCharts, { WeeklyMacroDistributionPanel } from '../components/NutritionCharts' import RecoveryCharts from '../components/RecoveryCharts' import KpiTilesOverview from '../components/KpiTilesOverview' import dayjs from 'dayjs' @@ -233,6 +233,51 @@ function buildBodyKpiTiles({ return tiles } +function NutritionGoalsStrip({ grouped }) { + const nav = useNavigate() + const goals = (grouped?.nutrition || []).filter(g => g.status === 'active').slice(0, 4) + if (!goals.length) return null + return ( +
+
+
Ernährungsbezogene Ziele
+ +
+
+ {goals.map(g => ( +
+
{g.name || g.label_de || g.goal_type}
+
+
+
+
+ {Math.round(g.progress_pct ?? 0)}% · Ziel {g.target_value}{g.unit ? ` ${g.unit}` : ''} +
+
+ ))} +
+
+ ) +} + function BodyGoalsStrip({ grouped }) { const nav = useNavigate() const goals = (grouped?.body || []).filter(g => g.status === 'active').slice(0, 4) @@ -653,191 +698,296 @@ function BodySection({ profile, insights, onRequest, loadingSlug, filterActiveSl
) } -// ── Nutrition Section ───────────────────────────────────────────────────────── -function NutritionSection({ nutrition, weights, profile, insights, onRequest, loadingSlug, filterActiveSlugs }) { - const [period, setPeriod] = useState(30) - if (!nutrition?.length) return ( - - ) - const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') - const filtN = nutrition.filter(d=>period===9999||d.date>=cutoff) - const sorted = [...filtN].sort((a,b)=>a.date.localeCompare(b.date)) +function buildNutritionKpiTiles({ + avgKcal, avgCarbs, avgFat, n, macroRules, dateSpanLabel, +}) { + const tiles = [ + { + key: 'kcal', + category: 'Kalorien (Ø)', + icon: '🔥', + value: `${avgKcal} kcal`, + sublabel: dateSpanLabel, + status: 'good', + verdict: '', + hoverTop: 'Durchschnittliche tägliche Energie', + hoverBody: `Mittel über ${n} Tage mit Ernährungseinträgen im gewählten Zeitraum.`, + }, + { + key: 'carbs', + category: 'KH (Ø)', + icon: '🌾', + value: `${avgCarbs} g`, + sublabel: 'Kohlenhydrate / Tag', + status: 'good', + verdict: '', + hoverTop: 'Durchschnittliche Kohlenhydrate', + hoverBody: 'Summe der täglichen Werte im Zeitraum, gemittelt.', + }, + { + key: 'fat', + category: 'Fett (Ø)', + icon: '🧈', + value: `${avgFat} g`, + sublabel: 'Fett / Tag', + status: 'good', + verdict: '', + hoverTop: 'Durchschnittliches Fett', + hoverBody: 'Summe der täglichen Werte im Zeitraum, gemittelt.', + }, + ] + macroRules.forEach((r, i) => { + tiles.push({ + key: `eval-${i}`, + category: r.category, + icon: r.icon, + value: r.value, + sublabel: r.title.length > 36 ? `${r.title.slice(0, 34)}…` : r.title, + status: r.status, + verdict: verdictShort(r.status === 'warn' ? 'warn' : r.status === 'bad' ? 'bad' : 'good'), + hoverTop: r.title, + hoverBody: r.detail, + }) + }) + return tiles +} - if (!filtN.length) return ( -
- - - +/** Kalorien (Ø 7T) vs. Gewicht — gleiche Logik wie früher unter Korrelationen. */ +function KcalVsWeightChart({ corrData: corrRows, profile, cutoffDate, allTime }) { + const raw = (corrRows || []).filter(d => { + if (!d.kcal || d.weight == null) return false + const ds = typeof d.date === 'string' ? d.date.slice(0, 10) : dayjs(d.date).format('YYYY-MM-DD') + return allTime || ds >= cutoffDate + }) + if (raw.length < 5) return null + + const sex = profile?.sex || 'm' + const height = profile?.height || 178 + const latestW = raw[raw.length - 1]?.weight || 80 + const age = profile?.dob ? Math.floor((Date.now() - new Date(profile.dob)) / (365.25 * 24 * 3600 * 1000)) : 35 + 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') + + return ( +
+
+ Kalorien (Ø 7 Tage) vs. Gewicht +
+
+ Gleitender 7-Tage-Mittelwert der Kalorien vs. tägliches Gewicht (gemeinsame Tage). Orange: kcal · Blau: Gewicht. +
+ + + + + + + [`${Math.round(v)} ${n === 'weight' ? 'kg' : 'kcal'}`, n === 'kcal_avg' ? 'Ø Kalorien' : 'Gewicht']} + /> + + + + + +
+ Referenz TDEE ~{tdee} kcal (Mifflin ×1,4, gestrichelt) · {raw.length} gemeinsame Tage +
) +} + +// ── Nutrition Section ───────────────────────────────────────────────────────── +function NutritionSection({ nutrition, weights, profile, insights, onRequest, loadingSlug, filterActiveSlugs, corrData }) { + const [period, setPeriod] = useState(30) + const [groupedGoals, setGroupedGoals] = useState(null) + const chartDays = period === 9999 ? 90 : period + const weeks = Math.max(4, Math.min(52, Math.ceil(chartDays / 7))) + const [weeklyMacro, setWeeklyMacro] = useState(null) + const [wmLoading, setWmLoading] = useState(false) + const [wmError, setWmError] = useState(null) + + useEffect(() => { + let cancelled = false + api.listGoalsGrouped() + .then(g => { if (!cancelled) setGroupedGoals(g) }) + .catch(() => { if (!cancelled) setGroupedGoals({}) }) + return () => { cancelled = true } + }, []) + + useEffect(() => { + let cancelled = false + setWmLoading(true) + setWmError(null) + api.getWeeklyMacroDistributionChart(weeks) + .then(d => { if (!cancelled) setWeeklyMacro(d) }) + .catch(e => { if (!cancelled) setWmError(e.message || 'Laden fehlgeschlagen') }) + .finally(() => { if (!cancelled) setWmLoading(false) }) + return () => { cancelled = true } + }, [weeks]) + + if (!nutrition?.length) { + return ( + + ) + } + + const cutoff = dayjs().subtract(period, 'day').format('YYYY-MM-DD') + const filtN = nutrition.filter(d => period === 9999 || d.date >= cutoff) + const sorted = [...filtN].sort((a, b) => a.date.localeCompare(b.date)) + + if (!filtN.length) { + return ( +
+ + + +
+ ) + } const n = filtN.length - const avgKcal = Math.round(filtN.reduce((s,d)=>s+(d.kcal||0),0)/n) - const avgProtein = Math.round(filtN.reduce((s,d)=>s+(d.protein_g||0),0)/n*10)/10 - const avgFat = Math.round(filtN.reduce((s,d)=>s+(d.fat_g||0),0)/n*10)/10 - const avgCarbs = Math.round(filtN.reduce((s,d)=>s+(d.carbs_g||0),0)/n*10)/10 - const latestW = weights?.[0]?.weight||80 - const ptLow = Math.round(latestW*1.6) - const ptHigh = Math.round(latestW*2.2) - const proteinOk = avgProtein>=ptLow + const avgKcal = Math.round(filtN.reduce((s, d) => s + (d.kcal || 0), 0) / n) + const avgProtein = Math.round(filtN.reduce((s, d) => s + (d.protein_g || 0), 0) / n * 10) / 10 + const avgFat = Math.round(filtN.reduce((s, d) => s + (d.fat_g || 0), 0) / n * 10) / 10 + const avgCarbs = Math.round(filtN.reduce((s, d) => s + (d.carbs_g || 0), 0) / n * 10) / 10 + const latestW = weights?.[0]?.weight || 80 + const ptLow = Math.round(latestW * 1.6) + const ptHigh = Math.round(latestW * 2.2) + const proteinOk = avgProtein >= ptLow - // Stacked macro bar (daily) - const cdMacro = sorted.map(d=>({ + const cdMacro = sorted.map(d => ({ date: fmtDate(d.date), - Protein: Math.round(d.protein_g||0), - KH: Math.round(d.carbs_g||0), - Fett: Math.round(d.fat_g||0), - kcal: Math.round(d.kcal||0), + Protein: Math.round(d.protein_g || 0), + KH: Math.round(d.carbs_g || 0), + Fett: Math.round(d.fat_g || 0), + kcal: Math.round(d.kcal || 0), })) - // Pie - const totalMacroKcal = avgProtein*4+avgCarbs*4+avgFat*9 + const totalMacroKcal = avgProtein * 4 + avgCarbs * 4 + avgFat * 9 const pieData = [ - {name:'Protein',value:Math.round(avgProtein*4/totalMacroKcal*100),color:'#1D9E75'}, - {name:'KH', value:Math.round(avgCarbs*4/totalMacroKcal*100), color:'#D4537E'}, - {name:'Fett', value:Math.round(avgFat*9/totalMacroKcal*100), color:'#378ADD'}, + { name: 'Protein', value: Math.round(avgProtein * 4 / totalMacroKcal * 100), color: '#059669' }, + { name: 'KH', value: Math.round(avgCarbs * 4 / totalMacroKcal * 100), color: '#EA580C' }, + { name: 'Fett', value: Math.round(avgFat * 9 / totalMacroKcal * 100), color: '#2563EB' }, ] - // Weekly macro bars - const weeklyMap={} - filtN.forEach(d=>{ - const wk=dayjs(d.date).format('YYYY-WW') - const weekNum = (() => { const dt=new Date(d.date); dt.setHours(0,0,0,0); dt.setDate(dt.getDate()+4-(dt.getDay()||7)); const y=new Date(dt.getFullYear(),0,1); return Math.ceil(((dt-y)/86400000+1)/7) })() - if(!weeklyMap[wk]) weeklyMap[wk]={label:'KW'+weekNum,n:0,protein:0,carbs:0,fat:0,kcal:0} - weeklyMap[wk].protein+=d.protein_g||0; weeklyMap[wk].carbs+=d.carbs_g||0 - weeklyMap[wk].fat+=d.fat_g||0; weeklyMap[wk].kcal+=d.kcal||0; weeklyMap[wk].n++ - }) - const weeklyData=Object.values(weeklyMap).slice(-12).map(w=>({ - label:w.label, - Protein:Math.round(w.protein/w.n), - KH:Math.round(w.carbs/w.n), - Fett:Math.round(w.fat/w.n), - kcal:Math.round(w.kcal/w.n), - })) + const macroRules = [] + if (!proteinOk) { + macroRules.push({ + status: 'bad', icon: '🥩', category: 'Protein', + title: `Unterversorgung: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, + detail: `1,6–2,2g/kg KG. Fehlend: ~${Math.max(0, ptLow - Math.round(avgProtein))}g täglich. Konsequenz: Muskelverlust bei Defizit.`, + value: `${avgProtein}g`, + }) + } else { + macroRules.push({ + status: 'good', icon: '🥩', category: 'Protein', + title: `Gut: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, + detail: 'Ausreichend für Muskelerhalt und -aufbau.', + value: `${avgProtein}g`, + }) + } + const protPct = Math.round(avgProtein * 4 / totalMacroKcal * 100) + if (protPct < 20) { + macroRules.push({ + status: 'warn', icon: '📊', category: 'Makro-Anteil', + title: `Protein-Anteil niedrig: ${protPct}% der Kalorien`, + detail: `Empfehlung oft 25–35%. Aktuell: ${protPct}% P / ${Math.round(avgCarbs * 4 / totalMacroKcal * 100)}% KH / ${Math.round(avgFat * 9 / totalMacroKcal * 100)}% F`, + value: `${protPct}%`, + }) + } - // Rules - const macroRules=[] - if(!proteinOk) macroRules.push({status:'bad',icon:'🥩',category:'Protein', - title:`Unterversorgung: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, - detail:`1,6–2,2g/kg KG. Fehlend: ~${ptLow-Math.round(avgProtein)}g täglich. Konsequenz: Muskelverlust bei Defizit.`, - value:avgProtein+'g'}) - else macroRules.push({status:'good',icon:'🥩',category:'Protein', - title:`Gut: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, - detail:`Ausreichend für Muskelerhalt und -aufbau.`,value:avgProtein+'g'}) - const protPct=Math.round(avgProtein*4/totalMacroKcal*100) - if(protPct<20) macroRules.push({status:'warn',icon:'📊',category:'Makro-Anteil', - title:`Protein-Anteil niedrig: ${protPct}% der Kalorien`, - detail:`Empfehlung: 25–35%. Aktuell: ${protPct}% P / ${Math.round(avgCarbs*4/totalMacroKcal*100)}% KH / ${Math.round(avgFat*9/totalMacroKcal*100)}% F`, - value:protPct+'%'}) + const dateSpanLabel = `${sorted[0]?.date?.slice(0, 10) ?? ''} – ${sorted[sorted.length - 1]?.date?.slice(0, 10) ?? ''}` + const kpiTiles = buildNutritionKpiTiles({ + avgKcal, avgCarbs, avgFat, n, macroRules, dateSpanLabel, + }) return (
-
- {[['Ø Kalorien',avgKcal+' kcal','#EF9F27'],['Ø Protein',avgProtein+'g',proteinOk?'#1D9E75':'#D85A30'], - ['Ø Fett',avgFat+'g','#378ADD'],['Ø KH',avgCarbs+'g','#D4537E'], - ['Einträge',n+' T','var(--text3)']].map(([l,v,c])=>( -
-
{v}
-
{l}
-
- ))} -
+

+ Kennzahlen und Charts nutzen dieselben Datenquellen wie die KI-Platzhalter (Ernährungs-Log, Gewicht).{' '} + Kalorien vs. Gewicht bezieht gemeinsame Tage aus Ernährung und Gewicht. +

- {/* Stacked macro bars (daily) */} -
-
- Makroverteilung täglich (g) · {sorted[0]?.date?.slice(0,7)} – {sorted[sorted.length-1]?.date?.slice(0,7)} + + + + + + +
+
+ Makroverteilung täglich (g) · Fokus Protein
- - - - - - - [`${v}g`,n]}/> - - - +
+ Gestapelte Balken in Gramm; gestrichelte Linie = Protein-Minimum ({ptLow} g) nach 1,6 g/kg (Referenzgewicht). +
+ + + + + + + [`${v}g`, name]} /> + + + -
- Protein - KH - Fett - Protein-Ziel +
+ Protein (oben) + KH + Fett
- {/* Pie + macro breakdown */} -
-
- Ø Makroverteilung · {n} Tage ({sorted[0]?.date?.slice(0,10)} – {sorted[sorted.length-1]?.date?.slice(0,10)}) -
-
- - - {pieData.map((e,i)=>)} - - [`${v}%`,n]}/> - -
- {pieData.map(p=>( -
-
-
{p.name}
-
{p.value}%
-
{Math.round(p.name==='Protein'?avgProtein:p.name==='KH'?avgCarbs:avgFat)}g
- {p.name==='Protein' &&
- {proteinOk?'✓':'⚠️'} Ziel {ptLow}g -
} +
+
+
+ Ø Makro-Quote ({n} Tage) +
+
+ + + {pieData.map((e, i) => )} + + [`${v}%`, name]} /> + +
+ {pieData.map(p => ( +
+
+
{p.name}
+
{p.value}%
+
+ {Math.round(p.name === 'Protein' ? avgProtein : p.name === 'KH' ? avgCarbs : avgFat)}g +
+
+ ))} +
+ Ø {avgKcal} kcal/Tag · Anteil der Makro-Kalorien am Tagesumsatz
- ))} -
- Gesamt: {avgKcal} kcal/Tag
-
- - {/* Weekly stacked bars */} - {weeklyData.length>=2 && ( -
-
Makros pro Woche (Ø g/Tag)
- - - - - - - [`${v}g`,n]}/> - - - - - +
+
+ Wöchentliche Makro-Verteilung (Backend) +
+
- )} - -
-
BEWERTUNG
- {macroRules.map((item,i)=>)}
- {/* New Nutrition Charts (Phase 0c) */} -
-
📊 DETAILLIERTE CHARTS
- +
+ Zeitverläufe (Energie & Protein)
+
@@ -964,10 +1114,7 @@ function CorrelationSection({ corrData, insights, profile, onRequest, loadingSlu 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) // light activity baseline - // Chart 1: Kcal vs Weight - const kcalVsW = rollingAvg(filtered.map(d=>({...d,date:fmtDate(d.date)})),'kcal') - - // Chart 2: Protein vs Lean Mass (only days with both) + // Protein vs Lean Mass (only days with both) const protVsLean = filtered.filter(d=>d.protein_g&&d.lean_mass) .map(d=>({date:fmtDate(d.date),protein:d.protein_g,lean:d.lean_mass})) @@ -1043,31 +1190,11 @@ function CorrelationSection({ corrData, insights, profile, onRequest, loadingSlu
- {/* Chart 1: Kcal vs Weight */} -
-
- 📉 Kalorien (Ø 7T) vs. Gewicht -
- - - - - - - [`${Math.round(v)} ${n==='weight'?'kg':'kcal'}`,n==='kcal_avg'?'Ø Kalorien':'Gewicht']}/> - - - - - -
- Gestrichelt: geschätzter TDEE {tdee} kcal · — Kalorien · — Gewicht -
-
+

+ Das Diagramm Kalorien (Ø 7T) vs. Gewicht liegt unter Verlauf → Ernährung (gleiche Datenbasis). +

- {/* Chart 2: Calorie balance */} + {/* Chart: Calorie balance */}
⚖️ Kalorienbilanz (Aufnahme − TDEE {tdee} kcal) @@ -1374,7 +1501,7 @@ export default function History() {
{tab==='body' && } - {tab==='nutrition' && } + {tab==='nutrition' && } {tab==='activity' && } {tab==='recovery' && } {tab==='correlation' && }