feat: enhance KPI tiles with contextual hints and improve chart legends
- 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:
parent
31fbf33031
commit
fc816da335
|
|
@ -103,6 +103,7 @@ def build_nutrition_history_kpi_tiles(
|
||||||
"sublabel": "Referenzgewicht fehlt",
|
"sublabel": "Referenzgewicht fehlt",
|
||||||
"status": "warn",
|
"status": "warn",
|
||||||
"verdict": _verdict("warn"),
|
"verdict": _verdict("warn"),
|
||||||
|
"hint": "Ohne aktuelles Körpergewicht lässt sich das Protein-Ziel (g/kg) nicht bewerten.",
|
||||||
"hoverTop": "Protein-Ziel nicht berechenbar",
|
"hoverTop": "Protein-Ziel nicht berechenbar",
|
||||||
"hoverBody": "Für 1,6–2,2 g/kg wird ein aktuelles Körpergewicht benötigt.",
|
"hoverBody": "Für 1,6–2,2 g/kg wird ein aktuelles Körpergewicht benötigt.",
|
||||||
"keys": ["protein_adequacy"],
|
"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)",
|
"sublabel": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||||||
"status": "bad",
|
"status": "bad",
|
||||||
"verdict": _verdict("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)",
|
"hoverTop": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)",
|
||||||
"hoverBody": (
|
"hoverBody": (
|
||||||
f"1,6–2,2g/kg KG. Fehlend: ~{miss}g täglich. "
|
f"1,6–2,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",
|
"sublabel": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
||||||
"status": "warn",
|
"status": "warn",
|
||||||
"verdict": _verdict("warn"),
|
"verdict": _verdict("warn"),
|
||||||
|
"hint": (
|
||||||
|
f"Viele Kalorien kommen aus KH/Fett; Proteinanteil oft sinnvoll bei 25–35 % "
|
||||||
|
f"(aktuell P {prot_pct} % / KH {kh_pct} % / F {fat_pct} %)."
|
||||||
|
),
|
||||||
"hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
"hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien",
|
||||||
"hoverBody": (
|
"hoverBody": (
|
||||||
f"Empfehlung oft 25–35%. Aktuell: {prot_pct}% P / {kh_pct}% KH / {fat_pct}% F"
|
f"Empfehlung oft 25–35%. Aktuell: {prot_pct}% P / {kh_pct}% KH / {fat_pct}% F"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export function buildKpiTileTitleString(t) {
|
||||||
* - `value` (ReactNode) — Hauptwert
|
* - `value` (ReactNode) — Hauptwert
|
||||||
* - `status` — für Farbstreifen: `good` | `warn` | `bad`
|
* - `status` — für Farbstreifen: `good` | `warn` | `bad`
|
||||||
* - optional: `icon`, `sublabel`, `verdict`, `valueColor`, `hoverTop`, `hoverBody`, `keys`
|
* - 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({
|
export default function KpiTilesOverview({
|
||||||
tiles,
|
tiles,
|
||||||
|
|
@ -82,6 +83,7 @@ export default function KpiTilesOverview({
|
||||||
{tiles.map(t => {
|
{tiles.map(t => {
|
||||||
const accent = getStatusColor(t.status)
|
const accent = getStatusColor(t.status)
|
||||||
const tip = buildKpiTileTitleString(t)
|
const tip = buildKpiTileTitleString(t)
|
||||||
|
const cardHint = t.hint ? String(t.hint) : null
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={t.key}
|
key={t.key}
|
||||||
|
|
@ -119,6 +121,23 @@ export default function KpiTilesOverview({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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). */
|
/** Kalorien (Ø 7T) vs. Gewicht — Daten aus Layer-2b-Bundle (nutrition_metrics / TDEE wie Data Layer). */
|
||||||
function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffDate, allTime }) {
|
function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffDate, allTime }) {
|
||||||
if (vizKcalWeight?.points?.length >= 5) {
|
if (vizKcalWeight?.points?.length >= 5) {
|
||||||
|
|
@ -716,7 +780,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
||||||
Kalorien (Ø 7 Tage) vs. Gewicht
|
Kalorien (Ø 7 Tage) vs. Gewicht
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 8 }}>
|
<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>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
<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" />
|
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</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
|
{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`}
|
: `Keine TDEE-Referenz (Gewicht/Demografie) · ${n} gemeinsame Tage`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -765,7 +830,7 @@ function KcalVsWeightChart({ vizKcalWeight, corrData: corrRows, profile, cutoffD
|
||||||
Kalorien (Ø 7 Tage) vs. Gewicht
|
Kalorien (Ø 7 Tage) vs. Gewicht
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 8 }}>
|
<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>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<LineChart data={kcalVsW} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
<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" />
|
<Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#2563EB" strokeWidth={2.5} dot={{ r: 2, fill: '#2563EB' }} name="weight" />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 6 }}>
|
<KcalVsWeightLegend showTdee />
|
||||||
Referenz TDEE ~{tdee} kcal (Fallback Mifflin ×1,4) · {raw.length} gemeinsame Tage
|
<div style={{ fontSize: 10, color: 'var(--text3)', textAlign: 'center', marginTop: 8 }}>
|
||||||
|
TDEE ~{tdee} kcal (Fallback Mifflin ×1,4) · {raw.length} gemeinsame Tage
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user