fitness: improve workouts chart with goal line, max bar width, and full 10-week range
Some checks failed
CI / update (push) Has been cancelled
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user