From 1f08820f116f83f4a6d2918dfc0a1dba54cab032 Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Thu, 30 Apr 2026 22:44:00 +0000 Subject: [PATCH] feat: fullscreen chart expand + market open/close vertical markers - Click 'Expand' button to view the trend chart fullscreen - Press Escape or click outside to close - Fullscreen uses full viewport height for better readability - Green dashed vertical lines at 9:30 AM ET (market open) - Red dashed vertical lines at 4:00 PM ET (market close) - Markers only shown on intraday and 1d windows (hidden on 7d+) - Chart content extracted into shared function to avoid duplication --- frontend/src/pages/CompanyDetail.tsx | 258 ++++++++++++++++----------- 1 file changed, 155 insertions(+), 103 deletions(-) diff --git a/frontend/src/pages/CompanyDetail.tsx b/frontend/src/pages/CompanyDetail.tsx index ec54a30..fa97ed3 100644 --- a/frontend/src/pages/CompanyDetail.tsx +++ b/frontend/src/pages/CompanyDetail.tsx @@ -1,5 +1,5 @@ import { useParams, useNavigate, Link } from '@tanstack/react-router'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useCompany, useCompanySources, @@ -750,6 +750,34 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90 const hasPrice = chartData.some((pt) => pt.price != null); + // Compute market open/close vertical markers for intraday and 1d windows + const showMarketMarkers = selectedWindow === 'intraday' || selectedWindow === '1d'; + const marketMarkers: { ts: number; label: string }[] = []; + if (showMarketMarkers && chartData.length > 0) { + const minTs = chartData[0].timestamp; + const maxTs = chartData[chartData.length - 1].timestamp; + // Walk each day in the range and compute 9:30 AM ET (open) and 4:00 PM ET (close) + const dayMs = 86400_000; + const startDay = new Date(minTs); + startDay.setUTCHours(0, 0, 0, 0); + for (let d = startDay.getTime(); d <= maxTs + dayMs; d += dayMs) { + const date = new Date(d); + const dow = date.getUTCDay(); + if (dow === 0 || dow === 6) continue; // skip weekends + // ET offset: EDT = UTC-4, EST = UTC-5. Approximate with -4 (summer). + // 9:30 AM ET = 13:30 UTC (EDT) + const openTs = d + 13 * 3600_000 + 30 * 60_000; + // 4:00 PM ET = 20:00 UTC (EDT) + const closeTs = d + 20 * 3600_000; + if (openTs >= minTs && openTs <= maxTs) { + marketMarkers.push({ ts: openTs, label: 'Open' }); + } + if (closeTs >= minTs && closeTs <= maxTs) { + marketMarkers.push({ ts: closeTs, label: 'Close' }); + } + } + } + // Available windows from the data (check both history and latest) const allTrends = [...(trends ?? []), ...(latestTrends ?? [])]; const availableWindows = [...new Set(allTrends.filter((t) => t.entity_id === ticker).map((t) => t.window))]; @@ -761,6 +789,93 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90 .sort((a, b) => new Date(b.generated_at).getTime() - new Date(a.generated_at).getTime()); const latest = latestForWindow[0] ?? (filtered.length > 0 ? filtered[filtered.length - 1] : null); + const [fullscreen, setFullscreen] = useState(false); + + // Close fullscreen on Escape key + useEffect(() => { + if (!fullscreen) return; + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setFullscreen(false); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [fullscreen]); + + // Shared chart content — rendered at different sizes + const chartContent = (height: number) => ( + + + + {marketMarkers.map((m, i) => ( + + ))} + } + tickLine={{ stroke: '#475569' }} + tickCount={8} + /> + `${v}%`} + /> + {hasPrice && ( + `$${v}`} + domain={[ + range90d.low != null ? Math.floor(range90d.low * 0.97) : 'dataMin - 2', + range90d.high != null ? Math.ceil(range90d.high * 1.03) : 'dataMax + 2', + ]} + /> + )} + + + {hasPrice && range90d.high != null && ( + + )} + {hasPrice && range90d.low != null && ( + + )} + + + + {hasPrice && ( + + )} + + + ); + return (
{/* Window selector */} @@ -789,110 +904,47 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90 <> {/* Trend Strength & Confidence Chart */} -

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

- - - - } - tickLine={{ stroke: '#475569' }} - tickCount={8} - /> - `${v}%`} - /> - {hasPrice && ( - `$${v}`} - domain={[ - range90d.low != null ? Math.floor(range90d.low * 0.97) : 'dataMin - 2', - range90d.high != null ? Math.ceil(range90d.high * 1.03) : 'dataMax + 2', - ]} - /> - )} - - - {hasPrice && range90d.high != null && ( - - )} - {hasPrice && range90d.low != null && ( - - )} - - - - {hasPrice && ( - - )} - - +
+

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

+ +
+ {chartContent(280)}
+ {/* Fullscreen overlay */} + {fullscreen && ( +
{ if (e.target === e.currentTarget) setFullscreen(false); }} + role="dialog" + aria-label="Expanded chart" + > +
+

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

+ +
+
+ {chartContent(Math.max(400, window.innerHeight - 160))} +
+
+ )} + + {/* Direction Timeline */} {/* Direction Timeline */}