diff --git a/frontend/src/pages/HealthPage.jsx b/frontend/src/pages/HealthPage.jsx index a0f4f67..74402a5 100644 --- a/frontend/src/pages/HealthPage.jsx +++ b/frontend/src/pages/HealthPage.jsx @@ -56,83 +56,79 @@ function getVo2Category(value, age, sex) { } function Vo2MaxGauge({ value, birthYear, biologicalSex }) { - // Fixed display range: 30–70 spans the full 180° semicircle const MIN = 30, MAX = 70 - const cx = 70, cy = 68, r = 50, sw = 12 + // cx/cy = centre of the semicircle; arc goes left→top→right (sweep=1, clockwise in SVG) + const cx = 70, cy = 74, r = 50, sw = 11 const age = birthYear ? new Date().getFullYear() - birthYear : 40 - // Map a VO2 value to standard-math angle (PI=left/30, 0=right/70) + // Standard-math angle: PI = left (VO2 30), 0 = right (VO2 70) const toAngle = v => Math.PI * (1 - Math.max(0, Math.min(1, (v - MIN) / (MAX - MIN)))) - // Map a VO2 value to SVG [x, y] on the arc. - // Uses sweep=0 (counter-clockwise) so the arc goes left→top→right. + // SVG coordinates for a VO2 value at a given radius from centre const toXY = (v, radius = r) => { const a = toAngle(v) return [cx + radius * Math.cos(a), cy - radius * Math.sin(a)] } - // SVG arc path from VO2 v1 to v2, sweep=0 (upper / counter-clockwise) - const seg = (v1, v2) => { - const [x1, y1] = toXY(v1) - const [x2, y2] = toXY(v2) + // Arc path from VO2 v1 to v2; sweep=1 → clockwise = upper semicircle in SVG + const arc = (v1, v2, radius = r) => { + const [x1, y1] = toXY(v1, radius) + const [x2, y2] = toXY(v2, radius) const large = (v2 - v1) / (MAX - MIN) > 0.5 ? 1 : 0 - return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}` + return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${radius} ${radius} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}` } - // Category segment boundaries from the age/sex ACSM table + // ACSM category boundaries for this user's age/sex const table = biologicalSex === 'female' ? VO2_FEMALE : VO2_MALE const row = table.find(([maxAge]) => age <= maxAge) || table[table.length - 1] const thresholds = row[1] - const bounds = [MIN, ...thresholds, MAX] // 6 boundary values for 5 segments + const bounds = [MIN, ...thresholds, MAX] // 6 boundary values for 5 colour bands const cat = value != null ? getVo2Category(value, age, biologicalSex) : null - // Triangular arrow pointer: tip points outward from arc, base sits inside track - const pointerPts = value != null ? (() => { + // Small white triangle: base outside the arc, tip touching the outer edge — points inward + const arrowPts = value != null ? (() => { const a = toAngle(Math.max(MIN, Math.min(MAX, value))) - const s = 0.13 // half-spread in radians (~7.5°) - const tipX = cx + (r + 9) * Math.cos(a), tipY = cy - (r + 9) * Math.sin(a) - const baseX = r - 5 - const b1x = cx + baseX * Math.cos(a + s), b1y = cy - baseX * Math.sin(a + s) - const b2x = cx + baseX * Math.cos(a - s), b2y = cy - baseX * Math.sin(a - s) + const outerEdge = r + sw / 2 // outer surface of the track + const tipR = outerEdge + 1 // tip just outside the track surface + const baseR = outerEdge + 12 // base further out + const s = 0.11 // half-spread ≈ 6° + const tipX = cx + tipR * Math.cos(a), tipY = cy - tipR * Math.sin(a) + const b1x = cx + baseR * Math.cos(a + s), b1y = cy - baseR * Math.sin(a + s) + const b2x = cx + baseR * Math.cos(a - s), b2y = cy - baseR * Math.sin(a - s) return `${tipX.toFixed(1)},${tipY.toFixed(1)} ${b1x.toFixed(1)},${b1y.toFixed(1)} ${b2x.toFixed(1)},${b2y.toFixed(1)}` })() : null return (