// Detailed view for one axis. Reads live data from the parent // (an `axis` row produced by deriveAxisRows() in index.html) and from // the global CHVault for axis history + recent signal events. const AxisDetail = ({ axis, onBack, onLog }) => { const A = axis; if (!A) return null; const [history, setHistory] = React.useState(null); // [{date, score}] const [error, setError] = React.useState(null); React.useEffect(() => { let dead = false; (async () => { try { // /api/v1/health/axes returns history with combined CM/CH per day; // for per-axis trends we use the daily axis snapshot endpoint. const r = await fetch(window.CHVault.apiRoot + '/api/v1/health/axis/' + A.code + '/history?range=30D', { credentials: 'include', headers: { 'Accept': 'application/json' }, }); if (!r.ok) throw new Error('history-' + r.status); const j = await r.json(); if (dead) return; const arr = (j.data || j.history || j.points || []).map(p => ({ date: p.date || p.snapshot_date, score: typeof p.score === 'number' ? p.score : (typeof p.value === 'number' ? p.value : (p.axis_value || 0)), })); setHistory(arr); } catch (e) { if (dead) return; setError(e.message || String(e)); } })(); return () => { dead = true; }; }, [A.code]); /* --- Trend chart (live, falls back to flat if no history yet) Canonical 0..100 scale. Legacy /health/axis/.../history may return 0..10 values — detect via the global max and scale up so the chart and the score header agree. */ const W = 720, H = 200, P = 12; let rawSeries = (history && history.length) ? history.map(h => Number(h.score) || 0) : null; if (rawSeries && rawSeries.length) { const seriesMax = Math.max(...rawSeries); const isLegacy10 = seriesMax <= 10; // legacy endpoint returns 0..10 rawSeries = rawSeries.map(v => Math.max(0, Math.min(100, isLegacy10 ? v * 10 : v))); } let series = rawSeries && rawSeries.length ? rawSeries : [A.value]; const min = Math.min(...series); const max = Math.max(...series); const pts = series.map((v, i) => { const x = P + (series.length === 1 ? (W - P*2)/2 : (i / (series.length - 1)) * (W - P*2)); const y = P + (1 - v / 100) * (H - P*2); return [x, y]; }); const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const area = pts.length > 1 ? `${path} L${pts[pts.length - 1][0]},${H - P} L${pts[0][0]},${H - P} Z` : ''; /* --- Sub-signal contributions (live, from /master-equation/me/axes signal_scores) --- */ const signalScores = A.signal_scores || {}; const signalRows = Object.keys(signalScores).map(sid => ({ id: sid, score: signalScores[sid], })).sort((a,b) => b.score - a.score); const decayColor = A.decay.status === 'decaying' ? 'var(--accent-coral)' : A.decay.status === 'grace' ? 'var(--bracket-poor)' : A.decay.status === 'unknown' ? 'var(--fg-3)' : 'var(--accent-clinical)'; const valueLabel = A.coverage > 0 ? A.value.toFixed(1) : '—'; return (
{/* Header */}
Axis · {A.code}

{A.name}

{A.tagline}
{A.role}
{valueLabel}
● {A.decay.label.toUpperCase()}
{Math.round(A.coverage*100)}% COVERAGE
{/* Trend chart */}
Trend · 30 days

How the axis has moved

min {min.toFixed(1)} max {max.toFixed(1)} now {A.value.toFixed(1)} / 100
{history === null && !error && (
loading axis history…
)} {history !== null && history.length === 0 && (
No historical snapshots yet for {A.name}. Log a few signals or sync your wearable and the trend will appear here within a day.
)} {history !== null && history.length > 0 && ( {[20, 40, 60, 80].map(y => { const yPos = P + (1 - y / 100) * (H - P * 2); return ; })} {pts.length > 1 && } {pts.length > 0 && } )}
{/* Sub-signal contributions */}
Sub-signals · live

What this axis is built from

{signalRows.length === 0 ? (
No sub-signal data observed yet for {A.name}. Open the sub-signal catalog to see what's tracked.
) : (
{signalRows.map((r, i) => { const pct = Math.max(0, Math.min(100, r.score)); return (
{r.id}
{pct.toFixed(0)}
); })}
)}
{/* Axis context */}
Role · Master Equation

Why {A.code} matters

{A.code === 'PO' && 'PO is half of S (the somatic foundation). Strong PO + strong NM gives the left wing its leverage.'} {A.code === 'NM' && 'NM is the other half of S. Combined with PO it sets how much consistency the rest of your CH can rest on.'} {A.code === 'ER' && 'ER is one of the two resilience factors. It enters the equation as (ER × RS)^(C/3) — so when consistency is high, resilience compounds.'} {A.code === 'SC' && 'SC is the coherence amplifier (Sp in the formula). Strong social fabric multiplies everything else.'} {A.code === 'RS' && 'RS is the other resilience factor. Together with ER it forms the resilience wing.'} {A.code === 'ES' && 'ES is the environmental addend. Together with TA it forms the base of the right wing (T + E)^p.'} {A.code === 'TA' && 'TA is the technological addend in the right wing. Healthy tooling and data hygiene compound your other axes.'} {A.code === 'PV' && 'PV is the right-wing exponent (p). Stronger purpose makes T + E count for more.'}
Explore in calculator →
); }; window.AxisDetail = AxisDetail;