New features
This commit is contained in:
parent
55a112415d
commit
8981dc9615
17 changed files with 880 additions and 99 deletions
209
src/routes/summary/ExerciseGraph.svelte
Normal file
209
src/routes/summary/ExerciseGraph.svelte
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue