Compute NP from timeseries in frontend for activities missing np_power_w in JSON

This commit is contained in:
Davide Scaini
2026-05-12 23:51:22 +02:00
parent bd0595ee79
commit c46e91d0f5
+38 -2
View File
@@ -112,6 +112,42 @@
$: galleryImages = (detail?.custom as any)?.images as string[] ?? []; $: galleryImages = (detail?.custom as any)?.images as string[] ?? [];
// Coggan NP from timeseries — mirrors the Python implementation in metrics.py.
// Used as fallback for activities extracted before np_power_w was added to the JSON.
function computeNpFromTimeseries(ts: Timeseries): number | null {
const { t, power_w } = ts;
if (!power_w || !t || t.length < 30) return null;
if (!power_w.some(v => v != null)) return null;
const sparse = new Map<number, number>();
for (let i = 0; i < t.length; i++) {
if (power_w[i] != null) sparse.set(t[i], power_w[i]!);
}
if (sparse.size < 2) return null;
const tMin = Math.min(...sparse.keys());
const tMax = Math.max(...sparse.keys());
if (tMax - tMin > 7 * 24 * 3600) return null;
const dense: number[] = [];
for (let i = 0; i <= tMax - tMin; i++) dense.push(sparse.get(tMin + i) ?? 0);
const win = 30;
if (dense.length < win) return null;
const half = Math.floor(win / 2);
let windowSum = dense.slice(0, win).reduce((a, b) => a + b, 0);
const fourthPowers: number[] = [];
for (let i = half; i < dense.length - half; i++) {
fourthPowers.push((windowSum / win) ** 4);
if (i + half + 1 < dense.length) windowSum += dense[i + half + 1] - dense[i - half];
}
if (!fourthPowers.length) return null;
return Math.round((fourthPowers.reduce((a, b) => a + b, 0) / fourthPowers.length) ** 0.25);
}
$: npPower = detail?.np_power_w ?? (timeseries ? computeNpFromTimeseries(timeseries) : null);
const stat = (label: string, value: string, key?: string) => ({ label, value, key }); const stat = (label: string, value: string, key?: string) => ({ label, value, key });
$: hiddenStats = new Set<string>((detail?.custom as any)?.hide_stats ?? []); $: hiddenStats = new Set<string>((detail?.custom as any)?.hide_stats ?? []);
$: stats = [ $: stats = [
@@ -124,8 +160,8 @@
stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'), stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'),
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'), stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
...(activity.avg_power_w != null ? [ ...(activity.avg_power_w != null ? [
stat('Avg power', `${activity.avg_power_w} W`, 'power'), stat('Avg power', `${activity.avg_power_w} W`, 'power'),
stat('NP', detail?.np_power_w != null ? `${detail.np_power_w} W` : '—', 'power'), stat('NP', npPower != null ? `${npPower} W` : '—', 'power'),
] : []), ] : []),
].filter(s => !s.key || !hiddenStats.has(s.key)); ].filter(s => !s.key || !hiddenStats.has(s.key));
</script> </script>