diff --git a/src/lib/components/fitness/FitnessChart.svelte b/src/lib/components/fitness/FitnessChart.svelte index 852898b..d739647 100644 --- a/src/lib/components/fitness/FitnessChart.svelte +++ b/src/lib/components/fitness/FitnessChart.svelte @@ -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, - data: [...(ds.data || [])], - borderColor: ds.borderColor || nordColors[i % nordColors.length], - backgroundColor: ds.backgroundColor ?? (type === 'bar' - ? (nordColors[i % nordColors.length]) - : 'transparent'), - borderWidth: ds.borderWidth ?? (type === 'line' ? 2 : 0), - pointRadius: ds.pointRadius ?? (type === 'line' ? 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 - })); + 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 ?? (isBar + ? (nordColors[i % nordColors.length]) + : 'transparent'), + 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, + ...(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, diff --git a/src/routes/api/fitness/stats/overview/+server.ts b/src/routes/api/fitness/stats/overview/+server.ts index bce8d8f..05c8aca 100644 --- a/src/routes/api/fitness/stats/overview/+server.ts +++ b/src/routes/api/fitness/stats/overview/+server.ts @@ -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 diff --git a/src/routes/fitness/[stats=fitnessStats]/+page.svelte b/src/routes/fitness/[stats=fitnessStats]/+page.svelte index defcc9a..1dea8a1 100644 --- a/src/routes/fitness/[stats=fitnessStats]/+page.svelte +++ b/src/routes/fitness/[stats=fitnessStats]/+page.svelte @@ -187,6 +187,7 @@ data={workoutsChartData} title={t('workouts_per_week', lang)} height="220px" + goalLine={goalWeekly ?? undefined} /> {:else}

{t('no_workout_data', lang)}