fitness: use time-scale x-axis for weight chart to handle date gaps
All checks were successful
CI / update (push) Successful in 2m23s
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:
@@ -50,6 +50,8 @@
|
|||||||
"@sveltejs/adapter-node": "^5.0.0",
|
"@sveltejs/adapter-node": "^5.0.0",
|
||||||
"@tauri-apps/plugin-geolocation": "^2.3.2",
|
"@tauri-apps/plugin-geolocation": "^2.3.2",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"file-type": "^19.0.0",
|
"file-type": "^19.0.0",
|
||||||
"ioredis": "^5.9.0",
|
"ioredis": "^5.9.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ importers:
|
|||||||
chart.js:
|
chart.js:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 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:
|
file-type:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.6.0
|
version: 19.6.0
|
||||||
@@ -1086,6 +1092,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
|
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
|
||||||
engines: {pnpm: '>=8'}
|
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:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
@@ -1137,6 +1149,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
|
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
debug@4.3.4:
|
debug@4.3.4:
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -2613,6 +2628,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@kurkle/color': 0.3.4
|
'@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:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
@@ -2661,6 +2681,8 @@ snapshots:
|
|||||||
whatwg-mimetype: 4.0.0
|
whatwg-mimetype: 4.0.0
|
||||||
whatwg-url: 15.1.0
|
whatwg-url: 15.1.0
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@4.3.4:
|
debug@4.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{
|
* @type {{
|
||||||
* type?: 'line' | 'bar',
|
* 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,
|
* title?: string,
|
||||||
* height?: string,
|
* height?: string,
|
||||||
* yUnit?: string,
|
* yUnit?: string,
|
||||||
@@ -47,13 +48,20 @@
|
|||||||
const textColor = dark ? '#D8DEE9' : '#2E3440';
|
const textColor = dark ? '#D8DEE9' : '#2E3440';
|
||||||
const gridColor = dark ? 'rgba(216,222,233,0.1)' : 'rgba(46,52,64,0.08)';
|
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 plainDatasets = (data.datasets || []).map((ds, i) => {
|
||||||
const isLine = ds.type === 'line' || (type === 'line' && !ds.type);
|
const isLine = ds.type === 'line' || (type === 'line' && !ds.type);
|
||||||
const isBar = ds.type === 'bar' || (type === 'bar' && !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 {
|
return {
|
||||||
...ds,
|
...ds,
|
||||||
data: [...(ds.data || [])],
|
data: chartData,
|
||||||
borderColor: ds.borderColor || nordColors[i % nordColors.length],
|
borderColor: ds.borderColor || nordColors[i % nordColors.length],
|
||||||
backgroundColor: ds.backgroundColor ?? (isBar
|
backgroundColor: ds.backgroundColor ?? (isBar
|
||||||
? (nordColors[i % nordColors.length])
|
? (nordColors[i % nordColors.length])
|
||||||
@@ -93,7 +101,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
chart = new Chart(ctx, {
|
chart = new Chart(ctx, /** @type {any} */ ({
|
||||||
type,
|
type,
|
||||||
data: { labels: plainLabels, datasets: plainDatasets },
|
data: { labels: plainLabels, datasets: plainDatasets },
|
||||||
plugins,
|
plugins,
|
||||||
@@ -102,7 +110,13 @@
|
|||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: { duration: 0 },
|
animation: { duration: 0 },
|
||||||
scales: {
|
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 },
|
grid: { display: false },
|
||||||
border: { display: false },
|
border: { display: false },
|
||||||
ticks: { color: textColor, font: { size: 11 }, maxTicksLimit: 8 }
|
ticks: { color: textColor, font: { size: 11 }, maxTicksLimit: 8 }
|
||||||
@@ -146,7 +160,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -155,17 +155,19 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
// Build chart-ready weight data with SMA ± 1 std dev confidence band
|
// Build chart-ready weight data with SMA ± 1 std dev confidence band
|
||||||
const weightChart: {
|
const weightChart: {
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
dates: string[];
|
||||||
data: number[];
|
data: number[];
|
||||||
sma: (number | null)[];
|
sma: (number | null)[];
|
||||||
upper: (number | null)[];
|
upper: (number | null)[];
|
||||||
lower: (number | null)[];
|
lower: (number | null)[];
|
||||||
} = { labels: [], data: [], sma: [], upper: [], lower: [] };
|
} = { labels: [], dates: [], data: [], sma: [], upper: [], lower: [] };
|
||||||
const weights: number[] = [];
|
const weights: number[] = [];
|
||||||
for (const m of weightMeasurements) {
|
for (const m of weightMeasurements) {
|
||||||
const d = new Date(m.date);
|
const d = new Date(m.date);
|
||||||
weightChart.labels.push(
|
weightChart.labels.push(
|
||||||
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
||||||
);
|
);
|
||||||
|
weightChart.dates.push(d.toISOString());
|
||||||
weightChart.data.push(m.weight!);
|
weightChart.data.push(m.weight!);
|
||||||
weights.push(m.weight!);
|
weights.push(m.weight!);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
|
|
||||||
const weightChartData = $derived({
|
const weightChartData = $derived({
|
||||||
labels: stats.weightChart?.labels ?? [],
|
labels: stats.weightChart?.labels ?? [],
|
||||||
|
dates: stats.weightChart?.dates,
|
||||||
datasets: [
|
datasets: [
|
||||||
...(hasSma ? [
|
...(hasSma ? [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user