feat: enhance KPI tiles with contextual hints and improve chart legends
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Added contextual hints to KPI tiles in the nutrition interpretation to provide users with actionable insights regarding protein intake and weight assessment.
- Updated the KpiTilesOverview component to display these hints, improving user understanding of nutrition metrics.
- Introduced a new KcalVsWeightLegend component to clarify chart data representation, enhancing the overall user experience in the history visualization.
This commit is contained in:
Lars 2026-04-19 17:36:45 +02:00
parent 31fbf33031
commit fc816da335
3 changed files with 100 additions and 6 deletions

View File

@ -103,6 +103,7 @@ def build_nutrition_history_kpi_tiles(
"sublabel": "Referenzgewicht fehlt",
"status": "warn",
"verdict": _verdict("warn"),
"hint": "Ohne aktuelles Körpergewicht lässt sich das Protein-Ziel (g/kg) nicht bewerten.",
"hoverTop": "Protein-Ziel nicht berechenbar",
"hoverBody": "Für 1,62,2 g/kg wird ein aktuelles Körpergewicht benötigt.",
"keys": ["protein_adequacy"],
@ -119,6 +120,10 @@ def build_nutrition_history_kpi_tiles(
"sublabel": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}{pt_high}g)",
"status": "bad",
"verdict": _verdict("bad"),
"hint": (
f"Es fehlen rund {miss} g Protein pro Tag bei Kaloriendefizit "
"steigt das Risiko für Muskelerhalt."
),
"hoverTop": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}{pt_high}g)",
"hoverBody": (
f"1,62,2g/kg KG. Fehlend: ~{miss}g täglich. "
@ -153,6 +158,10 @@ def build_nutrition_history_kpi_tiles(
"sublabel": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
"status": "warn",
"verdict": _verdict("warn"),
"hint": (
f"Viele Kalorien kommen aus KH/Fett; Proteinanteil oft sinnvoll bei 2535 % "
f"(aktuell P {prot_pct} % / KH {kh_pct} % / F {fat_pct} %)."
),
"hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
"hoverBody": (
f"Empfehlung oft 2535%. Aktuell: {prot_pct}% P / {kh_pct}% KH / {fat_pct}% F"

View File

@ -28,6 +28,7 @@ export function buildKpiTileTitleString(t) {
* - `value` (ReactNode) Hauptwert
* - `status` für Farbstreifen: `good` | `warn` | `bad`
* - optional: `icon`, `sublabel`, `verdict`, `valueColor`, `hoverTop`, `hoverBody`, `keys`
* - optional: `hint` Kurz-Hinweis/Warnung direkt auf der Kachel (z. B. Ernährung bei warn/bad)
*/
export default function KpiTilesOverview({
tiles,
@ -82,6 +83,7 @@ export default function KpiTilesOverview({
{tiles.map(t => {
const accent = getStatusColor(t.status)
const tip = buildKpiTileTitleString(t)
const cardHint = t.hint ? String(t.hint) : null
return (
<div
key={t.key}
@ -119,6 +121,23 @@ export default function KpiTilesOverview({
</div>
) : null}
</div>
{cardHint ? (
<div
className="kpi-tiles-card__hint"
style={{
marginTop: 10,
padding: '8px 10px',
borderRadius: 8,
fontSize: 10,
lineHeight: 1.45,
color: 'var(--text2)',
background: 'var(--surface2)',
borderLeft: `3px solid ${accent}`,
}}
>
{cardHint}
</div>
) : null}
</div>
)
})}

View File

@ -700,6 +700,70 @@ function BodySection({ profile, insights, onRequest, loadingSlug, filterActiveSl
)
}
/** Legende unter dem Chart: Linien + ggf. TDEE-Referenz (gestrichelt). */
function KcalVsWeightLegend({ showTdee }) {
const line = (color) => ({
display: 'inline-block',
width: 22,
height: 3,
background: color,
borderRadius: 1,
verticalAlign: 'middle',
marginRight: 6,
})
return (
<div
className="kcal-vs-weight-legend"
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
gap: '12px 18px',
marginTop: 10,
fontSize: 10,
color: 'var(--text2)',
lineHeight: 1.35,
}}
>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<span style={line('#EA580C')} />
Ø Kalorien (7-Tage-Mittel)
</span>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<span
style={{
display: 'inline-block',
width: 9,
height: 9,
borderRadius: '50%',
background: '#2563EB',
marginRight: 6,
verticalAlign: 'middle',
}}
/>
Gewicht (kg)
</span>
{showTdee ? (
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<span
style={{
display: 'inline-block',
width: 22,
height: 0,
verticalAlign: 'middle',
marginRight: 6,
borderTop: '2px dashed #EA580C',
opacity: 0.95,
}}
/>
TDEE-Referenz (geschätzt)
</span>
) : null}
</div>
)
}
/** Kalorien (Ø 7T) vs. Gewicht — Daten aus Layer-2b-Bundle (nutrition_metrics / TDEE wie Data Layer). */
function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffDate, allTime }) {
if (vizKcalWeight?.points?.length >= 5) {
@ -716,7 +780,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
Kalorien (Ø 7 Tage) vs. Gewicht
</div>
<div style={{ fontSize: 10, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 8 }}>
Gleitender 7-Tage-Mittelwert der Kalorien vs. tägliches Gewicht (gemeinsame Tage). Orange: kcal · Blau: Gewicht.
Nur Tage mit Kalorien- und Gewichtsdaten. Linke Achse: kcal (Ø 7 Tage), rechte Achse: kg.
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
@ -735,9 +799,10 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
</LineChart>
</ResponsiveContainer>
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 6 }}>
<KcalVsWeightLegend showTdee={tdeeLabel != null} />
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 8 }}>
{tdeeLabel != null
? `Referenz TDEE ~${tdeeLabel} kcal (Data Layer, gestrichelt) · ${n} gemeinsame Tage`
? `TDEE ~${tdeeLabel} kcal · ${n} gemeinsame Tage`
: `Keine TDEE-Referenz (Gewicht/Demografie) · ${n} gemeinsame Tage`}
</div>
</div>
@ -765,7 +830,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
Kalorien (Ø 7 Tage) vs. Gewicht
</div>
<div style={{ fontSize: 10, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 8 }}>
Gleitender 7-Tage-Mittelwert der Kalorien vs. tägliches Gewicht (gemeinsame Tage). Orange: kcal · Blau: Gewicht.
Nur Tage mit Kalorien- und Gewichtsdaten. Linke Achse: kcal (Ø 7 Tage), rechte Achse: kg.
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
@ -782,8 +847,9 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
</LineChart>
</ResponsiveContainer>
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 6 }}>
Referenz TDEE ~{tdee} kcal (Fallback Mifflin ×1,4) · {raw.length} gemeinsame Tage
<KcalVsWeightLegend showTdee />
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 8 }}>
TDEE ~{tdee} kcal (Fallback Mifflin ×1,4) · {raw.length} gemeinsame Tage
</div>
</div>
)