Compute NP from timeseries in frontend for activities missing np_power_w in JSON
This commit is contained in:
@@ -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 = [
|
||||||
@@ -125,7 +161,7 @@
|
|||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user