fitness: use time-scale x-axis for weight chart to handle date gaps
All checks were successful
CI / update (push) Successful in 2m23s

Weight chart now spaces data points proportionally to actual dates
instead of evenly. Days without a weight log no longer compress adjacent
points together. Uses Chart.js time scale with chartjs-adapter-date-fns.
This commit is contained in:
2026-03-30 08:59:59 +02:00
parent 5bed3f3781
commit 27a29b6f69
5 changed files with 48 additions and 7 deletions

View File

@@ -50,6 +50,8 @@
"@sveltejs/adapter-node": "^5.0.0",
"@tauri-apps/plugin-geolocation": "^2.3.2",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"file-type": "^19.0.0",
"ioredis": "^5.9.0",
"leaflet": "^1.9.4",

22
pnpm-lock.yaml generated
View File

@@ -20,6 +20,12 @@ importers:
chart.js:
specifier: ^4.5.0
version: 4.5.0
chartjs-adapter-date-fns:
specifier: ^3.0.0
version: 3.0.0(chart.js@4.5.0)(date-fns@4.1.0)
date-fns:
specifier: ^4.1.0
version: 4.1.0
file-type:
specifier: ^19.0.0
version: 19.6.0
@@ -1086,6 +1092,12 @@ packages:
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
engines: {pnpm: '>=8'}
chartjs-adapter-date-fns@3.0.0:
resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==}
peerDependencies:
chart.js: '>=2.8.0'
date-fns: '>=2.0.0'
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -1137,6 +1149,9 @@ packages:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -2613,6 +2628,11 @@ snapshots:
dependencies:
'@kurkle/color': 0.3.4
chartjs-adapter-date-fns@3.0.0(chart.js@4.5.0)(date-fns@4.1.0):
dependencies:
chart.js: 4.5.0
date-fns: 4.1.0
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -2661,6 +2681,8 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 15.1.0
date-fns@4.1.0: {}
debug@4.3.4:
dependencies:
ms: 2.1.2

View File

@@ -1,11 +1,12 @@
<script>
import { onMount } from 'svelte';
import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-date-fns';
/**
* @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, type?: string, borderDash?: number[], [key: string]: any }> },
* data: { labels: string[], dates?: 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,
@@ -47,13 +48,20 @@
const textColor = dark ? '#D8DEE9' : '#2E3440';
const gridColor = dark ? 'rgba(216,222,233,0.1)' : 'rgba(46,52,64,0.08)';
const plainLabels = [...(data.labels || [])];
const useTimeAxis = !!(data.dates && data.dates.length > 0);
const dates = data.dates || [];
const plainLabels = useTimeAxis ? [] : [...(data.labels || [])];
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);
const rawData = [...(ds.data || [])];
// When using time axis, pair each value with its date
const chartData = useTimeAxis
? rawData.map((v, j) => ({ x: dates[j], y: v }))
: rawData;
return {
...ds,
data: [...(ds.data || [])],
data: chartData,
borderColor: ds.borderColor || nordColors[i % nordColors.length],
backgroundColor: ds.backgroundColor ?? (isBar
? (nordColors[i % nordColors.length])
@@ -93,7 +101,7 @@
});
}
chart = new Chart(ctx, {
chart = new Chart(ctx, /** @type {any} */ ({
type,
data: { labels: plainLabels, datasets: plainDatasets },
plugins,
@@ -102,7 +110,13 @@
maintainAspectRatio: false,
animation: { duration: 0 },
scales: {
x: {
x: useTimeAxis ? {
type: 'time',
time: { unit: 'day', tooltipFormat: 'MMM d' },
grid: { display: false },
border: { display: false },
ticks: { color: textColor, font: { size: 11 }, maxTicksLimit: 8 }
} : {
grid: { display: false },
border: { display: false },
ticks: { color: textColor, font: { size: 11 }, maxTicksLimit: 8 }
@@ -146,7 +160,7 @@
}
})
}
});
}));
}
onMount(() => {

View File

@@ -155,17 +155,19 @@ export const GET: RequestHandler = async ({ locals }) => {
// Build chart-ready weight data with SMA ± 1 std dev confidence band
const weightChart: {
labels: string[];
dates: string[];
data: number[];
sma: (number | null)[];
upper: (number | null)[];
lower: (number | null)[];
} = { labels: [], data: [], sma: [], upper: [], lower: [] };
} = { labels: [], dates: [], data: [], sma: [], upper: [], lower: [] };
const weights: number[] = [];
for (const m of weightMeasurements) {
const d = new Date(m.date);
weightChart.labels.push(
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
);
weightChart.dates.push(d.toISOString());
weightChart.data.push(m.weight!);
weights.push(m.weight!);
}

View File

@@ -82,6 +82,7 @@
const weightChartData = $derived({
labels: stats.weightChart?.labels ?? [],
dates: stats.weightChart?.dates,
datasets: [
...(hasSma ? [
{