fitness: add kcal estimation based on Lytle et al. (2019) regression model
Estimate strength workout energy expenditure using the Lytle et al. multiple linear regression model. Maps all 77 exercises to 7 studied categories with confidence levels. Shows kcal on stats page (cumulative), session cards, workout detail, and workout completion screen. Supports sex/height demographics via profile section on measure page. Includes info tooltip with DOI reference.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Plus, Trash2, Play, Pause, Trophy, Clock, Dumbbell, Route, RefreshCw, Check, ChevronUp, ChevronDown } from 'lucide-svelte';
|
||||
import { Plus, Trash2, Play, Pause, Trophy, Clock, Dumbbell, Route, RefreshCw, Check, ChevronUp, ChevronDown, Flame } from 'lucide-svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
@@ -9,6 +9,7 @@
|
||||
import { getWorkout } from '$lib/js/workout.svelte';
|
||||
import { getWorkoutSync } from '$lib/js/workoutSync.svelte';
|
||||
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
|
||||
import { estimateWorkoutKcal } from '$lib/data/kcalEstimate';
|
||||
import ExerciseName from '$lib/components/fitness/ExerciseName.svelte';
|
||||
import SetTable from '$lib/components/fitness/SetTable.svelte';
|
||||
import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte';
|
||||
@@ -192,6 +193,24 @@
|
||||
};
|
||||
});
|
||||
|
||||
// Estimate kcal for strength exercises
|
||||
/** @type {import('$lib/data/kcalEstimate').ExerciseData[]} */
|
||||
const kcalExercises = [];
|
||||
for (const ex of local.exercises) {
|
||||
const exercise = getExerciseById(ex.exerciseId, lang);
|
||||
const metrics = getExerciseMetrics(exercise);
|
||||
if (metrics.includes('distance')) continue;
|
||||
const weightMultiplier = exercise?.bilateral ? 2 : 1;
|
||||
const sets = ex.sets
|
||||
.filter((/** @type {any} */ s) => s.completed && s.reps > 0)
|
||||
.map((/** @type {any} */ s) => ({
|
||||
weight: (s.weight ?? 0) * weightMultiplier,
|
||||
reps: s.reps ?? 0
|
||||
}));
|
||||
if (sets.length > 0) kcalExercises.push({ exerciseId: ex.exerciseId, sets });
|
||||
}
|
||||
const kcalResult = kcalExercises.length > 0 ? estimateWorkoutKcal(kcalExercises) : null;
|
||||
|
||||
return {
|
||||
sessionId: saved._id,
|
||||
name: local.name,
|
||||
@@ -201,7 +220,8 @@
|
||||
totalTonnage,
|
||||
totalDistance,
|
||||
exerciseSummaries,
|
||||
prs
|
||||
prs,
|
||||
kcalResult
|
||||
};
|
||||
}
|
||||
|
||||
@@ -366,6 +386,13 @@
|
||||
<span class="comp-stat-label">{t('distance', lang)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if completionData.kcalResult}
|
||||
<div class="comp-stat kcal">
|
||||
<Flame size={18} />
|
||||
<span class="comp-stat-value">{completionData.kcalResult.kcal} ± {completionData.kcalResult.kcal - completionData.kcalResult.lower} kcal</span>
|
||||
<span class="comp-stat-label">{t('est_kcal', lang)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if completionData.prs.length > 0}
|
||||
@@ -605,6 +632,9 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.comp-stat.kcal {
|
||||
color: var(--nord12);
|
||||
}
|
||||
|
||||
.prs-section {
|
||||
background: color-mix(in srgb, var(--nord13) 8%, transparent);
|
||||
|
||||
Reference in New Issue
Block a user