refactor: streamline vital signs matrix handling and enhance recovery dashboard layout
- Removed unnecessary snapshot key omission in the `build_vital_signs_matrix_chart_payload` function for improved data clarity. - Introduced new components for better organization and presentation of vital signs insights, including `SectionHeading` and `VitalZoneHint`. - Enhanced axis tick formatting in the `RecoveryDashboardOverview` component for clearer data representation. - Updated narrative rendering logic to improve user experience and contextual understanding of vital metrics.
This commit is contained in:
parent
ce84f330f0
commit
857cc1043a
|
|
@ -91,11 +91,7 @@ def get_recovery_dashboard_viz_bundle(profile_id: str, days: int) -> Dict[str, A
|
|||
"hrv_rhr": build_hrv_rhr_baseline_chart_payload(profile_id, chart_days),
|
||||
"sleep_duration_quality": build_sleep_duration_quality_chart_payload(profile_id, chart_days),
|
||||
"sleep_debt": build_sleep_debt_chart_payload(profile_id, chart_days),
|
||||
"vital_signs_matrix": build_vital_signs_matrix_chart_payload(
|
||||
profile_id,
|
||||
vital_days,
|
||||
omit_snapshot_keys={"resting_hr", "hrv"},
|
||||
),
|
||||
"vital_signs_matrix": build_vital_signs_matrix_chart_payload(profile_id, vital_days),
|
||||
"vitals_history": build_vitals_history_and_analytics(
|
||||
profile_id, vital_days, hrv_vs_baseline_pct=hrv_f, rhr_vs_baseline_pct=rhr_f
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@ import dayjs from 'dayjs'
|
|||
|
||||
const fmtDate = (d) => dayjs(d).format('DD.MM.')
|
||||
|
||||
/** Nur diese Kennzahlen als eigene Verläufe — Ruhepuls/HRV nur im kombinierten Diagramm (keine Doppelung). */
|
||||
const VITAL_TREND_ONLY_KEYS = ['vo2_max', 'spo2', 'respiratory_rate']
|
||||
|
||||
function formatAxisTick(v) {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) return ''
|
||||
const a = Math.abs(n)
|
||||
if (a >= 100) return String(Math.round(n))
|
||||
if (a >= 10) return n.toFixed(1)
|
||||
return Number(n.toFixed(2)).toString()
|
||||
}
|
||||
|
||||
function insightBulletStripe(tone) {
|
||||
if (tone === 'good') return getStatusColor('good')
|
||||
if (tone === 'bad') return getStatusColor('bad')
|
||||
|
|
@ -15,10 +27,51 @@ function insightBulletStripe(tone) {
|
|||
return getStatusColor('warn')
|
||||
}
|
||||
|
||||
function ChartCard({ title, loading, error, children }) {
|
||||
function SectionHeading({ title, hint, compactTop }) {
|
||||
return (
|
||||
<div style={{ marginTop: compactTop ? 8 : 20, marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text1)' }}>{title}</div>
|
||||
{hint ? (
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4, lineHeight: 1.45 }}>{hint}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VitalZoneHint({ item }) {
|
||||
if (!item) return null
|
||||
const stripe = insightBulletStripe(item.tone)
|
||||
const t = item.tone
|
||||
const hintBg =
|
||||
t === 'good' ? getStatusBg('good') : t === 'bad' ? getStatusBg('bad') : t === 'warn' ? getStatusBg('warn') : 'var(--surface2)'
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: `4px solid ${stripe}`,
|
||||
background: hintBg,
|
||||
fontSize: 11,
|
||||
color: 'var(--text2)',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600, color: 'var(--text1)', marginRight: 6 }}>Zuletzt (Snapshot):</span>
|
||||
<span style={{ fontWeight: 600, color: stripe }}>{item.zone_label_de}</span>
|
||||
<span style={{ marginLeft: 6 }}>{item.hint_de}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChartCard({ title, loading, error, children, description }) {
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>{title}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: description ? 4 : 8 }}>{title}</div>
|
||||
{description ? (
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)', marginBottom: 8, lineHeight: 1.4 }}>{description}</div>
|
||||
) : null}
|
||||
{loading && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
|
||||
<div className="spinner" style={{ width: 32, height: 32 }} />
|
||||
|
|
@ -110,6 +163,17 @@ export default function RecoveryDashboardOverview({
|
|||
const vitalsData = viz.charts?.vital_signs_matrix
|
||||
const vitalsHistory = viz.charts?.vitals_history
|
||||
|
||||
const vitalItemsByKey = {}
|
||||
;(vitalsData?.metadata?.vital_items || []).forEach((it) => {
|
||||
vitalItemsByKey[it.key] = it
|
||||
})
|
||||
|
||||
const showVitalNarrativeBlock =
|
||||
vitalsHistory &&
|
||||
vitalsHistory.metadata?.confidence !== 'insufficient' &&
|
||||
(((vitalsHistory.analytics?.consolidated_paragraphs || []).length > 0 ||
|
||||
(vitalsHistory.analytics?.bullets || []).length > 0))
|
||||
|
||||
const kpiTiles = (viz.kpi_tiles || []).map((t) => ({
|
||||
...t,
|
||||
sublabel:
|
||||
|
|
@ -145,7 +209,14 @@ export default function RecoveryDashboardOverview({
|
|||
tickLine={false}
|
||||
interval={Math.max(0, Math.floor(chartData.length / 6) - 1)}
|
||||
/>
|
||||
<YAxis tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={[0, 100]} />
|
||||
<YAxis
|
||||
tick={{ fontSize: 9, fill: 'var(--text3)' }}
|
||||
tickLine={false}
|
||||
domain={[0, 100]}
|
||||
tickFormatter={(v) => (Number.isFinite(Number(v)) ? String(Math.round(Number(v))) : '')}
|
||||
tickCount={6}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'var(--surface)',
|
||||
|
|
@ -188,8 +259,23 @@ export default function RecoveryDashboardOverview({
|
|||
tickLine={false}
|
||||
interval={Math.max(0, Math.floor(chartData.length / 6) - 1)}
|
||||
/>
|
||||
<YAxis yAxisId="left" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} />
|
||||
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} />
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
tick={{ fontSize: 9, fill: 'var(--text3)' }}
|
||||
tickLine={false}
|
||||
tickFormatter={formatAxisTick}
|
||||
tickCount={6}
|
||||
width={44}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tick={{ fontSize: 9, fill: 'var(--text3)' }}
|
||||
tickLine={false}
|
||||
tickFormatter={formatAxisTick}
|
||||
tickCount={6}
|
||||
width={44}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'var(--surface)',
|
||||
|
|
@ -297,87 +383,94 @@ export default function RecoveryDashboardOverview({
|
|||
)
|
||||
}
|
||||
|
||||
const renderVitalsHistory = () => {
|
||||
/** Nur Fließtext Einordnung Vital & Belastung (für Block „Einschätzungen“). */
|
||||
const renderVitalBelastungNarrative = () => {
|
||||
const vh = vitalsHistory
|
||||
if (!vh || vh.metadata?.confidence === 'insufficient') return null
|
||||
const paragraphs = vh.analytics?.consolidated_paragraphs || []
|
||||
const bullets = vh.analytics?.bullets || []
|
||||
const showParagraphs = paragraphs.length > 0
|
||||
const showBulletsFallback = !showParagraphs && bullets.length > 0
|
||||
if (!showParagraphs && !showBulletsFallback) return null
|
||||
return (
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
{showParagraphs ? (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '12px 14px',
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: '4px solid var(--accent)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: 12,
|
||||
color: 'var(--text2)',
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{paragraphs.map((text, i) => (
|
||||
<p key={i} style={{ margin: i === 0 ? 0 : '10px 0 0' }}>
|
||||
{text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{showBulletsFallback ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{bullets.map((b) => (
|
||||
<div
|
||||
key={b.key}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: `4px solid ${insightBulletStripe(b.tone)}`,
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{b.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.45 }}>{b.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** VO2 / SpO2 / Atemfrequenz — Verlauf mit Zonen-Hinweis aus Snapshot; kein Ruhepuls/HRV (siehe kombiniertes Diagramm). */
|
||||
const renderWeitereVitalVerlaeufe = (vitalItemsByKey) => {
|
||||
const vh = vitalsHistory
|
||||
if (!vh) {
|
||||
return <div style={{ padding: 16, fontSize: 12, color: 'var(--text3)' }}>Keine Verlaufs-Daten (Bundle).</div>
|
||||
return <div style={{ padding: 12, fontSize: 12, color: 'var(--text3)' }}>Keine Verlaufs-Daten (Bundle).</div>
|
||||
}
|
||||
if (vh.metadata?.confidence === 'insufficient') {
|
||||
return (
|
||||
<div style={{ padding: 16, fontSize: 12, color: 'var(--text3)', lineHeight: 1.5 }}>
|
||||
<div style={{ padding: 12, fontSize: 12, color: 'var(--text3)', lineHeight: 1.5 }}>
|
||||
{vh.metadata?.message || 'Zu wenige Vitaldaten im gewählten Fenster für Verläufe.'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const series = vh.series || {}
|
||||
const keys = Object.keys(series)
|
||||
const paragraphs = vh.analytics?.consolidated_paragraphs || []
|
||||
const bullets = vh.analytics?.bullets || []
|
||||
const showParagraphs = paragraphs.length > 0
|
||||
const showBulletsFallback = !showParagraphs && bullets.length > 0
|
||||
const keys = VITAL_TREND_ONLY_KEYS.filter((k) => series[k]?.points?.length)
|
||||
if (keys.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: 8, fontSize: 12, color: 'var(--text3)' }}>
|
||||
Keine zusätzlichen Vital-Verläufe (VO2max, SpO2, Atemfrequenz) im Fenster — oder nur Ruhepuls/HRV erfasst.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', minWidth: 0 }}>
|
||||
{showParagraphs ? (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>
|
||||
Einordnung (Vital & Belastung)
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '12px 14px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: 12,
|
||||
color: 'var(--text2)',
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{paragraphs.map((text, i) => (
|
||||
<p key={i} style={{ margin: i === 0 ? 0 : '10px 0 0' }}>
|
||||
{text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showBulletsFallback ? (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>
|
||||
Einordnung (Vital & Belastung)
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{bullets.map((b) => (
|
||||
<div
|
||||
key={b.key}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: `4px solid ${insightBulletStripe(b.tone)}`,
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{b.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.45 }}>{b.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 8 }}>
|
||||
Je Kennzahl eigene Skala (physische Einheit). Gestrichelt: gleitender Mittelwert (max. 7 aufeinanderfolgende
|
||||
Messungen), glättet starke Einzelschwankungen.
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 10 }}>
|
||||
Gestrichelte Linie: gleitender Mittelwert (max. 7 aufeinanderfolgende Messungen). Y-Achse auf den Datenbereich
|
||||
begrenzt.
|
||||
</div>
|
||||
|
||||
{keys.map((k) => {
|
||||
const m = series[k]
|
||||
const pts = m.points || []
|
||||
const maPts = m.points_ma7 || []
|
||||
if (pts.length === 0) return null
|
||||
const zoneItem = vitalItemsByKey[k]
|
||||
const chartData = pts.map((p, i) => ({
|
||||
...p,
|
||||
d: fmtDate(p.date),
|
||||
|
|
@ -391,23 +484,22 @@ export default function RecoveryDashboardOverview({
|
|||
const mn = Math.min(...vals)
|
||||
const mx = Math.max(...vals)
|
||||
const span = mx - mn
|
||||
const pad = span < 1e-9 ? Math.max(Math.abs(mn) * 0.05, 0.5) : span * 0.12
|
||||
const pad = span < 1e-9 ? Math.max(Math.abs(mn) * 0.05, 0.5) : Math.max(span * 0.12, 0.01)
|
||||
const hasMa = maPts.length > 0 && maPts.some((x) => x?.value != null)
|
||||
|
||||
return (
|
||||
<div key={k} style={{ marginBottom: 16, width: '100%' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text2)', marginBottom: 4 }}>
|
||||
<div key={k} style={{ marginBottom: 18, width: '100%' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text2)', marginBottom: 6 }}>
|
||||
{m.label_de} ({m.unit})
|
||||
{m.n != null ? (
|
||||
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> · n = {m.n}</span>
|
||||
) : null}
|
||||
{m.n != null ? <span style={{ fontWeight: 400, color: 'var(--text3)' }}> · n = {m.n}</span> : null}
|
||||
{m.mean != null ? (
|
||||
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> · Ø {m.mean}</span>
|
||||
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> · Ø {formatAxisTick(m.mean)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<VitalZoneHint item={zoneItem} />
|
||||
{pts.length === 1 ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)', padding: '8px 0' }}>
|
||||
Ein Messpunkt ({m.last}) — weiter erfassen, um einen Verlauf zu sehen.
|
||||
Ein Messpunkt ({formatAxisTick(m.last)}) — weiter erfassen, um einen Verlauf zu sehen.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: hasMa ? 220 : 200, minHeight: hasMa ? 220 : 200 }}>
|
||||
|
|
@ -418,7 +510,9 @@ export default function RecoveryDashboardOverview({
|
|||
<YAxis
|
||||
domain={[mn - pad, mx + pad]}
|
||||
tick={{ fontSize: 9, fill: 'var(--text3)' }}
|
||||
width={44}
|
||||
tickFormatter={formatAxisTick}
|
||||
tickCount={6}
|
||||
width={48}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
|
|
@ -427,6 +521,7 @@ export default function RecoveryDashboardOverview({
|
|||
borderRadius: 8,
|
||||
fontSize: 11,
|
||||
}}
|
||||
formatter={(value) => (value != null ? formatAxisTick(value) : '')}
|
||||
/>
|
||||
{hasMa ? <Legend wrapperStyle={{ fontSize: 10, paddingTop: 4 }} /> : null}
|
||||
<Line
|
||||
|
|
@ -457,10 +552,6 @@ export default function RecoveryDashboardOverview({
|
|||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<div style={{ padding: 12, fontSize: 12, color: 'var(--text3)' }}>Keine Vital-Zeitreihen im Fenster.</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -487,17 +578,9 @@ export default function RecoveryDashboardOverview({
|
|||
const vitDate = meta.vitals_measured_at
|
||||
const bpDate = meta.blood_pressure_measured_at
|
||||
const disclaimer = meta.disclaimer_de
|
||||
const hasRhrCard = items.some((it) => it.key === 'resting_hr')
|
||||
const hasHrvCard = items.some((it) => it.key === 'hrv')
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.length > 0 && !hasRhrCard && !hasHrvCard ? (
|
||||
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 10 }}>
|
||||
Ruhepuls und HRV sind in diesem Bereich bewusst nicht noch einmal als Zonen-Karten geführt — die Einordnung
|
||||
steht oben im Vital-Verlauf und in der KPI-Kachel „Herz & autonomes System“.
|
||||
</p>
|
||||
) : null}
|
||||
{items.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 14 }}>
|
||||
{items.map((it) => {
|
||||
|
|
@ -597,44 +680,90 @@ export default function RecoveryDashboardOverview({
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 10 }}>
|
||||
Auswertung aus dem Recovery-Data-Layer (Issue 53). Fenster ca. <strong>{eff}</strong> Tage · Charts{' '}
|
||||
<strong>{cDays}</strong> Tage · Vital-Matrix <strong>{vDays}</strong> Tage.
|
||||
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 12 }}>
|
||||
Daten-Layer Auswertung · Fenster ca. <strong>{eff}</strong> Tage · Chart-Horizont <strong>{cDays}</strong> Tage ·
|
||||
Vital-Snapshot <strong>{vDays}</strong> Tage.
|
||||
</p>
|
||||
|
||||
<KpiTilesOverview tiles={kpiTiles} heading="Kennzahlen" />
|
||||
<KpiTilesOverview tiles={kpiTiles} heading="Kennzahlen" marginBottom={16} />
|
||||
|
||||
{insights.length > 0 ? (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
{insights.length > 0 || showVitalNarrativeBlock ? (
|
||||
<div style={{ marginBottom: 18 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>Einschätzungen</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{insights.map((ins) => (
|
||||
<div
|
||||
key={ins.key}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: `4px solid ${getStatusColor(['good', 'warn', 'bad'].includes(ins.tone) ? ins.tone : 'warn')}`,
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{ins.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.45 }}>{ins.body}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{insights.map((ins) => {
|
||||
const t = ['good', 'warn', 'bad'].includes(ins.tone) ? ins.tone : 'warn'
|
||||
return (
|
||||
<div
|
||||
key={ins.key}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderLeft: `4px solid ${getStatusColor(t)}`,
|
||||
background: getStatusBg(t),
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{ins.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', lineHeight: 1.45 }}>{ins.body}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{showVitalNarrativeBlock ? (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>
|
||||
Vitalverlauf & Belastung (Text)
|
||||
</div>
|
||||
{renderVitalBelastungNarrative()}
|
||||
</div>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8, marginTop: 4 }}>Diagramme</div>
|
||||
<SectionHeading
|
||||
compactTop
|
||||
title="Schlaf & Erholung"
|
||||
hint="Recovery-Score und Schlaf im gleichen Zeitraum wie die Kennzahlen oben."
|
||||
/>
|
||||
<ChartCard
|
||||
title="Recovery Score"
|
||||
description="0–100, Verlauf im Chart-Fenster. Höher ist in der Regel günstiger."
|
||||
>
|
||||
{renderRecoveryScore()}
|
||||
</ChartCard>
|
||||
<ChartCard title="Schlaf: Dauer & Qualität" description="Dauer (h) und Qualitätsanteil (%) — eigene Achsen.">
|
||||
{renderSleepQuality()}
|
||||
</ChartCard>
|
||||
<ChartCard title="Schlafschuld" description="Kumulierte Differenz zur Zielschlafdauer.">
|
||||
{renderSleepDebt()}
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard title="📊 Recovery Score">{renderRecoveryScore()}</ChartCard>
|
||||
<ChartCard title="📊 HRV & Ruhepuls">{renderHrvRhr()}</ChartCard>
|
||||
<ChartCard title="📈 Vitalwerte — Verlauf (je Kennzahl)">{renderVitalsHistory()}</ChartCard>
|
||||
<ChartCard title="📊 Schlaf: Dauer & Qualität">{renderSleepQuality()}</ChartCard>
|
||||
<ChartCard title="📊 Schlafschuld">{renderSleepDebt()}</ChartCard>
|
||||
<ChartCard title="📋 Vitalwerte — aktuelle Einordnung">{renderVitalSigns()}</ChartCard>
|
||||
<SectionHeading
|
||||
title="Herz & Kreislauf"
|
||||
hint="Ruhepuls und HRV nur hier im kombinierten Verlauf — keine zweite Darstellung darunter."
|
||||
/>
|
||||
<ChartCard
|
||||
title="HRV & Ruhepuls"
|
||||
description="Zwei Y-Achsen: HRV (ms, links), Ruhepuls (bpm, rechts). Gleiche Tage wie oben."
|
||||
>
|
||||
{renderHrvRhr()}
|
||||
</ChartCard>
|
||||
|
||||
<SectionHeading
|
||||
title="Weitere Vitalparameter (Verlauf)"
|
||||
hint="VO2max, SpO2 und Atemfrequenz mit Zonen-Einordnung zum letzten Snapshot (farbig unter dem Titel)."
|
||||
/>
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>Verläufe</div>
|
||||
{renderWeitereVitalVerlaeufe(vitalItemsByKey)}
|
||||
</div>
|
||||
|
||||
<SectionHeading
|
||||
title="Aktuelle Messwerte & Zonen"
|
||||
hint="Neueste Baseline-Vitals und Blutdruck im Fenster — gleiche Logik wie Vital-Seite (keine Diagnose)."
|
||||
/>
|
||||
<ChartCard title="Snapshot & Einordnung">{renderVitalSigns()}</ChartCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user