184 lines
4.8 KiB
React
184 lines
4.8 KiB
React
|
|
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>
|
|||
|
|
)
|
|||
|
|
}
|