Skip to content

Commit 17224c6

Browse files
committed
feat: Centralize percentage formatting with smart truncation for large delta values across all dashboard components
1 parent 082ed8c commit 17224c6

14 files changed

+308
-20
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Delta Percentage Formatting Reference
2+
3+
## Logic Explanation
4+
5+
The "x" format represents **total performance multiplier**, not the increase amount.
6+
7+
**Formula**: `Total Multiplier = 1 + (Percentage Increase / 100)`
8+
9+
## Conversion Table
10+
11+
| Percentage Increase | Total Performance | Display Format |
12+
|-------------------|-------------------|----------------|
13+
| 0% increase | 1.0x baseline | 0% (below threshold) |
14+
| 50% increase | 1.5x baseline | 50% (below threshold) |
15+
| 100% increase | 2.0x baseline | 100% (below threshold) |
16+
| 149% increase | 2.49x baseline | 149% (below threshold) |
17+
| **150% increase** | **2.5x baseline** | **2.5x**|
18+
| 175% increase | 2.75x baseline | 2.8x ✨ |
19+
| **200% increase** | **3.0x baseline** | **3x**|
20+
| 250% increase | 3.5x baseline | 3.5x ✨ |
21+
| **300% increase** | **4.0x baseline** | **4x**|
22+
23+
## Key Points
24+
25+
- **< 150%**: Shows as regular percentage (e.g., "100%", "125%")
26+
- **≥ 150%**: Shows as total multiplier (e.g., "2.5x", "3x")
27+
- **Units always present**: Either "%" or "x"
28+
- **Logic**: "x" = baseline (1x) + increase (percentage/100)
29+
30+
## Examples
31+
32+
- **100% increase** → Performance doubled → **2x total**
33+
- **200% increase** → Performance tripled → **3x total**
34+
- **300% increase** → Performance quadrupled → **4x total**
35+
36+
This makes the "x" format intuitive: it directly tells you how many times better the performance is compared to baseline.

frontend/components/HistoryView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { WeightUnit } from '../utils/storage/localStorage';
2424
import { convertWeight } from '../utils/format/units';
2525
import { formatSignedNumber } from '../utils/format/formatters';
2626
import { formatDisplayVolume } from '../utils/format/volumeDisplay';
27+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../utils/format/deltaFormat';
2728
import { formatRelativeWithDate, getEffectiveNowFromWorkoutData, getSessionKey } from '../utils/date/dateUtils';
2829
import { parseHevyDateString } from '../utils/date/parseHevyDateString';
2930
import { LazyRender } from './LazyRender';
@@ -64,15 +65,18 @@ const SessionDeltaBadge: React.FC<{ current: number; previous: number; suffix?:
6465
const isPositive = delta > 0;
6566
const Icon = isPositive ? TrendingUp : TrendingDown;
6667
const colorClass = isPositive ? 'text-emerald-400' : 'text-rose-400';
67-
const pct = Math.round((delta / previous) * 100);
68+
const deltaPercent = Math.round((delta / previous) * 100);
69+
70+
// Use centralized formatting for better UX with large percentages
71+
const formattedPercent = formatDeltaPercentage(deltaPercent, getDeltaFormatPreset('badge'));
6872

6973
return (
7074
<span
7175
className={`relative -top-[2px] inline-flex items-center gap-0.5 ml-1 text-[10px] font-bold leading-none ${colorClass}`}
72-
title={`${pct}% ${label} ${context}`}
76+
title={`${deltaPercent}% ${label} ${context}`}
7377
>
7478
<Icon className={`w-3 h-3 ${colorClass}`} />
75-
<span>{pct}%</span>
79+
<span>{formattedPercent}</span>
7680
</span>
7781
);
7882
};

frontend/components/InsightCards.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { WeightUnit } from '../utils/storage/localStorage';
1717
import { convertWeight } from '../utils/format/units';
1818
import { formatHumanReadableDate } from '../utils/date/dateUtils';
1919
import { formatNumber } from '../utils/format/formatters';
20+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../utils/format/deltaFormat';
2021

2122
// Simple monochrome SVG for Gemini (Google) that inherits color via currentColor
2223
const GeminiIcon: React.FC<{ className?: string }> = ({ className }) => (
@@ -102,15 +103,15 @@ const DeltaBadge: React.FC<{ delta: DeltaResult; suffix?: string; showPercent?:
102103
showPercent = true,
103104
context = ''
104105
}) => {
105-
const { direction, deltaPercent } = delta;
106+
const { direction, formattedPercent } = delta;
106107

107108
if (direction === 'same') {
108109
return (
109110
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-slate-500/10 text-slate-400">
110111
<Minus className="w-3 h-3" />
111112
<span className="text-[10px] font-bold">
112113
Stable
113-
{showPercent ? ` (${deltaPercent}%)` : ''}
114+
{showPercent ? ` (${delta.deltaPercent}%)` : ''}
114115
</span>
115116
{context && <span className="text-[9px] opacity-75">{context}</span>}
116117
</span>
@@ -126,8 +127,7 @@ const DeltaBadge: React.FC<{ delta: DeltaResult; suffix?: string; showPercent?:
126127
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded ${bgClass} ${colorClass}`}>
127128
<Icon className="w-3 h-3" />
128129
<span className="text-[10px] font-bold">
129-
{isUp ? '+' : ''}
130-
{showPercent ? `${deltaPercent}%` : formatNumber(delta.delta, { maxDecimals: 2 })}
130+
{formattedPercent}
131131
{suffix}
132132
</span>
133133
{context && <span className="text-[9px] opacity-75">{context}</span>}

frontend/components/MuscleAnalysis.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { TrendingUp, TrendingDown, Dumbbell, X, Activity, Layers, PersonStanding, BicepsFlexed } from 'lucide-react';
2929
import { normalizeMuscleGroup, type NormalizedMuscleGroup } from '../utils/muscle/muscleNormalization';
3030
import { LazyRender } from './LazyRender';
31+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../utils/format/deltaFormat';
3132
import { ChartSkeleton } from './ChartSkeleton';
3233
import { Tooltip as HoverTooltip, TooltipData } from './Tooltip';
3334
import { CHART_TOOLTIP_STYLE } from '../utils/ui/uiConstants';
@@ -493,11 +494,15 @@ export const MuscleAnalysis: React.FC<MuscleAnalysisProps> = ({ data, filtersSlo
493494
const delta = Number((current - previous).toFixed(1));
494495
const deltaPercent = Math.round((delta / previous) * 100);
495496

497+
// Use centralized formatting for better UX with large percentages
498+
const formattedPercent = formatDeltaPercentage(deltaPercent, getDeltaFormatPreset('badge'));
499+
496500
return {
497501
current,
498502
previous,
499503
delta,
500504
deltaPercent,
505+
formattedPercent,
501506
direction: delta > 0 ? 'up' : delta < 0 ? 'down' : 'same' as 'up' | 'down' | 'same',
502507
};
503508
}, [trendData]);
@@ -851,7 +856,7 @@ export const MuscleAnalysis: React.FC<MuscleAnalysisProps> = ({ data, filtersSlo
851856
: 'bg-rose-500/10 text-rose-400'
852857
}`}>
853858
{volumeDelta.direction === 'up' ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
854-
{volumeDelta.direction === 'up' ? '+' : ''}{volumeDelta.deltaPercent}% vs prev {trendPeriod === 'weekly' ? 'wk' : trendPeriod === 'monthly' ? 'mo' : 'day'}
859+
{volumeDelta.formattedPercent} vs prev {trendPeriod === 'weekly' ? 'wk' : trendPeriod === 'monthly' ? 'mo' : 'day'}
855860
</span>
856861
)}
857862
</div>

frontend/components/dashboard/IntensityEvolutionCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { LazyRender } from '../LazyRender';
2727
import { ChartSkeleton } from '../ChartSkeleton';
2828
import { formatNumber, formatSignedNumber } from '../../utils/format/formatters';
29+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../../utils/format/deltaFormat';
2930
import { addEmaSeries, DEFAULT_EMA_HALF_LIFE_DAYS } from '../../utils/analysis/ema';
3031

3132
type IntensityView = 'area' | 'stackedBar';
@@ -266,11 +267,11 @@ export const IntensityEvolutionCard = ({
266267
key={s.short}
267268
label={
268269
<BadgeLabel
269-
main={`${s.short} ${s.pct.toFixed(0)}%`}
270+
main={`${s.short} ${formatDeltaPercentage(s.pct, getDeltaFormatPreset('badge'))}`}
270271
meta={
271272
<ShiftedMeta>
272273
<TrendIcon direction={s.delta.direction} />
273-
<span>{`${formatSigned(s.delta.deltaPercent)}% vs prev mo`}</span>
274+
<span>{formatDeltaPercentage(s.delta.deltaPercent, getDeltaFormatPreset('badge'))} vs prev mo</span>
274275
</ShiftedMeta>
275276
}
276277
/>

frontend/components/dashboard/MuscleTrendCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ChartSkeleton } from '../ChartSkeleton';
1717
import { normalizeMuscleGroup } from '../../utils/muscle/muscleNormalization';
1818
import { MUSCLE_COLORS } from '../../utils/domain/categories';
1919
import { formatSignedNumber } from '../../utils/format/formatters';
20+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../../utils/format/deltaFormat';
2021
import {
2122
BadgeLabel,
2223
ChartDescription,
@@ -39,7 +40,7 @@ type MuscleTrendInsight = {
3940

4041
const formatSigned = (n: number) => formatSignedNumber(n, { maxDecimals: 2 });
4142
const formatSignedPctWithNoun = (pct: number, noun: string) =>
42-
`${formatSignedNumber(pct, { maxDecimals: 0 })}% ${noun}`;
43+
`${formatDeltaPercentage(pct, getDeltaFormatPreset('badge'))} ${noun}`;
4344

4445
export const MuscleTrendCard = ({
4546
isMounted,

frontend/components/dashboard/PrTrendCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'recharts';
1414
import type { TimeFilterMode } from '../../utils/storage/localStorage';
1515
import { formatSignedNumber } from '../../utils/format/formatters';
16+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../../utils/format/deltaFormat';
1617
import { addEmaSeries, DEFAULT_EMA_HALF_LIFE_DAYS } from '../../utils/analysis/ema';
1718
import {
1819
BadgeLabel,
@@ -188,7 +189,7 @@ export const PrTrendCard = ({
188189
main={
189190
<span className="inline-flex items-center gap-1">
190191
<TrendIcon direction={prTrendDelta.direction} />
191-
<span>{`${formatSigned(prTrendDelta.deltaPercent)}%`}</span>
192+
<span>{formatDeltaPercentage(prTrendDelta.deltaPercent, getDeltaFormatPreset('badge'))}</span>
192193
</span>
193194
}
194195
meta="vs prev mo"
@@ -207,7 +208,7 @@ export const PrTrendCard = ({
207208
main={
208209
<span className="inline-flex items-center gap-1">
209210
<TrendIcon direction={prTrendDelta7d.direction} />
210-
<span>{`${formatSigned(prTrendDelta7d.deltaPercent)}%`}</span>
211+
<span>{formatDeltaPercentage(prTrendDelta7d.deltaPercent, getDeltaFormatPreset('badge'))}</span>
211212
</span>
212213
}
213214
meta="vs prev 7d"

frontend/components/dashboard/TopExercisesCard.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from 'recharts';
1313
import type { ExerciseAsset } from '../../utils/data/exerciseAssets';
1414
import { formatSignedNumber } from '../../utils/format/formatters';
15+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../../utils/format/deltaFormat';
1516
import { LazyRender } from '../LazyRender';
1617
import { ChartSkeleton } from '../ChartSkeleton';
1718
import {
@@ -68,7 +69,7 @@ export const TopExercisesCard = ({
6869
assetsLowerMap: Map<string, ExerciseAsset> | null;
6970
}) => {
7071
const formatSignedPctWithNoun = (pct: number, noun: string) =>
71-
`${formatSignedNumber(pct, { maxDecimals: 0 })}% ${noun}`;
72+
`${formatDeltaPercentage(pct, getDeltaFormatPreset('badge'))} ${noun}`;
7273

7374
const pie = pieColors;
7475

@@ -451,7 +452,7 @@ export const TopExercisesCard = ({
451452
)}
452453
{topExercisesInsight.top && (
453454
<TrendBadge
454-
label={<BadgeLabel main={`${topExercisesInsight.topShare.toFixed(0)}%`} meta="of shown" />}
455+
label={<BadgeLabel main={`${formatDeltaPercentage(topExercisesInsight.topShare)}`} meta="of shown" />}
455456
tone="neutral"
456457
/>
457458
)}
@@ -481,7 +482,7 @@ export const TopExercisesCard = ({
481482
)}
482483
{topExercisesInsight.top && (
483484
<TrendBadge
484-
label={<BadgeLabel main={`${topExercisesInsight.topShare.toFixed(0)}%`} meta="of shown" />}
485+
label={<BadgeLabel main={`${formatDeltaPercentage(topExercisesInsight.topShare)}`} meta="of shown" />}
485486
tone={topExercisesInsight.topShare >= 45 ? 'bad' : topExercisesInsight.topShare >= 30 ? 'neutral' : 'good'}
486487
/>
487488
)}

frontend/components/dashboard/VolumeDensityCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'recharts';
1414
import type { TimeFilterMode, WeightUnit } from '../../utils/storage/localStorage';
1515
import { formatNumber, formatSignedNumber } from '../../utils/format/formatters';
16+
import { formatDeltaPercentage, getDeltaFormatPreset } from '../../utils/format/deltaFormat';
1617
import { addEmaSeries, DEFAULT_EMA_HALF_LIFE_DAYS } from '../../utils/analysis/ema';
1718
import {
1819
BadgeLabel,
@@ -208,7 +209,7 @@ export const VolumeDensityCard = ({
208209
main={
209210
<span className="inline-flex items-center gap-1">
210211
<TrendIcon direction={volumeDensityTrend.delta.direction} />
211-
<span>{`${formatSigned(volumeDensityTrend.delta.deltaPercent)}%`}</span>
212+
<span>{formatDeltaPercentage(volumeDensityTrend.delta.deltaPercent, getDeltaFormatPreset('badge'))}</span>
212213
</span>
213214
}
214215
meta="vs prev mo"

frontend/components/dashboard/WeeklyRhythmCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
XAxis,
1515
YAxis,
1616
} from 'recharts';
17+
import { formatDeltaPercentage } from '../../utils/format/deltaFormat';
1718
import {
1819
BadgeLabel,
1920
ChartDescription,
@@ -124,8 +125,8 @@ export const WeeklyRhythmCard = ({
124125
<InsightLine>
125126
{weeklyRhythmInsight ? (
126127
<>
127-
<TrendBadge label={<BadgeLabel main={`Top ${weeklyRhythmInsight.top.subject} ${weeklyRhythmInsight.top.share.toFixed(0)}%`} />} tone="info" />
128-
<TrendBadge label={<BadgeLabel main={`Low ${weeklyRhythmInsight.bottom.subject} ${weeklyRhythmInsight.bottom.share.toFixed(0)}%`} />} tone="neutral" />
128+
<TrendBadge label={<BadgeLabel main={`Top ${weeklyRhythmInsight.top.subject} ${formatDeltaPercentage(weeklyRhythmInsight.top.share)}`} />} tone="info" />
129+
<TrendBadge label={<BadgeLabel main={`Low ${weeklyRhythmInsight.bottom.subject} ${formatDeltaPercentage(weeklyRhythmInsight.bottom.share)}`} />} tone="neutral" />
129130
<TrendBadge label={<BadgeLabel main={weeklyRhythmInsight.rhythmLabel} />} tone={weeklyRhythmInsight.rhythmTone} />
130131
</>
131132
) : (

0 commit comments

Comments
 (0)