diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 71164ff..b0eb75a 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -4,7 +4,8 @@ import { useProfile } from '../context/ProfileContext' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, - ReferenceLine, PieChart, Pie, Cell, ComposedChart + ReferenceLine, PieChart, Pie, Cell, ComposedChart, + ScatterChart, Scatter, } from 'recharts' import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2 } from 'lucide-react' import { api } from '../utils/api' @@ -990,6 +991,9 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct

Kennzahlen und Charts nutzen dieselbe Berechnung wie die KI-Platzhalter (Ernährungs-Data-Layer).{' '} Kalorien vs. Gewicht und TDEE-Referenz entsprechen Mifflin–St Jeor × PAL 1,55 bzw. kg-Fallback (32,5 kcal/kg). + {' '} + Kalorienbilanz, Protein vs. Magermasse und den Block{' '} + «Kurz-Einordnung» finden Sie hier — früher im eigenen Reiter «Korrelation» (jetzt Data-Layer-Bundle).

@@ -1242,36 +1246,163 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA ) } -function LagCorrelationMetaCard({ title, block }) { - if (!block) return null - const ok = block.available +function overviewSectionTone(sec) { + const kpis = sec.kpi_short || [] + if (kpis.some((k) => k.status === 'bad')) return 'bad' + if (kpis.some((k) => k.status === 'warn')) return 'warn' + const interp = sec.interpretation_short || [] + if (interp.some((x) => x.status === 'bad')) return 'bad' + if (interp.some((x) => x.status === 'warn')) return 'warn' + const heur = sec.heuristic_short || [] + if (heur.some((h) => h.status === 'warn')) return 'warn' + return 'good' +} + +function overviewConfidenceUi(conf) { + if (conf === 'high') return { label: 'Datenlage: gut', tone: 'good', hint: 'Ausreichend Messpunkte für sinnvolle Kurzinfos.' } + if (conf === 'medium') return { label: 'Datenlage: mittel', tone: 'warn', hint: 'Einzelne Bereiche sind noch dünn besetzt.' } + return { label: 'Datenlage: dünn', tone: 'bad', hint: 'Mehr Einträge verbessern die Aussagekraft.' } +} + +function chartJsScatterPoints(payload) { + const raw = payload?.data?.datasets?.[0]?.data || [] + if (!Array.isArray(raw)) return [] + return raw.map((p) => ({ x: Number(p.x), y: Number(p.y) })) +} + +function driverBarFromStatus(st) { + const s = String(st || '').toLowerCase() + if (s.includes('hinder')) return { v: -1, fill: 'var(--danger)' } + if (s.includes('förder') || s.includes('foerder')) return { v: 1, fill: 'var(--accent)' } + return { v: 0.15, fill: '#6B7280' } +} + +function chartJsBarRows(payload, fallbackDrivers) { + const labels = payload?.data?.labels || [] + const values = payload?.data?.datasets?.[0]?.data || [] + const colors = payload?.data?.datasets?.[0]?.backgroundColor + if (labels.length && values.length) { + return labels.map((name, i) => ({ + name: name.length > 42 ? `${name.slice(0, 40)}…` : name, + value: Number(values[i]), + fill: Array.isArray(colors) ? colors[i] : Number(values[i]) < 0 ? '#EF4444' : '#1D9E75', + })) + } + if (fallbackDrivers?.length) { + return fallbackDrivers.map((d) => { + const { v, fill } = driverBarFromStatus(d.status) + return { + name: String(d.factor || '—').length > 40 ? `${String(d.factor).slice(0, 38)}…` : String(d.factor || '—'), + value: v, + fill, + subtitle: d.reason, + } + }) + } + return [] +} + +function CorrelationScatterTile({ title, accent, payload }) { + const meta = payload?.metadata || {} + const pts = chartJsScatterPoints(payload) + const hasChart = pts.length > 0 && meta.correlation != null + const r = Number(meta.correlation) + const strength = + !Number.isFinite(r) ? 'bad' : Math.abs(r) >= 0.35 ? 'good' : Math.abs(r) >= 0.15 ? 'warn' : 'bad' + return ( -
-
{title}
- {!ok ? ( -
Nicht genug Daten für diese Auswertung.
+
+
{title}
+
+ r = {meta.correlation != null ? Number(meta.correlation).toFixed(3) : '—'} + {meta.best_lag_days != null ? ` · Lag ${meta.best_lag_days} T` : ''} + {meta.metric ? ` · ${meta.metric}` : ''} + {meta.confidence ? ` · ${meta.confidence}` : ''} +
+ {!hasChart ? ( +
{meta.message || 'Keine Daten für diese Korrelation.'}
) : ( - <> -
- r ≈ {block.correlation != null ? Number(block.correlation).toFixed(3) : '—'} - {block.best_lag_days != null ? ` · Lag ${block.best_lag_days} Tage` : ''} - {block.metric ? ` · ${block.metric}` : ''} - {block.confidence ? ` · ${block.confidence}` : ''} -
- {block.interpretation ? ( -
{block.interpretation}
- ) : null} - + + + + + + + + + + )} + {meta.interpretation ? ( +
{meta.interpretation}
+ ) : null}
) } -// ── Gesamtansicht (Layer 2b: GET /charts/history-overview-viz) ───────────────── +function DriversImpactTile({ payload, driversFallback }) { + const meta = payload?.metadata || {} + const rows = chartJsBarRows(payload, driversFallback) + if (!rows.length) { + return ( +
+
C4 Einflussfaktoren
+
{meta.message || 'Keine Treiber-Daten.'}
+
+ ) + } + const h = Math.min(220, Math.max(96, rows.length * 34)) + return ( +
+
C4 Einflussfaktoren
+ + + + + { + if (!active || !pp?.length) return null + const p = pp[0].payload + return ( +
+
{p.name}
+ {p.subtitle ?
{p.subtitle}
: null} +
+ ) + }} + /> + + {rows.map((e, i) => ( + + ))} + +
+
+
+ ) +} + +// ── Gesamtansicht (Layer 2b: overview + Chart-Endpunkte C1–C4) ────────────────── function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActiveSlugs }) { const navigate = useNavigate() const [period, setPeriod] = useState(30) - const [data, setData] = useState(null) + const [bundle, setBundle] = useState(null) const [err, setErr] = useState(null) const [loading, setLoading] = useState(true) @@ -1279,10 +1410,16 @@ function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActive let cancelled = false const daysReq = period === 9999 ? 3650 : period setLoading(true) - api.getHistoryOverviewViz(daysReq) - .then((d) => { + Promise.all([ + api.getHistoryOverviewViz(daysReq), + api.getWeightEnergyCorrelationChart(14), + api.getLbmProteinCorrelationChart(14), + api.getLoadVitalsCorrelationChart(14), + api.getRecoveryPerformanceChart(), + ]) + .then(([overview, chartC1, chartC2, chartC3, chartC4]) => { if (!cancelled) { - setData(d) + setBundle({ overview, chartC1, chartC2, chartC3, chartC4 }) setErr(null) } }) @@ -1315,86 +1452,153 @@ function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActive ) } + const data = bundle?.overview + const chartC1 = bundle?.chartC1 + const chartC2 = bundle?.chartC2 + const chartC3 = bundle?.chartC3 + const chartC4 = bundle?.chartC4 + const lag = data?.lag_correlations || {} - const c4 = lag.recovery_performance + const c4drivers = lag.recovery_performance?.drivers || [] const sections = data?.sections || [] + const confUi = overviewConfidenceUi(data?.confidence) return (
-

- Kurzüberblick aus denselben Data-Layer-Bundles wie die Reiter Körper bis Erholung (Issue 53). Lag-Korrelationen C1–C4 - stammen aus correlations.py / Chart-Endpunkte. + +

+ {confUi.tone === 'good' ? '●' : confUi.tone === 'warn' ? '◐' : '○'} +
+
{confUi.label}
+
{confUi.hint}
+
+
+ +

+ KPIs und Texte kommen aus den Layer-2b-Bundles (Körper, Ernährung, Fitness, Erholung).{' '} + Ehem. «Korrelation»-Charts (Bilanz, Protein/Mager, Kurz-Einordnung) liegen unter{' '} + + . Die Kacheln C1–C4 unten nutzen dieselben Chart-Endpunkte wie die API (/api/charts/*).

- {sections.map((sec) => ( -
-
-
{sec.title}
- -
-
{sec.summary_line}
- {(sec.interpretation_short || []).map((it, i) => ( -
- {it.title} -
{it.detail}
-
- ))} - {(sec.kpi_short || []).map((k, i) => ( -
- {k.icon} {k.category} - {' · '} - {k.value} - {k.sublabel ? — {k.sublabel} : null} -
- ))} - {(sec.heuristic_short || []).map((h, i) => ( -
- {h.title} -
{h.detail}
-
- ))} - {(sec.insights_short || []).map((ins, i) => ( -
- {ins.title} -
{ins.body}
-
- ))} -
- ))} +
+
+ + {tone === 'good' ? '✓' : tone === 'warn' ? '!' : '!!'} + +
{sec.title}
+
+ +
+
{sec.summary_line}
-
- Lag-Korrelationen (C1–C3) -
- - - + {(sec.kpi_short || []).length > 0 && ( +
+ {(sec.kpi_short || []).map((k, i) => ( +
+
{k.category}
+
{k.value}
+ {k.sublabel ?
{k.sublabel}
: null} +
+ ))} +
+ )} -
- Einflussfaktoren (C4) -
-
- {c4?.drivers?.length ? ( - c4.drivers.map((d, i) => ( -
- {d.factor} - ({d.status}) -
{d.reason}
+ {(sec.interpretation_short || []).map((it, i) => ( +
+ {it.title} +
{it.detail}
+
+ ))} + {(sec.heuristic_short || []).map((h, i) => ( +
+ {h.title} +
{h.detail}
+
+ ))} + {(sec.insights_short || []).map((ins, i) => ( +
+ {ins.title} +
{ins.body}
+
+ ))}
- )) - ) : ( -
Keine Treiber-Daten.
- )} + ) + })}
+
Lag-Korrelationen (C1–C3)
+
+ + + +
+ +
Einflussfaktoren (C4)
+ +
)