diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-27 22:44:20 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-27 22:44:20 -0600 |
commit | 37787895d56888ab44362252f21fb05c05e97250 (patch) | |
tree | 5ff7eaababa6fffd5dc950920f521c5f242010af /client/src/MarketDataPage.jsx | |
parent | f32142947b853076889801913d47b8c2c0f4f456 (diff) |
reorganize
Diffstat (limited to 'client/src/MarketDataPage.jsx')
-rw-r--r-- | client/src/MarketDataPage.jsx | 320 |
1 files changed, 0 insertions, 320 deletions
diff --git a/client/src/MarketDataPage.jsx b/client/src/MarketDataPage.jsx deleted file mode 100644 index 5e028f8..0000000 --- a/client/src/MarketDataPage.jsx +++ /dev/null @@ -1,320 +0,0 @@ -import React, { useEffect, useState, useContext, useRef, useMemo } from 'react'; -import { Typography, Spin, Message, Card, Grid } from '@arco-design/web-react'; -import '@arco-design/web-react/dist/css/arco.css'; -import { initVChartArcoTheme } from '@visactor/vchart-arco-theme'; -import { VChart } from '@visactor/react-vchart'; -import API_BASE_URL from './config'; -import { MarketContext, MARKET_FULL_NAMES } from './App'; - -// TODO: -// -// - [ ] Cancel Promise if market changes -// - [ ] Fix Data Zoom being reset - -const { Row, Col } = Grid; - -function TimeAgo({ timestamp, prefix }) { - const [now, setNow] = useState(new Date()); - - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000); - return () => clearInterval(timer); - }, []); - - const formatTimeAgo = () => { - if (!timestamp) return ''; - const seconds = Math.floor((now - timestamp) / 1000); - if (seconds < 60) return `${seconds} sec ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} min ago`; - const hours = Math.floor(minutes / 60); - return `${hours} hr ago`; - }; - - return ( - <Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 8 }}> - {prefix}: {formatTimeAgo()} - </Typography.Text> - ); -} - -function MarketDataPage() { - const { selectedMarket } = useContext(MarketContext); - const fiveMinChartRef = useRef(null); - const dayAheadChartRef = useRef(null); - const [fiveMinZoom, setFiveMinZoom] = useState({ start: 0.9, end: 1 }); - const [dayAheadZoom, setDayAheadZoom] = useState({ start: 0.05, end: 1 }); - const [fiveMinData, setFiveMinData] = useState([]); - const [dayAheadData, setDayAheadData] = useState([]); - const [loading, setLoading] = useState(true); - const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null); - const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null); - const [, setNow] = useState(new Date()); - - const handleZoomChange = (e, title) => { - if (!e?.start || !e?.end) return; - if (title === 'fiveMin') { - setFiveMinZoom({ start: e.start, end: e.end }); - } else { - setDayAheadZoom({ start: e.start, end: e.end }); - } - }; - - useEffect(() => { - const fetchFiveMinData = async () => { - try { - const res = await fetch(`${API_BASE_URL}/market/real-time?market=${selectedMarket}`); - if (!res.ok) throw new Error('Failed to fetch real-time data'); - const json = await res.json(); - setFiveMinData(json); - setFiveMinLastUpdated(new Date()); - } catch (err) { - console.error(err); - Message.error('Failed to load real-time market data.'); - } - }; - - const fetchDayAheadData = async () => { - try { - const res = await fetch(`${API_BASE_URL}/market/day-ahead?market=${selectedMarket}`); - if (!res.ok) throw new Error('Failed to fetch day-ahead data'); - const json = await res.json(); - setDayAheadData(json); - setDayAheadLastUpdated(new Date()); - } catch (err) { - console.error(err); - Message.error('Failed to load day-ahead market data.'); - } - }; - - const initialize = async () => { - setLoading(true); - await Promise.all([fetchFiveMinData(), fetchDayAheadData()]); - setLoading(false); - - initVChartArcoTheme({ - defaultMode: 'light', - isWatchingMode: true - }); - }; - - initialize(); - - const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); - const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); - const timer = setInterval(() => setNow(new Date()), 1000); - - return () => { - clearInterval(fiveMinInterval); - clearInterval(dayAheadInterval); - clearInterval(timer); - }; - }, [selectedMarket]); - - const fiveMinSpec = useMemo(() => ({ - type: 'line', - data: { - values: fiveMinData.map(d => ({ - timestamp: new Date(d.timestamp).toLocaleString(), - LMP: d.lmp, - Energy: d.energy, - Congestion: d.congestion, - Loss: d.loss - })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] - }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', - smooth: true, - legend: { position: 'top' }, - tooltip: { - formatter: (datum) => ({ - name: datum.name, - value: datum.value.toFixed(2) - }) - }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: fiveMinZoom.start, - end: fiveMinZoom.end - }] - }), [fiveMinData, fiveMinZoom]); // <== DEPENDENCIES - - const dayAheadSpec = useMemo(() => ({ - type: 'line', - data: { - values: dayAheadData.map(d => ({ - timestamp: new Date(d.timestamp).toLocaleString(), - LMP: d.lmp, - Energy: d.energy, - Congestion: d.congestion, - Loss: d.loss - })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] - }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', - smooth: true, - legend: { position: 'top' }, - tooltip: { - formatter: (datum) => ({ - name: datum.name, - value: datum.value.toFixed(2) - }) - }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: dayAheadZoom.start, - end: dayAheadZoom.end - }] - }), [dayAheadData, dayAheadZoom]); - - const computeKPIs = (data) => { - if (!data.length) return { avgLmp: 0, totalEnergy: 0, maxCongestion: 0, avgLoss: 0 }; - const avgLmp = data.reduce((sum, d) => sum + d.lmp, 0) / data.length; - const totalEnergy = data.reduce((sum, d) => sum + d.energy, 0); - const maxCongestion = Math.max(...data.map(d => d.congestion)); - const avgLoss = data.reduce((sum, d) => sum + d.loss, 0) / data.length; - return { avgLmp, totalEnergy, maxCongestion, avgLoss }; - }; - - const fiveMinKPIs = computeKPIs(fiveMinData); - const dayAheadKPIs = computeKPIs(dayAheadData); - - return ( - <div className="page-container"> - <Typography.Title heading={3} style={{ textAlign: 'center', marginBottom: 20 }} className="market-page-title" > - Market Data for {MARKET_FULL_NAMES[selectedMarket]} - </Typography.Title> - - {loading ? ( - <Spin style={{ display: 'block', margin: '80px auto' }} /> - ) : ( - <> - <Row gutter={24} style={{ marginBottom: 30 }}> - {[ - { label: 'Avg Real-Time LMP ($/MWh)', value: fiveMinKPIs.avgLmp }, - { label: 'Total Real-Time Energy (MWh)', value: fiveMinKPIs.totalEnergy }, - { label: 'Max Real-Time Congestion ($)', value: fiveMinKPIs.maxCongestion }, - { label: 'Avg Real-Time Loss ($)', value: fiveMinKPIs.avgLoss } - ].map((item, idx) => ( - <Col xs={12} md={6} key={idx} style={{ marginBottom: 16 }}> - <Card - hoverable - style={{ - textAlign: 'center', - borderRadius: 12, - boxShadow: '0 4px 12px rgba(0,0,0,0.05)' - }} - title={item.label} - > - <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title> - </Card> - </Col> - ))} - </Row> - - <Row gutter={24} style={{ marginBottom: 30 }}> - {[ - { label: 'Avg Day-Ahead LMP ($/MWh)', value: dayAheadKPIs.avgLmp }, - { label: 'Total Day-Ahead Energy (MWh)', value: dayAheadKPIs.totalEnergy }, - { label: 'Max Day-Ahead Congestion ($)', value: dayAheadKPIs.maxCongestion }, - { label: 'Avg Day-Ahead Loss ($)', value: dayAheadKPIs.avgLoss } - ].map((item, idx) => ( - <Col xs={12} md={6} key={idx} style={{ marginBottom: 16 }}> - <Card - hoverable - style={{ - textAlign: 'center', - borderRadius: 12, - boxShadow: '0 4px 12px rgba(0,0,0,0.05)' - }} - title={item.label} - > - <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title> - </Card> - </Col> - ))} - </Row> - - <Row gutter={24}> - <Col xs={24} md={12}> - <Card title="Real-Time Market Data" extra={<TimeAgo timestamp={fiveMinLastUpdated} prefix="Updated" />} style={{ borderRadius: 12, marginBottom: 24 }}> - <div style={{ padding: 10 }}> - <VChart - spec={fiveMinSpec} - ref={fiveMinChartRef} - onDataZoom={(e) => handleZoomChange(e.detail, 'fiveMin')} - /> - </div> - </Card> - </Col> - <Col xs={24} md={12}> - <Card title="Day-Ahead Market Data" extra={<TimeAgo timestamp={dayAheadLastUpdated} prefix="Updated" />} style={{ borderRadius: 12, marginBottom: 24 }}> - <div style={{ padding: 10 }}> - <VChart - spec={dayAheadSpec} - ref={dayAheadChartRef} - onDataZoom={(e) => handleZoomChange(e.detail, 'dayAhead')} - /> - </div> - </Card> - </Col> - </Row> - </> - )} - - <style>{` - @keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } - } - - .arco-card-header-title { - white-space: normal !important; /* allow wrapping */ - word-break: break-word; - } - - /* Optional: smaller text on tiny devices */ - @media (max-width: 768px) { - .arco-card-header-title { - font-size: 14px; - } - } - - .market-page-title { - white-space: normal; - word-break: keep-all; - overflow-wrap: break-word; - text-align: center; - } - - .page-container { - padding: 15px; - background: var(--color-fill-2); - animation: fadeIn 0.5s ease; - overflow-x: hidden; /* Also good practice */ - } - - /* Responsive: remove padding on small screens */ - @media (max-width: 768px) { - .page-container { - padding: 0; - } - } - `}</style> - </div> - ); -} - -export default MarketDataPage; |