VO2 max gauge: Garmin/Cooper Institute thresholds, add Superior category
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 9s

- Replace ACSM estimates with exact Garmin/Cooper Institute values for 6 age
  brackets (20-29 through 70+) for both male and female
- Add Superior (≥95th percentile) as the top category in purple; rename
  Very Poor → removed, categories are now Poor/Fair/Good/Excellent/Superior
- Update getVo2Category to use lower-bound logic (count thresholds met)
  matching how Garmin publishes the data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:15:26 +01:00
parent b6f185d5e8
commit bb9e8c59f4
+23 -21
View File
@@ -21,38 +21,40 @@ const RANGES = [
// ── VO2 Max gauge ────────────────────────────────────────────────────────────
// ACSM sex-specific VO2 max thresholds
// [maxAge, [veryPoor_max, poor_max, fair_max, good_max]] (above good_max → Excellent)
// Garmin/Cooper Institute VO2 max thresholds
// [maxAge, [fair_min, good_min, excellent_min, superior_min]]
// value < fair_min → Poor; >= superior_min → Superior
const VO2_MALE = [
[29, [32, 36, 41, 45]],
[39, [30, 34, 38, 43]],
[49, [29, 33, 37, 42]],
[59, [25, 30, 34, 38]],
[69, [20, 24, 28, 32]],
[Infinity, [17, 21, 24, 28]],
[29, [41.7, 45.4, 51.1, 55.4]],
[39, [40.5, 44.0, 48.3, 54.0]],
[49, [38.5, 42.4, 46.4, 52.5]],
[59, [35.6, 39.2, 43.4, 48.9]],
[69, [32.3, 35.5, 39.5, 45.7]],
[Infinity, [29.4, 32.3, 36.7, 42.1]],
]
const VO2_FEMALE = [
[29, [27, 33, 36, 41]],
[39, [26, 30, 35, 39]],
[49, [24, 28, 32, 36]],
[59, [21, 24, 28, 32]],
[69, [18, 21, 24, 28]],
[Infinity, [15, 18, 22, 25]],
[29, [36.1, 39.5, 43.9, 49.6]],
[39, [34.4, 37.8, 42.4, 47.4]],
[49, [33.0, 36.3, 39.7, 45.3]],
[59, [30.1, 33.0, 36.7, 41.1]],
[69, [27.5, 30.0, 33.0, 37.8]],
[Infinity, [25.9, 28.1, 30.9, 36.7]],
]
const VO2_CATEGORIES = [
{ label: 'Very Poor', color: '#ef4444' },
{ label: 'Poor', color: '#f97316' },
{ label: 'Fair', color: '#22c55e' },
{ label: 'Good', color: '#3b82f6' },
{ label: 'Excellent', color: '#a855f7' },
{ label: 'Poor', color: '#ef4444' },
{ label: 'Fair', color: '#f97316' },
{ label: 'Good', color: '#22c55e' },
{ label: 'Excellent', color: '#3b82f6' },
{ label: 'Superior', color: '#a855f7' },
]
function getVo2Category(value, age, sex) {
const table = sex === 'female' ? VO2_FEMALE : VO2_MALE
const row = table.find(([maxAge]) => age <= maxAge) || table[table.length - 1]
const thresholds = row[1]
const idx = thresholds.findIndex(t => value <= t)
return idx === -1 ? VO2_CATEGORIES[4] : VO2_CATEGORIES[idx]
// thresholds are lower-bounds: count how many the value meets or exceeds
const idx = thresholds.reduce((n, t) => value >= t ? n + 1 : n, 0)
return VO2_CATEGORIES[idx]
}
function Vo2MaxGauge({ value, birthYear, biologicalSex }) {