fix: smooth the quadrant trail and fade it by recency
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:22:30 +02:00
parent a07bfee6e6
commit e683513857
@@ -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 }}
/>
<ZAxis range={[16, 16]} />
<ZAxis range={[13, 13]} />
<Tooltip cursor={{ strokeDasharray: '3 3', stroke: 'rgba(255,255,255,0.2)' }} content={<QuadrantTip />} />
{/* Trail (chronological path) */}
<Scatter data={points} fill="#60a5fa" fillOpacity={0.5} line={{ stroke: 'rgba(96,165,250,0.3)' }} isAnimationActive={false} />
{/* Smoothed trail with a recency gradient (old → new) */}
<Scatter
data={trail}
line={{ stroke: 'rgba(96,165,250,0.18)', strokeWidth: 1.5 }}
isAnimationActive={false}
>
{trail.map((_, i) => (
<Cell key={i} fill={recencyColor(trail.length <= 1 ? 1 : i / (trail.length - 1))} />
))}
</Scatter>
{/* Today */}
{latest && (
<Scatter
@@ -135,9 +172,9 @@ export default function RegimeQuadrant() {
<span><span className="text-red-400"> Real downturn</span> regime breaking, broad</span>
</div>
<p className="mt-2 text-[11px] leading-relaxed text-gray-600">
White dot = today; trail = path over the last {TRAIL} sessions. The tell isn&apos;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&apos;t a single spot but the move (early warning rolling over while
the regime index climbs = divergence resolving downward). Observational not wired into trades.
</p>
</>
)}