Skip to content

Commit 237d941

Browse files
committed
Fix dashboard weekly sets caching and muscle analysis tooltip
1 parent 11d27cc commit 237d941

5 files changed

Lines changed: 83 additions & 46 deletions

File tree

frontend/components/dashboard/hooks/useDashboardWeeklySetsDashboard.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ExerciseAsset } from '../../../utils/data/exerciseAssets';
44
import {
55
computeWeeklySetsDashboardData,
66
type WeeklySetsGrouping,
7+
type WeeklySetsDashboardResult,
78
type WeeklySetsWindow,
89
} from '../../../utils/muscle/analytics';
910
import { computationCache } from '../../../utils/storage/computationCache';
@@ -34,25 +35,23 @@ export const useDashboardWeeklySetsDashboard = (args: {
3435
}
3536

3637
const cacheKey = dashboardCacheKeys.weeklySets(filterCacheKey, muscleCompQuick, compositionGrouping, secondarySetMultiplier);
37-
return computationCache.getOrCompute(
38+
const computed = computationCache.getOrCompute<WeeklySetsDashboardResult>(
3839
cacheKey,
3940
fullData,
40-
() => {
41-
const computed = computeWeeklySetsDashboardData(
42-
fullData,
43-
assetsMap,
44-
effectiveNow,
45-
muscleCompQuick,
46-
compositionGrouping,
47-
secondarySetMultiplier
48-
);
49-
return {
50-
heatmap: computed.heatmap,
51-
windowStart: computed.windowStart,
52-
};
53-
},
41+
() => computeWeeklySetsDashboardData(
42+
fullData,
43+
assetsMap,
44+
effectiveNow,
45+
muscleCompQuick,
46+
compositionGrouping,
47+
secondarySetMultiplier
48+
),
5449
{ ttl: 10 * 60 * 1000 }
5550
);
51+
return {
52+
heatmap: computed.heatmap,
53+
windowStart: computed.windowStart,
54+
};
5655
}, [assetsMap, fullData, effectiveNow, muscleCompQuick, compositionGrouping, filterCacheKey, secondarySetMultiplier]);
5756

5857
return { weeklySetsDashboard };

frontend/components/muscleAnalysis/hooks/useMuscleAnalysisHandlers.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import {
33
getHeadlessIdForDetailedSvgId,
44
HEADLESS_MUSCLE_NAMES,
@@ -35,9 +35,26 @@ export const useMuscleAnalysisHandlers = ({
3535
hypertrophyScoreMap,
3636
}: UseMuscleAnalysisHandlersParams) => {
3737
const [hoveredMuscle, setHoveredMuscle] = useState<string | null>(null);
38+
const [hoverContext, setHoverContext] = useState<{ muscleId: string; rect: DOMRect } | null>(null);
3839

3940
const volumeThresholds = useMemo(() => getVolumeThresholds(trainingLevel), [trainingLevel]);
4041

42+
const buildTooltip = useCallback((muscleId: string, rect: DOMRect): TooltipData => {
43+
const headlessId = getHeadlessIdForDetailedSvgId(muscleId) ?? muscleId;
44+
const rate = headlessRatesMap.get(headlessId) || 0;
45+
const zone = getVolumeZone(rate, volumeThresholds);
46+
const hScore = hypertrophyScoreMap.get(headlessId);
47+
const scoreLine = hScore !== undefined ? `Hypertrophy Score: ${hScore}/100` : `No score yet`;
48+
const bodyText = `${rate.toFixed(1)} sets/wk — ${zone.label}\n${scoreLine}\n${zone.explanation}`;
49+
50+
return {
51+
rect,
52+
title: (HEADLESS_MUSCLE_NAMES as any)[headlessId] ?? muscleId,
53+
body: bodyText,
54+
status: rate > 0 ? 'success' : 'default',
55+
};
56+
}, [headlessRatesMap, hypertrophyScoreMap, volumeThresholds]);
57+
4158
const handleMuscleClick = useCallback((muscleId: string) => {
4259
const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024;
4360

@@ -64,6 +81,7 @@ export const useMuscleAnalysisHandlers = ({
6481
const handleMuscleHover = useCallback((muscleId: string | null, e?: MouseEvent) => {
6582
setHoveredMuscle(muscleId);
6683
if (!muscleId || !e) {
84+
setHoverContext(null);
6785
setHoverTooltip(null);
6886
return;
6987
}
@@ -72,24 +90,19 @@ export const useMuscleAnalysisHandlers = ({
7290
const groupEl = target?.closest?.('g[id]') as Element | null;
7391
const rect = groupEl?.getBoundingClientRect?.() as DOMRect | undefined;
7492
if (!rect) {
93+
setHoverContext(null);
7594
setHoverTooltip(null);
7695
return;
7796
}
7897

79-
const headlessId = getHeadlessIdForDetailedSvgId(muscleId!) ?? muscleId!;
80-
const rate = headlessRatesMap.get(headlessId) || 0;
81-
const zone = getVolumeZone(rate, volumeThresholds);
82-
const hScore = hypertrophyScoreMap.get(muscleId);
83-
const scoreLine = hScore !== undefined ? `Hypertrophy Score: ${hScore}/100` : `No score yet`;
84-
const bodyText = `${rate.toFixed(1)} sets/wk — ${zone.label}\n${scoreLine}\n${zone.explanation}`;
98+
setHoverContext({ muscleId, rect });
99+
setHoverTooltip(buildTooltip(muscleId, rect));
100+
}, [buildTooltip, setHoverTooltip]);
85101

86-
setHoverTooltip({
87-
rect,
88-
title: (HEADLESS_MUSCLE_NAMES as any)[headlessId] ?? muscleId,
89-
body: bodyText,
90-
status: rate > 0 ? 'success' : 'default',
91-
});
92-
}, [headlessRatesMap, setHoverTooltip, volumeThresholds]);
102+
useEffect(() => {
103+
if (!hoverContext) return;
104+
setHoverTooltip(buildTooltip(hoverContext.muscleId, hoverContext.rect));
105+
}, [hoverContext, buildTooltip, setHoverTooltip]);
93106

94107
const selectedBodyMapIds = useMemo(() => {
95108
if (!selectedMuscle) return undefined;

frontend/components/muscleAnalysis/hooks/useMuscleHeatmapData.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { useMemo } from 'react';
22
import type { WorkoutSet } from '../../../types';
33
import { computationCache } from '../../../utils/storage/computationCache';
4-
import { computeWeeklySetsDashboardData, type WeeklySetsWindow } from '../../../utils/muscle/analytics';
4+
import {
5+
computeWeeklySetsDashboardData,
6+
type WeeklySetsDashboardResult,
7+
type WeeklySetsWindow,
8+
} from '../../../utils/muscle/analytics';
59
import { toHeadlessVolumeMap } from '../../../utils/muscle/mapping';
610
import { MUSCLE_GROUP_ORDER, SVG_TO_MUSCLE_GROUP, getHeadlessRadarSeries } from '../../../utils/muscle/mapping';
711
import type { NormalizedMuscleGroup } from '../../../utils/muscle/analytics';
@@ -30,30 +34,49 @@ export const useMuscleHeatmapData = ({
3034
filterCacheKey,
3135
secondarySetMultiplier,
3236
}: UseMuscleHeatmapDataParams) => {
37+
const getWeeklySetsDashboard = (
38+
cacheKey: string,
39+
window: WeeklySetsWindow,
40+
grouping: 'muscles' | 'groups'
41+
): WeeklySetsDashboardResult => {
42+
const compute = () =>
43+
computeWeeklySetsDashboardData(data, assetsMap!, effectiveNow, window, grouping, secondarySetMultiplier);
44+
45+
const cached = computationCache.getOrCompute<WeeklySetsDashboardResult>(
46+
cacheKey,
47+
data,
48+
compute,
49+
{ ttl: 10 * 60 * 1000 }
50+
);
51+
52+
// Older dashboard cache entries stored a heatmap-only shape under this key.
53+
// If we encounter one, force a recompute so tooltip rates are available.
54+
if (!(cached.weeklyRatesBySubject instanceof Map)) {
55+
return computationCache.getOrCompute<WeeklySetsDashboardResult>(
56+
cacheKey,
57+
data,
58+
compute,
59+
{ ttl: 10 * 60 * 1000, forceRecompute: true }
60+
);
61+
}
62+
63+
return cached;
64+
};
65+
3366
const weeklySetsDashboardMuscles = useMemo(() => {
3467
if (!assetsMap) return null;
3568

3669
const window: WeeklySetsWindow = weeklySetsWindow === 'all' ? 'all' : weeklySetsWindow;
3770
const cacheKey = muscleCacheKeys.weeklySets(filterCacheKey, window, 'muscles', secondarySetMultiplier);
38-
return computationCache.getOrCompute(
39-
cacheKey,
40-
data,
41-
() => computeWeeklySetsDashboardData(data, assetsMap, effectiveNow, window, 'muscles', secondarySetMultiplier),
42-
{ ttl: 10 * 60 * 1000 }
43-
);
71+
return getWeeklySetsDashboard(cacheKey, window, 'muscles');
4472
}, [assetsMap, data, effectiveNow, weeklySetsWindow, filterCacheKey, secondarySetMultiplier]);
4573

4674
const weeklySetsDashboardGroups = useMemo(() => {
4775
if (!assetsMap) return null;
4876

4977
const window: WeeklySetsWindow = weeklySetsWindow === 'all' ? 'all' : weeklySetsWindow;
5078
const cacheKey = muscleCacheKeys.weeklySets(filterCacheKey, window, 'groups', secondarySetMultiplier);
51-
return computationCache.getOrCompute(
52-
cacheKey,
53-
data,
54-
() => computeWeeklySetsDashboardData(data, assetsMap, effectiveNow, window, 'groups', secondarySetMultiplier),
55-
{ ttl: 10 * 60 * 1000 }
56-
);
79+
return getWeeklySetsDashboard(cacheKey, window, 'groups');
5780
}, [assetsMap, data, effectiveNow, weeklySetsWindow, filterCacheKey, secondarySetMultiplier]);
5881

5982
const windowedHeatmapData = useMemo(() => {

frontend/utils/muscle/analytics/dashboardWeeklySets.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ export const computeWeeklySetsDashboardData = (
125125
}
126126
}
127127

128-
const windowDays = window === 'all'
129-
? Math.max(1, differenceInCalendarDays(now, windowStart))
130-
: parseInt(window, 10);
128+
const clampedSpanDays = Math.max(1, differenceInCalendarDays(now, windowStart));
129+
const windowDays =
130+
window === 'all' || window === '365d'
131+
? clampedSpanDays
132+
: parseInt(window, 10);
131133
const weeks = Math.max(1, windowDays / 7);
132134

133135
const weeklyRates = new Map<string, number>();

frontend/utils/storage/cacheKeys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const cacheKeys = {
2828
*/
2929
export const dashboardCacheKeys = {
3030
weeklySets: (filterKey: string, window: WeeklySetsWindow, grouping: WeeklySetsGrouping, secondaryMultiplier: number = 0.5) =>
31-
`weeklySets:${VERSION}:${filterKey}:${window}:${grouping}:sec:${secondaryMultiplier}`,
31+
`weeklySets:${VERSION}:calc2:${filterKey}:${window}:${grouping}:sec:${secondaryMultiplier}`,
3232

3333
muscleSeries: (filterKey: string, mode: 'groups' | 'muscles', secondaryMultiplier: number = 0.5) =>
3434
`muscleSeries:${VERSION}:${filterKey}:${mode}:sec:${secondaryMultiplier}`,

0 commit comments

Comments
 (0)