fix: smooth the quadrant trail and fade it by recency
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:
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import {
|
import {
|
||||||
ScatterChart,
|
ScatterChart,
|
||||||
Scatter,
|
Scatter,
|
||||||
|
Cell,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
ZAxis,
|
ZAxis,
|
||||||
@@ -29,6 +30,33 @@ interface QPoint {
|
|||||||
date: string;
|
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 }[] }) {
|
function QuadrantTip({ active, payload }: { active?: boolean; payload?: { payload: QPoint }[] }) {
|
||||||
if (!active || !payload?.length) return null;
|
if (!active || !payload?.length) return null;
|
||||||
const p = payload[0].payload;
|
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 }));
|
.map((p) => ({ x: p.index, y: p.early_warning as number, date: p.date }));
|
||||||
}, [history.data]);
|
}, [history.data]);
|
||||||
|
|
||||||
|
const trail = useMemo(() => smoothTrail(points), [points]);
|
||||||
const latest = points.length ? points[points.length - 1] : null;
|
const latest = points.length ? points[points.length - 1] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -110,10 +139,18 @@ export default function RegimeQuadrant() {
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
label={{ value: 'Early warning', angle: -90, position: 'insideLeft', fill: '#6b7280', fontSize: 10 }}
|
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 />} />
|
<Tooltip cursor={{ strokeDasharray: '3 3', stroke: 'rgba(255,255,255,0.2)' }} content={<QuadrantTip />} />
|
||||||
{/* Trail (chronological path) */}
|
{/* Smoothed trail with a recency gradient (old → new) */}
|
||||||
<Scatter data={points} fill="#60a5fa" fillOpacity={0.5} line={{ stroke: 'rgba(96,165,250,0.3)' }} isAnimationActive={false} />
|
<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 */}
|
{/* Today */}
|
||||||
{latest && (
|
{latest && (
|
||||||
<Scatter
|
<Scatter
|
||||||
@@ -135,9 +172,9 @@ export default function RegimeQuadrant() {
|
|||||||
<span><span className="text-red-400">④ Real downturn</span> — regime breaking, broad</span>
|
<span><span className="text-red-400">④ Real downturn</span> — regime breaking, broad</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[11px] leading-relaxed text-gray-600">
|
<p className="mt-2 text-[11px] leading-relaxed text-gray-600">
|
||||||
White dot = today; trail = path over the last {TRAIL} sessions. The tell isn't a single spot but the
|
White dot = today; the trail fades from muted (older) to bright blue (newer) over the last {TRAIL}{' '}
|
||||||
move ①→④ (early warning rolling over while the regime index climbs = divergence resolving downward).
|
sessions, smoothed. The tell isn't a single spot but the move ①→④ (early warning rolling over while
|
||||||
Observational — not wired into trades.
|
the regime index climbs = divergence resolving downward). Observational — not wired into trades.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user