From 72054d06b04250fba89bb4108773c5620c17a993 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 10 Jul 2026 15:25:26 +0200 Subject: [PATCH] Radar axes: one canonical order everywhere The dashboard fingerprints sorted score dimensions alphabetically while the ticker page rendered them in API order, so the same ticker drew a differently-shaped polygon on each page. New shared helper radarAxesFromDimensions() pins the axis order to the backend's DIMENSIONS list (technical, sr_quality, sentiment, fundamental, momentum; unknown dimensions append alphabetically) and centralizes the label truncation. Used by the dashboard fingerprint strip, the ticker header radar, and ScoreCard. Co-Authored-By: Claude Fable 5 --- frontend/src/components/charts/horizon.tsx | 21 +++++++++++++++++++++ frontend/src/components/ui/ScoreCard.tsx | 11 ++--------- frontend/src/pages/DashboardPage.tsx | 18 +++++------------- frontend/src/pages/TickerDetailPage.tsx | 11 ++--------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/charts/horizon.tsx b/frontend/src/components/charts/horizon.tsx index e69d16a..f093336 100644 --- a/frontend/src/components/charts/horizon.tsx +++ b/frontend/src/components/charts/horizon.tsx @@ -134,6 +134,27 @@ export interface RadarAxis { full?: string; } +/** Canonical axis order — mirrors the backend's DIMENSIONS list + * (app/services/scoring_service.py). Every radar in the app must use this so + * fingerprint shapes stay comparable between the dashboard and ticker pages. */ +const RADAR_DIMENSION_ORDER = ['technical', 'sr_quality', 'sentiment', 'fundamental', 'momentum']; + +export function radarAxesFromDimensions( + dimensions: { dimension: string; score: number }[], +): RadarAxis[] { + const rank = (d: string) => { + const i = RADAR_DIMENSION_ORDER.indexOf(d.toLowerCase()); + return i === -1 ? RADAR_DIMENSION_ORDER.length : i; + }; + return [...dimensions] + .sort((a, b) => rank(a.dimension) - rank(b.dimension) || a.dimension.localeCompare(b.dimension)) + .map((d) => ({ + label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension, + full: d.dimension, + value: d.score, + })); +} + export function RadarChart({ axes, size = 120, labels = true, }: { axes: RadarAxis[]; size?: number; labels?: boolean }) { diff --git a/frontend/src/components/ui/ScoreCard.tsx b/frontend/src/components/ui/ScoreCard.tsx index a932b9a..4162baa 100644 --- a/frontend/src/components/ui/ScoreCard.tsx +++ b/frontend/src/components/ui/ScoreCard.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { DimensionBreakdownPanel } from '../ticker/DimensionBreakdownPanel'; -import { RadarChart } from '../charts/horizon'; +import { RadarChart, radarAxesFromDimensions } from '../charts/horizon'; import type { DimensionScoreDetail, CompositeBreakdown } from '../../lib/types'; interface ScoreCardProps { @@ -97,14 +97,7 @@ export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, show className={showComposite ? 'ml-auto' : 'mx-auto sm:mx-0'} title="Score fingerprint — hover a corner for the exact value" > - ({ - label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension, - full: d.dimension, - value: d.score, - }))} - size={104} - /> + )} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 774331b..8446b5c 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -11,7 +11,7 @@ import { regimeColor, regimeDot, regimeHeadline } from '../lib/regime'; import { Callout } from '../components/ui/Callout'; import { Section } from '../components/ui/Section'; import { OpenTradesPanel } from '../components/dashboard/OpenTradesPanel'; -import { PriceRail, RadarChart } from '../components/charts/horizon'; +import { PriceRail, RadarChart, radarAxesFromDimensions } from '../components/charts/horizon'; import type { RadarAxis } from '../components/charts/horizon'; import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton'; import { tradePnl } from '../lib/paperTrade'; @@ -259,22 +259,14 @@ export default function DashboardPage() { }, [trades.data, activation.data]); // Mini score fingerprints for the qualified setups (dimension scores come - // from the rankings endpoint; axis order is fixed alphabetically so shapes - // stay comparable across symbols). + // from the rankings endpoint; axis order is the canonical one shared with + // the ticker page so shapes stay comparable everywhere). const rankings = useRankings(); const fingerprints = useMemo(() => { const dimsBySymbol = new Map(); for (const r of rankings.data?.rankings ?? []) { - const dims = [...r.dimensions].sort((a, b) => a.dimension.localeCompare(b.dimension)); - if (dims.length >= 3) { - dimsBySymbol.set( - r.symbol.toUpperCase(), - dims.map((d) => ({ - label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension, - full: d.dimension, - value: d.score, - })), - ); + if (r.dimensions.length >= 3) { + dimsBySymbol.set(r.symbol.toUpperCase(), radarAxesFromDimensions(r.dimensions)); } } return radar.qualified diff --git a/frontend/src/pages/TickerDetailPage.tsx b/frontend/src/pages/TickerDetailPage.tsx index a187244..5bac164 100644 --- a/frontend/src/pages/TickerDetailPage.tsx +++ b/frontend/src/pages/TickerDetailPage.tsx @@ -9,7 +9,7 @@ import { useActivation } from '../hooks/useActivation'; import { topPickSymbol, qualifiesSetup } from '../lib/qualification'; import type { FetchSelector } from '../api/ingestion'; import { CandlestickChart } from '../components/charts/CandlestickChart'; -import { RadarChart } from '../components/charts/horizon'; +import { RadarChart, radarAxesFromDimensions } from '../components/charts/horizon'; import { ScoreCard } from '../components/ui/ScoreCard'; import { useTickerNames } from '../hooks/useTickers'; import { SkeletonCard } from '../components/ui/Skeleton'; @@ -373,14 +373,7 @@ export default function TickerDetailPage() { {scores.data && scores.data.dimensions.length >= 3 && ( - ({ - label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension, - full: d.dimension, - value: d.score, - }))} - size={112} - /> + )}