egentrening/src/routes/summary/ExerciseGraph.svelte

210 lines
5.3 KiB
Svelte
Raw Normal View History

2025-09-04 21:38:07 +02:00
<script lang="ts">
import type { TimePeriod } from './summaryUtils';
import { getWorkoutForEachDayCountingBackwards } from './summary.remote';
import type { WorkoutDataSummary, WorkoutDataWithDate } from '$lib/workout';
interface Props {
exerciseKey: string;
selectedPeriod: TimePeriod;
dailyGoal: number;
color: string;
}
let { exerciseKey, selectedPeriod, dailyGoal, color }: Props = $props();
function processWorkoutData(dailyWorkouts: WorkoutDataWithDate[], key: string) {
if (!dailyWorkouts || dailyWorkouts.length === 0) return [];
return dailyWorkouts
.map((workout) => ({
date: workout.date,
value: getValueFromWorkout(workout, key)
}))
.reverse(); // Reverse to show oldest to newest
}
function getValueFromWorkout(workout: WorkoutDataWithDate, key: string): number {
switch (key) {
case 'pushups':
return workout.pushups || 0;
case 'situps':
return workout.situps || 0;
case 'plankSeconds':
return workout.plankSeconds || 0;
case 'hangups':
return workout.hangups || 0;
case 'runKm':
return workout.runKm || 0;
default:
return 0;
}
}
// Graph dimensions
const width = 400;
const height = 200;
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const graphWidth = width - margin.left - margin.right;
const graphHeight = height - margin.top - margin.bottom;
// Animation state
let mounted = $state(false);
// Set mounted flag after component loads for animation
setTimeout(() => {
mounted = true;
}, 100);
function formatDate(date: Date): string {
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
}
function getColorClass(color: string): string {
switch (color) {
case 'blue':
return 'fill-blue-500';
case 'green':
return 'fill-green-500';
case 'orange':
return 'fill-orange-500';
case 'purple':
return 'fill-purple-500';
case 'red':
return 'fill-red-500';
default:
return 'fill-gray-500';
}
}
let dailyWorkouts = $state<WorkoutDataWithDate[]>([]);
let loading = $state(true);
// Fetch workout data when selectedPeriod changes
$effect(() => {
loading = true;
const fetchData = async () => {
try {
const result = await getWorkoutForEachDayCountingBackwards(selectedPeriod.days);
dailyWorkouts = result.data;
} catch (error) {
console.error('Error fetching workout data:', error);
dailyWorkouts = [];
} finally {
loading = false;
}
};
fetchData();
});
const dailyData = $derived(processWorkoutData(dailyWorkouts, exerciseKey));
const maxValue = $derived(Math.max(dailyGoal, ...dailyData.map((d: any) => d.value), 1));
const barSpacing = 4;
const barWidth = $derived(Math.max(graphWidth / Math.max(dailyData.length, 1) - barSpacing, 2));
const yScale = $derived(graphHeight / maxValue);
</script>
<div class="w-full">
{#if loading}
<div class="flex h-48 items-center justify-center text-gray-500">Loading...</div>
{:else if dailyData.length === 0}
<div class="flex h-48 items-center justify-center text-gray-500">
No data available for this period
</div>
{:else}
<svg {width} {height} class="mx-auto">
<!-- Goal line -->
<line
x1={margin.left}
y1={margin.top + graphHeight - dailyGoal * yScale}
x2={margin.left + graphWidth}
y2={margin.top + graphHeight - dailyGoal * yScale}
stroke="#ef4444"
stroke-width="2"
stroke-dasharray="5,5"
/>
<!-- Goal label -->
<text
x={margin.left + graphWidth - 5}
y={margin.top + graphHeight - dailyGoal * yScale - 5}
text-anchor="end"
class="fill-red-500 text-xs"
>
Goal: {dailyGoal}
</text>
<!-- Bars -->
{#each dailyData as { date, value }, i}
<rect
x={margin.left + i * (barWidth + barSpacing) + barSpacing / 2}
y={margin.top + graphHeight - (mounted ? value * yScale : 0)}
width={barWidth}
height={mounted ? value * yScale : 0}
class={getColorClass(color)}
opacity="0.8"
style="transition: height 1s ease-out {i * 10}ms, y 1s ease-out {i * 10}ms;"
>
<title>{formatDate(date)}: {value}</title>
</rect>
{/each}
<!-- Y-axis -->
<line
x1={margin.left}
y1={margin.top}
x2={margin.left}
y2={margin.top + graphHeight}
stroke="#6b7280"
stroke-width="1"
/>
<!-- X-axis -->
<line
x1={margin.left}
y1={margin.top + graphHeight}
x2={margin.left + graphWidth}
y2={margin.top + graphHeight}
stroke="#6b7280"
stroke-width="1"
/>
<!-- Y-axis labels -->
<text x={margin.left - 10} y={margin.top + 5} text-anchor="end" class="fill-gray-600 text-xs">
{maxValue}
</text>
<text
x={margin.left - 10}
y={margin.top + graphHeight + 5}
text-anchor="end"
class="fill-gray-600 text-xs"
>
0
</text>
<!-- X-axis labels (show first and last date) -->
{#if dailyData.length > 0}
<text
x={margin.left}
y={margin.top + graphHeight + 20}
text-anchor="start"
class="fill-gray-600 text-xs"
>
{formatDate(dailyData[0].date)}
</text>
{#if dailyData.length > 1}
<text
x={margin.left + graphWidth}
y={margin.top + graphHeight + 20}
text-anchor="end"
class="fill-gray-600 text-xs"
>
{formatDate(dailyData[dailyData.length - 1].date)}
</text>
{/if}
{/if}
</svg>
{/if}
</div>