From 916aaff9f3722acc591b4396c514ae3d2ed67d14 Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Tue, 14 Apr 2026 20:37:25 +0000 Subject: [PATCH] feat: add trend history chart with strength/confidence/contradiction time series --- frontend/src/pages/CompanyDetail.tsx | 226 ++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/CompanyDetail.tsx b/frontend/src/pages/CompanyDetail.tsx index 1eb6557..d3620dc 100644 --- a/frontend/src/pages/CompanyDetail.tsx +++ b/frontend/src/pages/CompanyDetail.tsx @@ -11,10 +11,15 @@ import { useHistoricalPatterns, useCompetitiveSignals, useCorporateDecisions, + useTrends, } from '../api/hooks'; import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui'; import { DataTable, type Column } from '../components/DataTable'; -import type { Source, Alias, MacroImpactRecord, CompetitorRelationship, HistoricalPattern, CompetitiveSignal, CorporateDecision } from '../api/hooks'; +import type { Source, Alias, MacroImpactRecord, CompetitorRelationship, HistoricalPattern, CompetitiveSignal, CorporateDecision, TrendSummary } from '../api/hooks'; +import { + LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, + CartesianGrid, Legend, +} from 'recharts'; const sourceCols: Column[] = [ { key: 'source_type', header: 'Type' }, @@ -34,11 +39,12 @@ export function CompanyDetailPage() { const { data: patterns } = useHistoricalPatterns(company?.ticker); const { data: signals } = useCompetitiveSignals(company?.ticker); const { data: decisions } = useCorporateDecisions(company?.ticker); - const [tab, setTab] = useState<'sources' | 'aliases' | 'macro' | 'competitors' | 'patterns' | 'signals' | 'decisions'>('sources'); + const { data: trends } = useTrends({ ticker: company?.ticker, limit: 200 }); + const [tab, setTab] = useState<'trends' | 'sources' | 'aliases' | 'macro' | 'competitors' | 'patterns' | 'signals' | 'decisions'>('trends'); if (isLoading || !company) return ; - const tabs = ['sources', 'aliases', 'macro', 'competitors', 'patterns', 'signals', 'decisions'] as const; + const tabs = ['trends', 'sources', 'aliases', 'macro', 'competitors', 'patterns', 'signals', 'decisions'] as const; return (
@@ -71,6 +77,10 @@ export function CompanyDetailPage() { ))}
+ {tab === 'trends' && ( + + )} + {tab === 'sources' && (
data={sources ?? []} columns={sourceCols} keyField="id" /> @@ -528,3 +538,213 @@ function DecisionsPanel({ decisions }: { decisions: CorporateDecision[] }) {
); } + +// --------------------------------------------------------------------------- +// Trend History Chart +// --------------------------------------------------------------------------- + +const DIRECTION_VALUE: Record = { + bullish: 1, + bearish: -1, + mixed: 0, + neutral: 0, +}; + +const WINDOW_ORDER = ['intraday', '1d', '7d', '30d', '90d']; + +interface ChartPoint { + time: string; + timestamp: number; + strength: number; + confidence: number; + contradiction: number; + direction: number; + directionLabel: string; + window: string; +} + +function TrendHistoryChart({ trends, ticker }: { trends: TrendSummary[]; ticker: string }) { + const [selectedWindow, setSelectedWindow] = useState('7d'); + + // Filter trends for this ticker and selected window, sorted by time + const filtered = (trends ?? []) + .filter((t) => t.entity_id === ticker && t.window === selectedWindow) + .sort((a, b) => new Date(a.generated_at).getTime() - new Date(b.generated_at).getTime()); + + const chartData: ChartPoint[] = filtered.map((t) => ({ + time: new Date(t.generated_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }), + timestamp: new Date(t.generated_at).getTime(), + strength: +(t.trend_strength * 100).toFixed(1), + confidence: +(t.confidence * 100).toFixed(1), + contradiction: +(t.contradiction_score * 100).toFixed(1), + direction: DIRECTION_VALUE[t.trend_direction] ?? 0, + directionLabel: t.trend_direction, + window: t.window, + })); + + // Available windows from the data + const availableWindows = [...new Set((trends ?? []).filter((t) => t.entity_id === ticker).map((t) => t.window))]; + availableWindows.sort((a, b) => WINDOW_ORDER.indexOf(a) - WINDOW_ORDER.indexOf(b)); + + return ( +
+ {/* Window selector */} +
+ Window: + {(availableWindows.length > 0 ? availableWindows : WINDOW_ORDER).map((w) => ( + + ))} +
+ + {chartData.length === 0 ? ( + +

No trend history for {ticker} / {selectedWindow}

+
+ ) : ( + <> + {/* Trend Strength & Confidence Chart */} + +

+ Trend Strength & Confidence — {ticker} / {selectedWindow} +

+ + + + + `${v}%`} + /> + [`${value}%`, name]} + /> + + + + + + +
+ + {/* Direction Timeline */} + +

+ Direction Timeline — {ticker} / {selectedWindow} +

+
+ {chartData.map((pt, i) => { + const color = + pt.directionLabel === 'bullish' ? 'bg-green-500' : + pt.directionLabel === 'bearish' ? 'bg-red-500' : + pt.directionLabel === 'mixed' ? 'bg-yellow-500' : + 'bg-gray-600'; + const height = Math.max(8, pt.strength * 0.5); + return ( +
+
+ {i % Math.max(1, Math.floor(chartData.length / 8)) === 0 && ( + {pt.time} + )} +
+ ); + })} +
+
+ Bullish + Bearish + Mixed + Neutral +
+ + + {/* Latest trend summary */} + +

Latest Trend

+ {filtered.length > 0 && (() => { + const latest = filtered[filtered.length - 1]; + return ( +
+
+
Direction
+
+
+
+
Strength
+
+
+
+
Confidence
+
+
+
+
Contradiction
+
0.5 ? 'text-yellow-400' : 'text-gray-300'}`}> + {(latest.contradiction_score * 100).toFixed(0)}% +
+
+
+
Catalysts
+
+ {(latest.dominant_catalysts ?? []).map((c, i) => ( + {c} + ))} +
+
+
+
Generated
+
{new Date(latest.generated_at).toLocaleString()}
+
+
+ ); + })()} +
+ + )} +
+ ); +}