import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, ReferenceLine, ComposedChart, ScatterChart, Scatter, Line, Cell, } from 'recharts' import { api } from '../../utils/api' import { getStatusColor, getStatusBg } from '../../utils/interpret' import { EmptySection, PeriodSelector, SectionHeader } from './historyPageChrome' import { HISTORY_OVERVIEW_VIZ_PAGE_FULL, normalizeHistoryOverviewVizConfig } from '../../widgetSystem/historyOverviewVizConfig' 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 lagDetailsToCurve(meta) { let ld = meta?.lag_details if (!Array.isArray(ld) || ld.length === 0) { const m = String(meta?.metric || '').toUpperCase() if (m === 'HRV' && Array.isArray(meta?.lag_details_hrv)) ld = meta.lag_details_hrv else if (m === 'RHR' && Array.isArray(meta?.lag_details_rhr)) ld = meta.lag_details_rhr else { const h = meta?.lag_details_hrv const r = meta?.lag_details_rhr const hl = Array.isArray(h) ? h.length : 0 const rl = Array.isArray(r) ? r.length : 0 if (hl >= rl && hl > 0) ld = h else if (rl > 0) ld = r else ld = [] } } if (!Array.isArray(ld) || ld.length === 0) return [] return ld .map((d) => ({ lag: Number(d?.lag), r: d?.r == null || d?.r === '' ? null : Number(d.r), n_pairs: d?.n_pairs != null ? Number(d.n_pairs) : null, })) .filter((d) => Number.isFinite(d.lag) && d.r != null && Number.isFinite(d.r)) .sort((a, b) => a.lag - b.lag) } 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 curve = lagDetailsToCurve(meta) 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' const bestLag = meta.best_lag_days != null ? Number(meta.best_lag_days) : null const maxLagAxis = curve.length ? Math.max(14, ...curve.map((d) => d.lag), bestLag || 0) : 28 return (
{title}
r = {meta.correlation != null ? Number(meta.correlation).toFixed(3) : '—'} {meta.best_lag_days != null ? ` · bestes Lag ${meta.best_lag_days} T` : ''} {meta.metric ? ` · ${meta.metric}` : ''} {meta.confidence ? ` · ${meta.confidence}` : ''}
{!hasChart ? ( <>
{meta.message || 'Keine Daten für diese Korrelation.'}
{curve.length > 0 && (
Lag-Sweep (kein Lag mit ≥15 Paaren): r über Lags — nur zur Einordnung.
)} {curve.length > 0 && ( [`r = ${Number(v).toFixed(3)}`, `Lag ${item?.payload?.lag} T · n = ${item?.payload?.n_pairs ?? '—'}`]} /> )} ) : curve.length >= 1 ? ( <>
Kurve: Pearson-r je Lag (Tage); starker Punkt = gewähltes bestes Lag.
[`r = ${Number(v).toFixed(3)}`, `Lag ${item?.payload?.lag} T · n = ${item?.payload?.n_pairs ?? '—'}`]} /> { const { cx, cy, payload: pl } = props if (cx == null || cy == null || !pl) return null const isBest = bestLag != null && Number(pl.lag) === bestLag return ( ) }} /> ) : ( )} {meta.interpretation ? (
{meta.interpretation}
) : null}
) } 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) => ( ))}
) } /** * Verlauf «Gesamt» / Dashboard-Widget: Layer-2b history-overview-viz (+ chart_payloads C1–C4). * * @param {object} props * @param {import('react').ReactNode} [props.footer] * @param {number} [props.externalPeriod] — feste Tage (Widget); sonst interner PeriodSelector (30…9999) * @param {boolean} [props.hidePeriodSelector] * @param {boolean} [props.embedded] * @param {Record} [props.visibility] — normalisierte Widget-Config; undefined = Verlauf volle Ansicht */ export default function HistoryOverviewVizSection({ footer = null, externalPeriod, hidePeriodSelector = false, embedded = false, visibility: visibilityProp, }) { const navigate = useNavigate() const [period, setPeriod] = useState(30) const [bundle, setBundle] = useState(null) const [err, setErr] = useState(null) const [loading, setLoading] = useState(true) const effPeriod = externalPeriod != null ? externalPeriod : period const daysReq = effPeriod === 9999 ? 3650 : effPeriod useEffect(() => { let cancelled = false setLoading(true) const attachCharts = (overview, c1, c2, c3, c4) => { if (!cancelled) { setBundle({ overview, chartC1: c1, chartC2: c2, chartC3: c3, chartC4: c4 }) setErr(null) } } const run = async () => { try { const overview = await api.getHistoryOverviewViz(daysReq) const cp = overview?.chart_payloads if (cp && cp.c1_weight_energy != null && cp.c2_protein_lbm != null && cp.c3_load_vitals != null && cp.c4_recovery_performance != null) { attachCharts(overview, cp.c1_weight_energy, cp.c2_protein_lbm, cp.c3_load_vitals, cp.c4_recovery_performance) } else { const [chartC1, chartC2, chartC3, chartC4] = await Promise.all([ api.getWeightEnergyCorrelationChart(14), api.getLbmProteinCorrelationChart(14), api.getLoadVitalsCorrelationChart(14), api.getRecoveryPerformanceChart(), ]) attachCharts(overview, chartC1, chartC2, chartC3, chartC4) } } catch (e) { if (!cancelled) setErr(e.message || 'Laden fehlgeschlagen') } finally { if (!cancelled) setLoading(false) } } run() return () => { cancelled = true } }, [daysReq]) if (loading) { return (
{!embedded && } {!hidePeriodSelector && externalPeriod == null && }
) } if (err) { return (
{!embedded && } {!hidePeriodSelector && externalPeriod == null && }
{err}
) } 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 c4drivers = lag.recovery_performance?.drivers || [] const sections = data?.sections || [] const confUi = overviewConfidenceUi(data?.confidence) const vis = visibilityProp != null ? normalizeHistoryOverviewVizConfig(visibilityProp) : HISTORY_OVERVIEW_VIZ_PAGE_FULL return (
{!embedded && } {!hidePeriodSelector && externalPeriod == null && } {vis.show_confidence_banner && (
{confUi.tone === 'good' ? '●' : confUi.tone === 'warn' ? '◐' : '○'}
{confUi.label}
{confUi.hint}
)} {vis.show_intro_blurb && (

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 entsprechen denselben Chart.js-Payloads wie /api/charts/* (bei aktuellem Backend im Overview-Bundle enthalten).

)} {vis.show_area_summaries && (sections.length === 0 ? ( ) : (
{sections.map((sec) => { const tone = overviewSectionTone(sec) const stripe = getStatusColor(tone) const badgeBg = getStatusBg(tone) return (
{tone === 'good' ? '✓' : tone === 'warn' ? '!' : '!!'}
{sec.title}
{sec.summary_line}
{(sec.kpi_short || []).length > 0 && (
{(sec.kpi_short || []).map((k, i) => (
{k.category}
{k.value}
{k.sublabel ?
{k.sublabel}
: null}
))}
)} {(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}
))}
) })}
))} {vis.show_correlation_c1_c3 && ( <>
Lag-Korrelationen (C1–C3)
)} {vis.show_drivers_c4 && ( <>
Einflussfaktoren (C4)
)} {footer}
) }