mitai-jinkendo/frontend/src/components/TrendKcalWeightChart.jsx

184 lines
4.8 KiB
React
Raw Normal View History

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>
)
}