diff --git a/site/src/components/AthleteView.svelte b/site/src/components/AthleteView.svelte
index fbca99c..0735307 100644
--- a/site/src/components/AthleteView.svelte
+++ b/site/src/components/AthleteView.svelte
@@ -210,7 +210,7 @@
{#if activeTab === 'power'}
{#if athlete.power_curve.all_time}
-
+
{:else}
No power data found. Make sure your activities include power meter data.
diff --git a/site/src/components/MmpChart.svelte b/site/src/components/MmpChart.svelte
index cbdb4bf..6b34def 100644
--- a/site/src/components/MmpChart.svelte
+++ b/site/src/components/MmpChart.svelte
@@ -5,6 +5,7 @@
export let athlete: AthleteJson;
export let activities: ActivitySummary[] = [];
+ export let base: string = '/';
// ── Range selection ────────────────────────────────────────────────────────
type RangeKey = 'all_time' | 'last_365d' | 'last_90d' | string;
@@ -35,6 +36,39 @@
return PALETTE[index % PALETTE.length];
}
+ // ── Record-holder lookup ───────────────────────────────────────────────────
+
+ const _now = new Date();
+ function _cutoff(days: number): string {
+ return new Date(_now.getTime() - days * 86400000).toISOString();
+ }
+ const cutoff365 = _cutoff(365);
+ const cutoff90 = _cutoff(90);
+
+ function activitiesInRange(key: RangeKey): ActivitySummary[] {
+ if (key === 'all_time') return activities;
+ if (key === 'last_365d') return activities.filter(a => a.started_at >= cutoff365);
+ if (key === 'last_90d') return activities.filter(a => a.started_at >= cutoff90);
+ const season = seasons.find(s => s.name === key);
+ if (!season) return [];
+ return activities.filter(a => a.started_at >= season.start && a.started_at <= season.end + 'T23:59:59');
+ }
+
+ // For each (rangeKey, duration_s) → the activity that holds the record.
+ // Precomputed as a Map to avoid re-scanning on every render.
+ function buildRecordMap(key: RangeKey): Map {
+ const map = new Map();
+ for (const a of activitiesInRange(key)) {
+ if (!a.mmp) continue;
+ for (const [d, w] of a.mmp) {
+ const cur = map.get(d);
+ const curW = cur ? (cur.mmp!.find(([dd]) => dd === d)?.[1] ?? 0) : -1;
+ if (w > curW) map.set(d, a);
+ }
+ }
+ return map;
+ }
+
// ── MMP curve computation ──────────────────────────────────────────────────
function mergeMmps(mmps: MmpCurve[]): MmpCurve {
@@ -74,10 +108,17 @@
$: selectedKeys = [...selectedRanges];
- $: plotData = selectedKeys.flatMap((key, i) => {
+ // Record maps per selected range — recomputed when activities or selected keys change.
+ $: recordMaps = Object.fromEntries(selectedKeys.map(k => [k, buildRecordMap(k)]));
+
+ $: plotData = selectedKeys.flatMap((key) => {
const curve = mmpsForRange(key);
if (!curve) return [];
- return curve.map(([d, w]) => ({ d, w, label: key }));
+ const rmap = recordMaps[key] ?? new Map();
+ return curve.map(([d, w]) => {
+ const act = rmap.get(d);
+ return { d, w, label: key, actTitle: act?.title ?? null, actId: act?.id ?? null };
+ });
});
$: colorMap = Object.fromEntries(selectedKeys.map((k, i) => [k, curveColor(k, i)]));
@@ -131,7 +172,7 @@
fill: 'label',
r: 3,
tip: true,
- title: (d: any) => `${labelFn(d.label)}\n${formatDuration(d.d)}: ${d.w} W`,
+ title: (d: any) => `${labelFn(d.label)}\n${formatDuration(d.d)}: ${d.w} W${d.actTitle ? '\n' + d.actTitle : ''}`,
}),
...(athlete.ftp_w ? [
Plot.ruleY([athlete.ftp_w], {
@@ -185,6 +226,25 @@
...Object.keys(PRESET_LABELS),
...seasons.map(s => s.name),
];
+
+ // ── Records table ──────────────────────────────────────────────────────────
+
+ let tableRange: RangeKey = 'all_time';
+
+ $: tableRecordMap = buildRecordMap(tableRange);
+
+ $: tableRows = (() => {
+ const curve = mmpsForRange(tableRange);
+ if (!curve) return [];
+ return curve.map(([d, w]) => {
+ const act = tableRecordMap.get(d);
+ return { d, w, act };
+ });
+ })();
+
+ function fmtDate(iso: string): string {
+ return new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
+ }