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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }) {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<RadarChart
|
||||
axes={dimensions.map((d) => ({
|
||||
label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension,
|
||||
full: d.dimension,
|
||||
value: d.score,
|
||||
}))}
|
||||
size={104}
|
||||
/>
|
||||
<RadarChart axes={radarAxesFromDimensions(dimensions)} size={104} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<string, RadarAxis[]>();
|
||||
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
|
||||
|
||||
@@ -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() {
|
||||
</Button>
|
||||
</div>
|
||||
{scores.data && scores.data.dimensions.length >= 3 && (
|
||||
<RadarChart
|
||||
axes={scores.data.dimensions.map((d) => ({
|
||||
label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension,
|
||||
full: d.dimension,
|
||||
value: d.score,
|
||||
}))}
|
||||
size={112}
|
||||
/>
|
||||
<RadarChart axes={radarAxesFromDimensions(scores.data.dimensions)} size={112} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user