From e683513857922cbf3a5bd49fc154489af635be10 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 26 Jun 2026 16:22:30 +0200 Subject: [PATCH] fix: smooth the quadrant trail and fade it by recency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single solid trail line read like a tangle. Make older→newer legible: the path now fades from muted slate (older) to bright blue (newer) via per-point colors, the connecting line is faint, and the points are de-noised with a centered moving average (today kept exact). Easier to see the direction of travel through the quadrants. Co-Authored-By: Claude Opus 4.8 --- .../src/components/regime/RegimeQuadrant.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/regime/RegimeQuadrant.tsx b/frontend/src/components/regime/RegimeQuadrant.tsx index 36b18a8..ae65e71 100644 --- a/frontend/src/components/regime/RegimeQuadrant.tsx +++ b/frontend/src/components/regime/RegimeQuadrant.tsx @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { ScatterChart, Scatter, + Cell, XAxis, YAxis, ZAxis, @@ -29,6 +30,33 @@ interface QPoint { date: string; } +/** Centered moving average to de-noise the path; today (last) kept exact. */ +function smoothTrail(points: QPoint[], half = 2): QPoint[] { + const n = points.length; + return points.map((p, i) => { + if (i === n - 1) return { ...p }; + let sx = 0; + let sy = 0; + let c = 0; + for (let j = Math.max(0, i - half); j <= Math.min(n - 1, i + half); j++) { + sx += points[j].x; + sy += points[j].y; + c += 1; + } + return { x: sx / c, y: sy / c, date: p.date }; + }); +} + +/** Recency gradient: 0 = oldest (muted slate), 1 = newest (bright blue). */ +function recencyColor(t: number): string { + const lerp = (a: number, b: number) => Math.round(a + (b - a) * t); + const r = lerp(71, 96); + const g = lerp(85, 165); + const b = lerp(105, 250); + const alpha = (0.3 + 0.7 * t).toFixed(2); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + function QuadrantTip({ active, payload }: { active?: boolean; payload?: { payload: QPoint }[] }) { if (!active || !payload?.length) return null; const p = payload[0].payload; @@ -54,6 +82,7 @@ export default function RegimeQuadrant() { .map((p) => ({ x: p.index, y: p.early_warning as number, date: p.date })); }, [history.data]); + const trail = useMemo(() => smoothTrail(points), [points]); const latest = points.length ? points[points.length - 1] : null; return ( @@ -110,10 +139,18 @@ export default function RegimeQuadrant() { axisLine={false} label={{ value: 'Early warning', angle: -90, position: 'insideLeft', fill: '#6b7280', fontSize: 10 }} /> - + } /> - {/* Trail (chronological path) */} - + {/* Smoothed trail with a recency gradient (old → new) */} + + {trail.map((_, i) => ( + + ))} + {/* Today */} {latest && ( ④ Real downturn — regime breaking, broad

- White dot = today; trail = path over the last {TRAIL} sessions. The tell isn't a single spot but the - move ①→④ (early warning rolling over while the regime index climbs = divergence resolving downward). - Observational — not wired into trades. + White dot = today; the trail fades from muted (older) to bright blue (newer) over the last {TRAIL}{' '} + sessions, smoothed. The tell isn't a single spot but the move ①→④ (early warning rolling over while + the regime index climbs = divergence resolving downward). Observational — not wired into trades.

)}