fitness: improve workouts chart with goal line, max bar width, and full 10-week range
Some checks failed
CI / update (push) Has been cancelled

Add weekly goal as a solid horizontal line on the bar chart via a
custom Chart.js plugin. Cap bar width at 40px. Always show all 10
weeks including empty ones instead of trimming leading zeros.
This commit is contained in:
2026-03-23 12:34:51 +01:00
parent 3ef61c900f
commit 1f83b451de
3 changed files with 52 additions and 23 deletions

View File

@@ -5,13 +5,14 @@
/**
* @type {{
* type?: 'line' | 'bar',
* data: { labels: string[], datasets: Array<{ label: string, data: (number|null)[], borderColor?: string, backgroundColor?: string, borderWidth?: number, pointRadius?: number, pointBackgroundColor?: string, tension?: number, fill?: boolean|string, order?: number }> },
* data: { labels: string[], datasets: Array<{ label: string, data: (number|null)[], borderColor?: string, backgroundColor?: string, borderWidth?: number, pointRadius?: number, pointBackgroundColor?: string, tension?: number, fill?: boolean|string, order?: number, type?: string, borderDash?: number[], [key: string]: any }> },
* title?: string,
* height?: string,
* yUnit?: string
* yUnit?: string,
* goalLine?: number
* }}
*/
let { type = 'line', data, title = '', height = '250px', yUnit = '' } = $props();
let { type = 'line', data, title = '', height = '250px', yUnit = '', goalLine = undefined } = $props();
/** @type {HTMLCanvasElement | undefined} */
let canvas = $state(undefined);
@@ -47,25 +48,55 @@
const gridColor = dark ? 'rgba(216,222,233,0.1)' : 'rgba(46,52,64,0.08)';
const plainLabels = [...(data.labels || [])];
const plainDatasets = (data.datasets || []).map((ds, i) => ({
label: ds.label,
const plainDatasets = (data.datasets || []).map((ds, i) => {
const isLine = ds.type === 'line' || (type === 'line' && !ds.type);
const isBar = ds.type === 'bar' || (type === 'bar' && !ds.type);
return {
...ds,
data: [...(ds.data || [])],
borderColor: ds.borderColor || nordColors[i % nordColors.length],
backgroundColor: ds.backgroundColor ?? (type === 'bar'
backgroundColor: ds.backgroundColor ?? (isBar
? (nordColors[i % nordColors.length])
: 'transparent'),
borderWidth: ds.borderWidth ?? (type === 'line' ? 2 : 0),
pointRadius: ds.pointRadius ?? (type === 'line' ? 3 : 0),
borderWidth: ds.borderWidth ?? (isLine ? 2 : 0),
pointRadius: ds.pointRadius ?? (isLine ? 3 : 0),
pointBackgroundColor: ds.pointBackgroundColor || ds.borderColor || nordColors[i % nordColors.length],
tension: ds.tension ?? 0.3,
fill: ds.fill ?? false,
spanGaps: true,
order: ds.order ?? i
}));
order: ds.order ?? i,
...(isBar ? { maxBarThickness: 40 } : {})
};
});
/** @type {import('chart.js').Plugin[]} */
const plugins = [];
if (goalLine != null) {
plugins.push({
id: 'goalLine',
afterDraw(chart) {
const yScale = chart.scales.y;
const xScale = chart.scales.x;
if (!yScale || !xScale) return;
const y = yScale.getPixelForValue(goalLine);
const ctx = chart.ctx;
ctx.save();
ctx.beginPath();
ctx.setLineDash([]);
ctx.strokeStyle = '#EBCB8B';
ctx.lineWidth = 2;
ctx.moveTo(xScale.left, y);
ctx.lineTo(xScale.right, y);
ctx.stroke();
ctx.restore();
}
});
}
chart = new Chart(ctx, {
type,
data: { labels: plainLabels, datasets: plainDatasets },
plugins,
options: {
responsive: true,
maintainAspectRatio: false,

View File

@@ -147,12 +147,9 @@ export const GET: RequestHandler = async ({ locals }) => {
allData.push(weekMap.get(key) ?? 0);
}
// Trim leading empty weeks, but always keep from first week with data
let firstNonZero = allData.findIndex((v) => v > 0);
if (firstNonZero === -1) firstNonZero = allData.length - 1; // show at least current week
const workoutsChart = {
labels: allLabels.slice(firstNonZero),
data: allData.slice(firstNonZero)
labels: allLabels,
data: allData
};
// Build chart-ready weight data with SMA ± 1 std dev confidence band

View File

@@ -187,6 +187,7 @@
data={workoutsChartData}
title={t('workouts_per_week', lang)}
height="220px"
goalLine={goalWeekly ?? undefined}
/>
{:else}
<p class="empty-chart">{t('no_workout_data', lang)}</p>