refactor: update nutrition chart colors and enhance layout
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- Changed color codes for macro nutrients in the nutrition interpretation and metrics files to improve visual consistency.
- Added new CSS styles for uniform chart height and layout adjustments in the frontend components, enhancing the overall user experience.
- Refactored the NutritionCharts component to utilize the new macro chart theme for better maintainability and readability.
This commit is contained in:
Lars 2026-04-19 17:28:41 +02:00
parent b96b1931db
commit 31fbf33031
7 changed files with 131 additions and 59 deletions

View File

@ -174,7 +174,7 @@ def build_macro_donut_from_averages(navg: Dict[str, Any]) -> Optional[List[Dict[
if tot <= 0:
return None
return [
{"name": "Protein", "value": round(pkcal / tot * 100), "color": "#059669", "grams": round(p, 1)},
{"name": "KH", "value": round(ckcal / tot * 100), "color": "#EA580C", "grams": round(c, 1)},
{"name": "Fett", "value": round(fkcal / tot * 100), "color": "#2563EB", "grams": round(f, 1)},
{"name": "Protein", "value": round(pkcal / tot * 100), "color": "#4a8f72", "grams": round(p, 1)},
{"name": "KH", "value": round(ckcal / tot * 100), "color": "#c17d45", "grams": round(c, 1)},
{"name": "Fett", "value": round(fkcal / tot * 100), "color": "#6e8eb8", "grams": round(f, 1)},
]

View File

@ -668,19 +668,19 @@ def get_weekly_macro_distribution_chart_data(profile_id: str, weeks: int) -> Dic
{
"label": "Protein (%)",
"data": protein_pcts,
"backgroundColor": "#1D9E75",
"backgroundColor": "#4a8f72",
"stack": "macro",
},
{
"label": "Kohlenhydrate (%)",
"data": carbs_pcts,
"backgroundColor": "#F59E0B",
"backgroundColor": "#c17d45",
"stack": "macro",
},
{
"label": "Fett (%)",
"data": fat_pcts,
"backgroundColor": "#EF4444",
"backgroundColor": "#6e8eb8",
"stack": "macro",
},
],

View File

@ -368,6 +368,35 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
min-width: 0;
}
/* Einheitliche Chart-Höhe (Donut-Bereich ≈ E3-Balken) */
.nutrition-macro-pair__chart-wrap {
width: 100%;
min-height: 260px;
}
.nutrition-macro-pair__donut-inner {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.nutrition-macro-pair__donut-chart {
width: 100%;
min-height: 260px;
}
.nutrition-macro-pair__legend {
width: 100%;
padding-top: 2px;
}
.nutrition-macro-pair .card.nutrition-macro-pair__donut,
.nutrition-macro-pair .card.nutrition-macro-pair__weekly {
display: flex;
flex-direction: column;
}
.history-page__title {
margin-bottom: 12px;
}

View File

@ -5,6 +5,7 @@ import {
ComposedChart, ReferenceArea,
} from 'recharts'
import { api } from '../utils/api'
import { MACRO_CHART, NUTRITION_MACRO_CHART_BLOCK_PX } from '../utils/macroChartTheme'
import dayjs from 'dayjs'
const fmtDate = d => dayjs(d).format('DD.MM')
@ -172,26 +173,28 @@ export function WeeklyMacroDistributionPanel({ macroWeeklyData, loading, error }
Anteil der Kalorien aus jedem Makronährstoff pro Kalenderwoche (100&nbsp;% gestapelt). Gut vergleichbar mit der
Donut-Übersicht links.
</div>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={chartData} margin={{ top: 8, right: 4, bottom: 4, left: -18 }} barCategoryGap="18%">
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="week"
tick={{ fontSize: 9, fill: 'var(--text3)' }}
tickLine={false}
interval={Math.max(0, Math.floor(chartData.length / 8) - 1)}
/>
<YAxis tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={[0, 100]} ticks={[0, 25, 50, 75, 100]} />
<Tooltip
contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 11 }}
formatter={(v, name) => [`${v}%`, name]}
/>
<Legend wrapperStyle={{ fontSize: 10, paddingTop: 8 }} />
<Bar dataKey="protein" stackId="a" fill="#059669" name="Protein %" radius={[0, 0, 0, 0]} />
<Bar dataKey="carbs" stackId="a" fill="#EA580C" name="KH %" radius={[0, 0, 0, 0]} />
<Bar dataKey="fat" stackId="a" fill="#B91C1C" name="Fett %" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<div className="nutrition-macro-pair__chart-wrap">
<ResponsiveContainer width="100%" height={NUTRITION_MACRO_CHART_BLOCK_PX}>
<BarChart data={chartData} margin={{ top: 8, right: 4, bottom: 4, left: -18 }} barCategoryGap="18%">
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="week"
tick={{ fontSize: 9, fill: 'var(--text3)' }}
tickLine={false}
interval={Math.max(0, Math.floor(chartData.length / 8) - 1)}
/>
<YAxis tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} domain={[0, 100]} ticks={[0, 25, 50, 75, 100]} />
<Tooltip
contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 11 }}
formatter={(v, name) => [`${v}%`, name]}
/>
<Legend wrapperStyle={{ fontSize: 10, paddingTop: 8 }} />
<Bar dataKey="protein" stackId="a" fill={MACRO_CHART.protein} name="Protein %" radius={[0, 0, 0, 0]} />
<Bar dataKey="fat" stackId="a" fill={MACRO_CHART.fat} name="Fett %" radius={[0, 0, 0, 0]} />
<Bar dataKey="carbs" stackId="a" fill={MACRO_CHART.carbs} name="KH %" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div style={{ marginTop: 10, fontSize: 10, color: 'var(--text3)', textAlign: 'center', lineHeight: 1.5 }}>
Ø Verteilung: P {meta.avg_protein_pct}% · KH {meta.avg_carbs_pct}% · F {meta.avg_fat_pct}% · Variabilität (CV): P{' '}
{meta.protein_cv}% · KH {meta.carbs_cv}% · F {meta.fat_cv}%

View File

@ -11,6 +11,7 @@ import { api } from '../utils/api'
import { photoMonthKey, photoSortKey, formatPhotoCaption } from '../utils/photoDisplay'
import { getBfCategory } from '../utils/calc'
import { getStatusColor, getStatusBg } from '../utils/interpret'
import { MACRO_CHART, macroFillByName, NUTRITION_MACRO_CHART_BLOCK_PX } from '../utils/macroChartTheme'
import Markdown from '../utils/Markdown'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import NutritionCharts, { WeeklyMacroDistributionPanel } from '../components/NutritionCharts'
@ -909,18 +910,18 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct
<XAxis dataKey="date" tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} interval={Math.max(0, Math.floor(cdMacro.length / 6) - 1)} />
<YAxis tick={{ fontSize: 9, fill: 'var(--text3)' }} tickLine={false} />
{ptLow > 0 && (
<ReferenceLine y={ptLow} stroke="#059669" strokeDasharray="5 4" strokeWidth={2} label={{ value: `${ptLow}g P`, fontSize: 9, fill: '#059669', position: 'insideTopRight' }} />
<ReferenceLine y={ptLow} stroke={MACRO_CHART.protein} strokeDasharray="5 4" strokeWidth={2} label={{ value: `${ptLow}g P`, fontSize: 9, fill: MACRO_CHART.protein, position: 'insideTopRight' }} />
)}
<Tooltip contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 11 }} formatter={(v, name) => [`${v}g`, name]} />
<Bar dataKey="Fett" stackId="a" fill="#93C5FD" name="Fett" />
<Bar dataKey="KH" stackId="a" fill="#FDBA74" name="KH" />
<Bar dataKey="Protein" stackId="a" fill="#059669" name="Protein" radius={[4, 4, 0, 0]} />
<Bar dataKey="Protein" stackId="a" fill={MACRO_CHART.protein} name="Protein" />
<Bar dataKey="Fett" stackId="a" fill={MACRO_CHART.fat} name="Fett" />
<Bar dataKey="KH" stackId="a" fill={MACRO_CHART.carbs} name="KH" radius={[5, 5, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', marginTop: 8, fontSize: 10, color: 'var(--text3)', flexWrap: 'wrap' }}>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: '#059669', borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />Protein (oben)</span>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: '#FDBA74', borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />KH</span>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: '#93C5FD', borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />Fett</span>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: MACRO_CHART.protein, borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />Protein (unten)</span>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: MACRO_CHART.fat, borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />Fett (Mitte)</span>
<span><span style={{ display: 'inline-block', width: 10, height: 10, background: MACRO_CHART.carbs, borderRadius: 2, verticalAlign: 'middle', marginRight: 4 }} />KH (oben)</span>
</div>
</div>
@ -929,35 +930,52 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>
Ø Makro-Quote ({n} Tage)
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
{pieData.length > 0 ? (
<>
<PieChart width={120} height={120}>
<Pie data={pieData} cx={58} cy={58} innerRadius={36} outerRadius={54} dataKey="value" startAngle={90} endAngle={-270}>
{pieData.map((e, i) => <Cell key={i} fill={e.color} />)}
</Pie>
<Tooltip formatter={(v, name) => [`${v}%`, name]} />
</PieChart>
<div style={{ flex: 1, minWidth: 160 }}>
{pieData.map(p => (
<div key={p.name} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<div style={{ width: 10, height: 10, borderRadius: 2, background: p.color, flexShrink: 0 }} />
{pieData.length > 0 ? (
<div className="nutrition-macro-pair__donut-inner">
<div className="nutrition-macro-pair__donut-chart">
<ResponsiveContainer width="100%" height={NUTRITION_MACRO_CHART_BLOCK_PX}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius="38%"
outerRadius="58%"
dataKey="value"
startAngle={90}
endAngle={-270}
paddingAngle={1}
>
{pieData.map((e, i) => (
<Cell key={i} fill={macroFillByName(e.name)} />
))}
</Pie>
<Tooltip formatter={(v, name) => [`${v}%`, name]} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="nutrition-macro-pair__legend">
{pieData.map(p => {
const fill = macroFillByName(p.name)
return (
<div key={p.name} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div style={{ width: 10, height: 10, borderRadius: 2, background: fill, flexShrink: 0 }} />
<div style={{ flex: 1, fontSize: 13 }}>{p.name}</div>
<div style={{ fontSize: 13, fontWeight: 600, color: p.color }}>{p.value}%</div>
<div style={{ fontSize: 13, fontWeight: 600, color: fill }}>{p.value}%</div>
<div style={{ fontSize: 11, color: 'var(--text3)' }}>
{p.grams != null ? `${p.grams}g` : '—'}
</div>
</div>
))}
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--text3)', borderTop: '1px solid var(--border)', paddingTop: 8 }}>
Ø {avgKcal} kcal/Tag · Anteil der Makro-Kalorien am Tagesumsatz
</div>
)
})}
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--text3)', borderTop: '1px solid var(--border)', paddingTop: 8 }}>
Ø {avgKcal} kcal/Tag · Anteil der Makro-Kalorien am Tagesumsatz
</div>
</>
) : (
<div style={{ fontSize: 12, color: 'var(--text3)' }}>Keine Makro-Mittelwerte im Zeitraum.</div>
)}
</div>
</div>
</div>
) : (
<div style={{ fontSize: 12, color: 'var(--text3)' }}>Keine Makro-Mittelwerte im Zeitraum.</div>
)}
</div>
<div className="card nutrition-macro-pair__weekly">
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 4 }}>

View File

@ -5,6 +5,7 @@ import {
ResponsiveContainer, CartesianGrid, Legend, ReferenceLine, ScatterChart, Scatter
} from 'recharts'
import { api as nutritionApi } from '../utils/api'
import { MACRO_CHART } from '../utils/macroChartTheme'
import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek'
dayjs.extend(isoWeek)
@ -709,9 +710,9 @@ function WeeklyMacros({ weekly }) {
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
formatter={(v,n)=>[`${Math.round(v)} g`, n]}/>
<Legend wrapperStyle={{fontSize:11}}/>
<Bar dataKey="Protein" stackId="a" fill="#1D9E75"/>
<Bar dataKey="Fett" stackId="a" fill="#378ADD"/>
<Bar dataKey="Kohlenhydrate" stackId="a" fill="#D4537E" radius={[3,3,0,0]}/>
<Bar dataKey="Protein" stackId="a" fill={MACRO_CHART.protein}/>
<Bar dataKey="Fett" stackId="a" fill={MACRO_CHART.fat}/>
<Bar dataKey="Kohlenhydrate" stackId="a" fill={MACRO_CHART.carbs} radius={[3,3,0,0]}/>
</BarChart>
</ResponsiveContainer>
)

View File

@ -0,0 +1,21 @@
/**
* Einheitliche Makro-Farben für Verlauf (Balken, Donut, E3).
* Reihenfolge gestapelter Balken (Recharts, unten zuerst): Protein Fett Kohlenhydrate.
*/
export const MACRO_CHART = {
protein: '#4a8f72',
fat: '#6e8eb8',
carbs: '#c17d45',
}
/** Einheitliche Höhe Donut-Bereich / E3-Balken (Verlauf) */
export const NUTRITION_MACRO_CHART_BLOCK_PX = 260
/** Farbe nach Segment-Name (Protein / KH / Fett / englische Keys). */
export function macroFillByName(name) {
const n = String(name || '').toLowerCase()
if (n.includes('protein') || n === 'p') return MACRO_CHART.protein
if (n.includes('fett') || n.includes('fat')) return MACRO_CHART.fat
if (n.includes('kh') || n.includes('kohlenhydrat') || n.includes('carb')) return MACRO_CHART.carbs
return MACRO_CHART.carbs
}