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