feat: fullscreen chart expand + market open/close vertical markers
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
- 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
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useParams, useNavigate, Link } from '@tanstack/react-router';
|
import { useParams, useNavigate, Link } from '@tanstack/react-router';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
useCompany,
|
useCompany,
|
||||||
useCompanySources,
|
useCompanySources,
|
||||||
@@ -750,6 +750,34 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90
|
|||||||
|
|
||||||
const hasPrice = chartData.some((pt) => pt.price != null);
|
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)
|
// Available windows from the data (check both history and latest)
|
||||||
const allTrends = [...(trends ?? []), ...(latestTrends ?? [])];
|
const allTrends = [...(trends ?? []), ...(latestTrends ?? [])];
|
||||||
const availableWindows = [...new Set(allTrends.filter((t) => t.entity_id === ticker).map((t) => t.window))];
|
const availableWindows = [...new Set(allTrends.filter((t) => t.entity_id === ticker).map((t) => t.window))];
|
||||||
@@ -761,40 +789,32 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90
|
|||||||
.sort((a, b) => new Date(b.generated_at).getTime() - new Date(a.generated_at).getTime());
|
.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 latest = latestForWindow[0] ?? (filtered.length > 0 ? filtered[filtered.length - 1] : null);
|
||||||
|
|
||||||
return (
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Window selector */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-sm text-gray-400">Window:</span>
|
|
||||||
{(availableWindows.length > 0 ? availableWindows : WINDOW_ORDER).map((w) => (
|
|
||||||
<button
|
|
||||||
key={w}
|
|
||||||
onClick={() => onWindowChange(w)}
|
|
||||||
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
|
||||||
selectedWindow === w
|
|
||||||
? 'bg-brand-600 text-white'
|
|
||||||
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{w}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{chartData.length === 0 ? (
|
// Close fullscreen on Escape key
|
||||||
<Card>
|
useEffect(() => {
|
||||||
<p className="text-sm text-gray-500">No trend history for {ticker} / {selectedWindow}</p>
|
if (!fullscreen) return;
|
||||||
</Card>
|
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setFullscreen(false); };
|
||||||
) : (
|
window.addEventListener('keydown', handler);
|
||||||
<>
|
return () => window.removeEventListener('keydown', handler);
|
||||||
{/* Trend Strength & Confidence Chart */}
|
}, [fullscreen]);
|
||||||
<Card>
|
|
||||||
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
// Shared chart content — rendered at different sizes
|
||||||
Trend Strength & Confidence — {ticker} / {selectedWindow}
|
const chartContent = (height: number) => (
|
||||||
</h2>
|
<ResponsiveContainer width="100%" height={height}>
|
||||||
<ResponsiveContainer width="100%" height={280}>
|
|
||||||
<LineChart data={chartData} margin={{ top: 5, right: 20, bottom: 40, left: 0 }}>
|
<LineChart data={chartData} margin={{ top: 5, right: 20, bottom: 40, left: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||||
|
{marketMarkers.map((m, i) => (
|
||||||
|
<ReferenceLine
|
||||||
|
key={`market-${i}`}
|
||||||
|
x={m.ts}
|
||||||
|
stroke={m.label === 'Open' ? '#22c55e' : '#ef4444'}
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeOpacity={0.5}
|
||||||
|
label={{ value: m.label, position: 'top', fill: m.label === 'Open' ? '#22c55e' : '#ef4444', fontSize: 9 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="timestamp"
|
dataKey="timestamp"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -846,53 +866,85 @@ function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices, range90
|
|||||||
label={{ value: `90d Low $${range90d.low.toFixed(2)}`, position: 'insideBottomRight', fill: '#ef4444', fontSize: 10 }}
|
label={{ value: `90d Low $${range90d.low.toFixed(2)}`, position: 'insideBottomRight', fill: '#ef4444', fontSize: 10 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Line
|
<Line yAxisId="left" type="monotone" dataKey="strength" name="Trend Strength" stroke="#3b82f6" strokeWidth={2} dot={{ r: 3, fill: '#3b82f6' }} activeDot={{ r: 5 }} />
|
||||||
yAxisId="left"
|
<Line yAxisId="left" type="monotone" dataKey="confidence" name="Confidence" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
|
||||||
type="monotone"
|
<Line yAxisId="left" type="monotone" dataKey="contradiction" name="Contradiction" stroke="#f59e0b" strokeWidth={1.5} strokeDasharray="5 5" dot={{ r: 2, fill: '#f59e0b' }} />
|
||||||
dataKey="strength"
|
|
||||||
name="Trend Strength"
|
|
||||||
stroke="#3b82f6"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={{ r: 3, fill: '#3b82f6' }}
|
|
||||||
activeDot={{ r: 5 }}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="confidence"
|
|
||||||
name="Confidence"
|
|
||||||
stroke="#10b981"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={{ r: 3, fill: '#10b981' }}
|
|
||||||
activeDot={{ r: 5 }}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="contradiction"
|
|
||||||
name="Contradiction"
|
|
||||||
stroke="#f59e0b"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
strokeDasharray="5 5"
|
|
||||||
dot={{ r: 2, fill: '#f59e0b' }}
|
|
||||||
/>
|
|
||||||
{hasPrice && (
|
{hasPrice && (
|
||||||
<Line
|
<Line yAxisId="right" type="monotone" dataKey="price" name="Price" stroke="#e879f9" strokeWidth={2} dot={{ r: 3, fill: '#e879f9' }} activeDot={{ r: 5 }} connectNulls />
|
||||||
yAxisId="right"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="price"
|
|
||||||
name="Price"
|
|
||||||
stroke="#e879f9"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={{ r: 3, fill: '#e879f9' }}
|
|
||||||
activeDot={{ r: 5 }}
|
|
||||||
connectNulls
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Window selector */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-400">Window:</span>
|
||||||
|
{(availableWindows.length > 0 ? availableWindows : WINDOW_ORDER).map((w) => (
|
||||||
|
<button
|
||||||
|
key={w}
|
||||||
|
onClick={() => onWindowChange(w)}
|
||||||
|
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||||
|
selectedWindow === w
|
||||||
|
? 'bg-brand-600 text-white'
|
||||||
|
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{w}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{chartData.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<p className="text-sm text-gray-500">No trend history for {ticker} / {selectedWindow}</p>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Trend Strength & Confidence Chart */}
|
||||||
|
<Card>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<h2 className="text-sm font-medium text-gray-400">
|
||||||
|
Trend Strength & Confidence — {ticker} / {selectedWindow}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setFullscreen(true)}
|
||||||
|
className="rounded-md border border-surface-700 px-2 py-1 text-xs text-gray-400 hover:bg-surface-800 hover:text-gray-200"
|
||||||
|
title="Expand chart"
|
||||||
|
>
|
||||||
|
⛶ Expand
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{chartContent(280)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Fullscreen overlay */}
|
||||||
|
{fullscreen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex flex-col bg-surface-950/95 p-6"
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) setFullscreen(false); }}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Expanded chart"
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-medium text-gray-200">
|
||||||
|
Trend Strength & Confidence — {ticker} / {selectedWindow}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setFullscreen(false)}
|
||||||
|
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800 hover:text-gray-200"
|
||||||
|
>
|
||||||
|
✕ Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
{chartContent(Math.max(400, window.innerHeight - 160))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Direction Timeline */}
|
||||||
{/* Direction Timeline */}
|
{/* Direction Timeline */}
|
||||||
<Card>
|
<Card>
|
||||||
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
||||||
|
|||||||
Reference in New Issue
Block a user