mitai-jinkendo/frontend/src/components/TrendKcalWeightChart.jsx
Lars 3d498d03c1
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat: Enhance dashboard widget configuration and introduce new widgets
- Updated the dashboard layout schema to include new widgets: DashboardGreeting, QuickWeightToday, BodyStatStrip, StatusPills, ProfileGoalsProgress, TrendKcalWeight, NutritionActivitySummary, RecoverySleepRest, and TrainingTypeDistribution.
- Improved widget configuration validation to support new features, including chart days for trend and distribution widgets.
- Refactored the default lab layout to align with the updated widget catalog and ensure proper default activation.
- Bumped app_dashboard version to 1.6.0 to reflect the addition of new widgets and configuration enhancements.
2026-04-07 14:19:45 +02:00

184 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
} from 'recharts'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
function rollingAvg(arr, key, w = 7) {
return arr.map((d, i) => {
const s = arr
.slice(Math.max(0, i - w + 1), i + 1)
.map((x) => x[key])
.filter((v) => v != null)
return s.length
? {
...d,
[`${key}_avg`]: Math.round((s.reduce((a, b) => a + b) / s.length) * 10) / 10,
}
: d
})
}
/**
* Kalorien + Gewicht im Zeitfenster (wie Dashboard-Trends).
* @param {{ weights: any[], nutrition: any[], windowDays?: number }} props
*/
export default function TrendKcalWeightChart({ weights, nutrition, windowDays = 30 }) {
const n = Math.max(7, Math.min(90, Number(windowDays) || 30))
const days = []
for (let i = n - 1; i >= 0; i--) days.push(dayjs().subtract(i, 'day').format('YYYY-MM-DD'))
const wMap = {}
;(weights || []).forEach((w) => {
wMap[w.date] = w.weight
})
const nMap = {}
;(nutrition || []).forEach((x) => {
nMap[x.date] = Math.round(x.kcal || 0)
})
let lastW = null
const combined = days
.map((date) => {
if (wMap[date]) lastW = wMap[date]
return {
date: dayjs(date).format('DD.MM'),
kcal: nMap[date] || null,
weight: wMap[date] || null,
weightLine: lastW,
}
})
.filter((d) => d.kcal || d.weightLine)
const withAvg = rollingAvg(combined, 'kcal')
const hasKcal = combined.some((d) => d.kcal)
const hasW = combined.some((d) => d.weightLine)
if (!hasKcal && !hasW) {
return (
<div style={{ padding: 20, textAlign: 'center', fontSize: 12, color: 'var(--text3)' }}>
Mehr Ernährungs- und Gewichtsdaten für den Chart nötig
</div>
)
}
return (
<ResponsiveContainer width="100%" height={160}>
<LineChart data={withAvg} margin={{ top: 4, right: 8, bottom: 0, left: -20 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
<XAxis
dataKey="date"
tick={{ fontSize: 9, fill: 'var(--text3)' }}
tickLine={false}
interval={Math.max(0, Math.floor(withAvg.length / 6) - 1)}
/>
{hasKcal && (
<YAxis
yAxisId="kcal"
tick={{ fontSize: 9, fill: 'var(--text3)' }}
tickLine={false}
domain={['auto', 'auto']}
/>
)}
{hasW && (
<YAxis
yAxisId="weight"
orientation="right"
tick={{ fontSize: 9, fill: 'var(--text3)' }}
tickLine={false}
domain={['auto', 'auto']}
/>
)}
<Tooltip
contentStyle={{
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 11,
}}
formatter={(v, name) => [
v == null ? '' : `${Math.round(v)} ${name === 'weightLine' || name === 'weight' ? 'kg' : 'kcal'}`,
name === 'kcal_avg'
? 'Ø Kalorien (7T)'
: name === 'kcal'
? 'Kalorien'
: name === 'weightLine'
? 'Gewicht (interpoliert)'
: 'Gewicht Messung',
]}
/>
{hasKcal && (
<Line
yAxisId="kcal"
type="monotone"
dataKey="kcal"
stroke="#EF9F2744"
strokeWidth={1}
dot={false}
connectNulls={false}
/>
)}
{hasKcal && (
<Line
yAxisId="kcal"
type="monotone"
dataKey="kcal_avg"
stroke="#EF9F27"
strokeWidth={2}
dot={false}
connectNulls
name="kcal_avg"
/>
)}
{hasW && (
<Line
yAxisId="weight"
type="monotone"
dataKey="weightLine"
stroke="#378ADD88"
strokeWidth={1.5}
dot={false}
connectNulls
name="weightLine"
/>
)}
{hasW && (
<Line
yAxisId="weight"
type="monotone"
dataKey="weight"
stroke="#378ADD"
strokeWidth={0}
dot={(props) => {
const { cx, cy, value } = props
return value != null ? (
<circle
key={cx}
cx={cx}
cy={cy}
r={4}
fill="#378ADD"
stroke="white"
strokeWidth={1.5}
/>
) : (
<g key={cx} />
)
}}
connectNulls={false}
name="weight"
/>
)}
</LineChart>
</ResponsiveContainer>
)
}