Skip to content

Commit fd614fc

Browse files
committed
feat: Enhance hypertrophy charts with color-coded progress indicators and tooltip improvements
1 parent 237d941 commit fd614fc

4 files changed

Lines changed: 71 additions & 38 deletions

File tree

frontend/components/dashboard/hypertrophy/HypertrophyBarCard.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TrendingUp, BarChart3 } from 'lucide-react';
33
import { Tooltip, useTooltip } from '../../ui/Tooltip';
44
import { SegmentControl } from '../../ui/SegmentControl';
55
import { useIsMobile } from '../../insights/useIsMobile';
6+
import { ChartDescription, InsightText } from '../../dashboard/insights/ChartBits';
67
import {
78
FACTOR_COLORS,
89
FACTOR_WEIGHTS,
@@ -117,6 +118,10 @@ export const HypertrophyBarCard: React.FC<HypertrophyBarCardProps> = ({
117118
return map;
118119
}, [hypertrophyData30d, hypertrophyPeriod]);
119120

121+
const volColor = (v: number) => v <= 15 ? '#ef4444' : v <= 35 ? '#f59e0b' : '#22c55e';
122+
const progColor = (v: number) => v <= 11 ? '#ef4444' : v <= 22 ? '#f59e0b' : '#22c55e';
123+
const freqColor = (v: number) => v <= 3 ? '#ef4444' : v <= 6 ? '#f59e0b' : '#22c55e';
124+
120125
const handleMouseEnter = (e: React.MouseEvent, m: MuscleHypertrophyData) => {
121126
const raw = m.score.raw;
122127
const volW = Math.round(m.score.volumeScore * FACTOR_WEIGHTS.volumeScore);
@@ -127,19 +132,22 @@ export const HypertrophyBarCard: React.FC<HypertrophyBarCardProps> = ({
127132
const volMax = Math.round(FACTOR_WEIGHTS.volumeScore * 100);
128133
const progMax = Math.round(FACTOR_WEIGHTS.progressiveOverload * 100);
129134
const freqMax = Math.round(FACTOR_WEIGHTS.frequency * 100);
135+
130136
showTooltip(e, {
131137
title: m.muscleName,
132-
body: `Volume: ${volW}/${volMax}${raw.weeklySets.toFixed(1)} sets/week\n` +
133-
`Progress: ${progW}/${progMax}${trendLabel} trend\n` +
134-
`Frequency: ${freqW}/${freqMax}${raw.daysPerWeek.toFixed(1)} days/week`,
138+
bodySections: [
139+
{ text: `Volume: ${volW}/${volMax}${raw.weeklySets.toFixed(1)} sets/week`, color: volColor(volW) },
140+
{ text: `Progress: ${progW}/${progMax}${trendLabel} trend`, color: progColor(progW) },
141+
{ text: `Frequency: ${freqW}/${freqMax}${raw.daysPerWeek.toFixed(1)} days/week`, color: freqColor(freqW) },
142+
],
135143
status: m.score.totalScore >= 60 ? 'success' : m.score.totalScore >= 40 ? 'info' : 'warning',
136144
});
137145
};
138146

139147
return (
140-
<div className="bg-black/70 rounded-xl border border-slate-700/50 overflow-hidden h-[300px] sm:h-[450px] lg:h-full flex flex-col">
141-
<div className="p-3 flex-shrink-0">
142-
<div className="flex items-center justify-between mb-2">
148+
<div className="bg-black/70 rounded-xl border border-slate-700/50 px-2 sm:px-3 py-4 sm:py-6 min-h-[400px] sm:min-h-[520px] lg:min-h-0 lg:h-full flex flex-col">
149+
<div className="flex-shrink-0">
150+
<div className="flex items-center justify-between mb-3 gap-3">
143151
<div>
144152
<h2 className="text-xs font-bold text-white">Hypertrophy Scores</h2>
145153
<p className="text-[10px] text-slate-500 mt-0.5">Per muscle breakdown</p>
@@ -195,7 +203,7 @@ export const HypertrophyBarCard: React.FC<HypertrophyBarCardProps> = ({
195203
)}
196204
</div>
197205

198-
<div className="px-3 pb-3 flex-1 min-h-0 overflow-y-auto">
206+
<div className="flex-1 min-h-0 overflow-y-auto pb-3">
199207
{hypertrophyData.length > 0 ? (
200208
<div className="space-y-2 pr-3">
201209
<div className="flex items-center gap-3 px-1">
@@ -256,6 +264,9 @@ export const HypertrophyBarCard: React.FC<HypertrophyBarCardProps> = ({
256264
<div className="text-[10px] text-slate-500 py-4 text-center">No muscle data available.</div>
257265
)}
258266
</div>
267+
<ChartDescription>
268+
<InsightText text="The hypertrophy score estimates muscle growth potential from 0 to 100 percent. It combines three factors: volume how many weekly sets, progress how much your strength is trending up, and frequency how often you train each muscle. Higher scores mean a better stimulus for growth." />
269+
</ChartDescription>
259270
{tooltip && <Tooltip data={tooltip} />}
260271
</div>
261272
);

frontend/components/dashboard/hypertrophy/HypertrophyScatterCard.tsx

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ import {
1818
const PROGRESS_MID = 25;
1919
const VOLUME_MID = 25;
2020

21-
const QUADRANT_COLORS: Record<string, string> = {
22-
'Volume Focus': '#ef4444',
23-
'Optimal Growth': '#22c55e',
24-
'Undertrained': '#f59e0b',
25-
'Strength Focus': '#3b82f6',
21+
const SCORE_COLORS = ['#ef4444', '#f59e0b', '#22c55e'] as const;
22+
23+
const getDotColor = (total: number) => {
24+
if (total <= 30) return SCORE_COLORS[0];
25+
if (total <= 60) return SCORE_COLORS[1];
26+
return SCORE_COLORS[2];
2627
};
2728

29+
const volColor = (v: number) => v <= 15 ? '#ef4444' : v <= 35 ? '#f59e0b' : '#22c55e';
30+
const progColor = (v: number) => v <= 11 ? '#ef4444' : v <= 22 ? '#f59e0b' : '#22c55e';
31+
2832
interface ChartPoint {
2933
name: string;
3034
muscleId: string;
@@ -100,24 +104,26 @@ export const HypertrophyScatterCard: React.FC<HypertrophyScatterCardProps> = ({
100104
const d: ChartPoint | undefined = payload[0]?.payload;
101105
if (!d) return null;
102106

103-
const quadrantDesc: Record<string, string> = {
104-
'Volume Focus': 'High volume but lagging strength progress, add progressive overload',
105-
'Optimal Growth': 'High volume + strong progress, ideal for hypertrophy',
106-
'Undertrained': 'Low volume + low progress, increase training frequency',
107-
'Strength Focus': 'Strong progress despite low volume, great strength adaptation',
107+
const quadrantDesc: Record<string, { desc: string; advice: string }> = {
108+
'Volume Focus': { desc: 'High volume but lagging strength progress', advice: 'Focus on progressive overload, add weight or reps slowly' },
109+
'Optimal Growth': { desc: 'High volume + strong progress, ideal for hypertrophy', advice: 'Keep it up! Maintain this balance for gains' },
110+
'Undertrained': { desc: 'Low volume + low progress', advice: 'If prioritizing this muscle, add sets and train 2-3x/week' },
111+
'Strength Focus': { desc: 'Strong progress despite low volume', advice: 'Consider increasing volume for more size gains' },
108112
};
109113

114+
const q = quadrantDesc[d.quadrant];
110115
return (
111116
<div className="rounded-lg px-3 py-2 shadow-2xl border text-xs"
112117
style={{ backgroundColor: 'rgb(var(--panel-rgb) / 0.95)', borderColor: 'rgb(var(--border-rgb) / 0.5)', color: 'var(--text-primary)' }}>
113118
<p className="font-semibold mb-1.5">{d.name} <span className="opacity-60 font-normal">({d.total}/100)</span></p>
114119
<div className="flex items-center gap-3 mb-1.5">
115-
<span style={{ color: '#3b82f6' }}>Progress <b>{d.progress}/40</b></span>
116-
<span style={{ color: '#22c55e' }}>Volume <b>{d.volume}/50</b></span>
120+
<span>Progress <b style={{ color: progColor(d.progress) }}>{d.progress}/40</b></span>
121+
<span>Volume <b style={{ color: volColor(d.volume) }}>{d.volume}/50</b></span>
117122
</div>
118-
<div className="border-t pt-1.5" style={{ borderColor: 'rgb(var(--border-rgb) / 0.3)' }}>
119-
<p className="font-semibold text-[10px]" style={{ color: QUADRANT_COLORS[d.quadrant] }}>{d.quadrant}</p>
120-
<p className="text-[9px] opacity-70 leading-tight mt-0.5">{quadrantDesc[d.quadrant]}</p>
123+
<div className="border-t pt-1.5 text-slate-400" style={{ borderColor: 'rgb(var(--border-rgb) / 0.3)' }}>
124+
<p className="font-semibold text-[10px]">{d.quadrant}</p>
125+
<p className="text-[9px] leading-tight mt-0.5">{q.desc}</p>
126+
<p className="text-[8px] mt-1">{q.advice}</p>
121127
</div>
122128
</div>
123129
);
@@ -142,7 +148,7 @@ export const HypertrophyScatterCard: React.FC<HypertrophyScatterCardProps> = ({
142148
</div>
143149
</div>
144150

145-
<div className="flex-1 px-3 pb-3 relative" style={{ minHeight: 350 }}>
151+
<div className="flex-1 w-full relative" style={{ minHeight: 300 }}>
146152
{hypertrophyData.length > 0 ? (
147153
<>
148154
<ResponsiveContainer width="100%" height="100%">
@@ -160,16 +166,19 @@ export const HypertrophyScatterCard: React.FC<HypertrophyScatterCardProps> = ({
160166
<RechartsTooltip content={<CustomScatterTooltip />} />
161167

162168
<Scatter data={chartData} shape="circle" isAnimationActive={false}>
163-
{chartData.map((entry) => (
164-
<Cell
165-
key={entry.muscleId}
166-
fill={QUADRANT_COLORS[entry.quadrant]}
167-
fillOpacity={0.85}
168-
stroke={QUADRANT_COLORS[entry.quadrant]}
169-
strokeWidth={0.5}
170-
style={{ cursor: 'pointer' }}
171-
/>
172-
))}
169+
{chartData.map((entry) => {
170+
const c = getDotColor(entry.total);
171+
return (
172+
<Cell
173+
key={entry.muscleId}
174+
fill={c}
175+
fillOpacity={0.85}
176+
stroke={c}
177+
strokeWidth={0.5}
178+
style={{ cursor: 'pointer' }}
179+
/>
180+
);
181+
})}
173182
</Scatter>
174183

175184
<Scatter data={chartData.filter(d => labeledIds.includes(d.muscleId))} shape="circle" isAnimationActive={false} legendType="none"
@@ -187,6 +196,7 @@ export const HypertrophyScatterCard: React.FC<HypertrophyScatterCardProps> = ({
187196
<div className="text-[10px] text-slate-500 py-4 text-center">No muscle data available.</div>
188197
)}
189198
</div>
199+
190200
</div>
191201
);
192202
};

frontend/components/muscleAnalysis/ui/LifetimeAchievementCard.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ const FactorProgressBar: React.FC<{
242242

243243
const SCATTER_DOT_COLORS = ['#22c55e', '#84cc16', '#f59e0b', '#f97316', '#ef4444'];
244244

245+
const volColor = (v: number) => v <= 15 ? '#ef4444' : v <= 35 ? '#f59e0b' : '#22c55e';
246+
const progColor = (v: number) => v <= 11 ? '#ef4444' : v <= 22 ? '#f59e0b' : '#22c55e';
247+
245248
const HypertrophyScatterChart: React.FC<{ data: MuscleHypertrophyData[] }> = ({ data }) => {
246249
const chartData = useMemo(() =>
247250
data.map(m => ({
@@ -295,8 +298,8 @@ const HypertrophyScatterChart: React.FC<{ data: MuscleHypertrophyData[] }> = ({
295298
}}>
296299
<p className="font-semibold mb-1.5">{d?.name} <span className="opacity-60 font-normal">({d?.total}/100)</span></p>
297300
<div className="flex items-center gap-3">
298-
<span style={{ color: '#3b82f6' }}>Progress <b>{d?.progress}/40</b></span>
299-
<span style={{ color: '#22c55e' }}>Volume <b>{d?.volume}/50</b></span>
301+
<span>Progress <b style={{ color: progColor(d?.progress) }}>{d?.progress}/40</b></span>
302+
<span>Volume <b style={{ color: volColor(d?.volume) }}>{d?.volume}/50</b></span>
300303
</div>
301304
</div>
302305
);

frontend/components/ui/Tooltip.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export interface TooltipData {
77
mouseX?: number;
88
mouseY?: number;
99
title: string;
10-
body: string;
10+
body?: string;
11+
bodySections?: Array<{ text: string; color: string }>;
1112
footer?: string;
1213
status: AnalysisStatus | 'default';
1314
metrics?: Array<{ label: string; value: string }>;
@@ -18,7 +19,7 @@ interface TooltipProps {
1819
}
1920

2021
export const Tooltip: React.FC<TooltipProps> = ({ data }) => {
21-
const { rect, mouseX, mouseY, title, body, footer, status, metrics } = data;
22+
const { rect, mouseX, mouseY, title, body, bodySections, footer, status, metrics } = data;
2223
const theme = TOOLTIP_THEMES[status];
2324

2425
let positionStyle;
@@ -42,7 +43,15 @@ export const Tooltip: React.FC<TooltipProps> = ({ data }) => {
4243
<div className="flex items-center gap-2 mb-1 pb-1 border-b border-white/10">
4344
<span className="font-bold uppercase text-[10px] tracking-wider">{title}</span>
4445
</div>
45-
<div className="text-xs leading-relaxed opacity-90 whitespace-pre-line break-words">{body}</div>
46+
{bodySections ? (
47+
<div className="text-xs leading-relaxed whitespace-pre-line break-words">
48+
{bodySections.map((s, i) => (
49+
<div key={i} style={{ color: s.color }}>{s.text}</div>
50+
))}
51+
</div>
52+
) : body ? (
53+
<div className="text-xs leading-relaxed opacity-90 whitespace-pre-line break-words">{body}</div>
54+
) : null}
4655
{metrics && metrics.length > 0 && (
4756
<div className="mt-3 pt-2 border-t border-white/10 flex gap-4 text-xs font-mono opacity-80">
4857
{metrics.map((m, i) => (

0 commit comments

Comments
 (0)