diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-27 20:12:17 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-27 20:12:17 -0600 |
commit | ba8d8f5cb2ef6a8b42c0952d2ca53da72f69c20d (patch) | |
tree | 7a01bf9d72d3b78e45b183e6713c0d71d3c73afd | |
parent | d2a5e2c68c9f5294dd8c430a808e0c4d8318ac57 (diff) |
auto update & add KPIs
-rw-r--r-- | client/src/MarketDataPage.jsx | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/client/src/MarketDataPage.jsx b/client/src/MarketDataPage.jsx new file mode 100644 index 0000000..3ffa099 --- /dev/null +++ b/client/src/MarketDataPage.jsx @@ -0,0 +1,250 @@ +import React, { useEffect, useState } 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'; + +const { Row, Col } = Grid; + +function MarketDataPage() { + const [fiveMinData, setFiveMinData] = useState([]); + const [dayAheadData, setDayAheadData] = useState([]); + const [loading, setLoading] = useState(true); + const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null); + const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null); + const [now, setNow] = useState(new Date()); + + useEffect(() => { + const fetchFiveMinData = async () => { + try { + const res = await fetch(`${API_BASE_URL}/market/real-time`); + 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`); + 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); // 5 minutes + const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); // 1 hour + + const timer = setInterval(() => { + setNow(new Date()); + }, 1000); + + return () => { + clearInterval(fiveMinInterval); + clearInterval(dayAheadInterval); + clearInterval(timer); + }; + }, []); + + const getChartSpec = (data, title) => ({ + type: 'line', + data: { + values: data.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: title === 'fiveMin' ? 0.9 : 0.05, + end: 1 + } + ] + }); + + 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); + + const formatTimeAgo = (timestamp) => { + 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 ( + <div style={{ padding: 20, backgroundColor: 'var(--color-fill-2)' }}> + <Typography.Title heading={4}>Market Data Visualization</Typography.Title> + <Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}> + Real-Time Data Last Updated: {formatTimeAgo(fiveMinLastUpdated)} + </Typography.Text> + <Typography.Text type="secondary" style={{ marginBottom: 20, display: 'block' }}> + Day-Ahead Data Last Updated: {formatTimeAgo(dayAheadLastUpdated)} + </Typography.Text> + + {loading ? ( + <Spin /> + ) : ( + <> + <Row gutter={24} style={{ marginTop: 20, marginBottom: 20 }}> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Avg Real-Time LMP ($/MWh)"> + <Typography.Title heading={5}>${fiveMinKPIs.avgLmp.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Total Real-Time Energy (MWh)"> + <Typography.Title heading={5}>{fiveMinKPIs.totalEnergy.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Max Real-Time Congestion ($)"> + <Typography.Title heading={5}>${fiveMinKPIs.maxCongestion.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Avg Real-Time Loss ($)"> + <Typography.Title heading={5}>${fiveMinKPIs.avgLoss.toFixed(2)}</Typography.Title> + </Card> + </Col> + </Row> + <Row gutter={24} style={{ marginTop: 20, marginBottom: 20 }}> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Avg Day-Ahead LMP ($/MWh)"> + <Typography.Title heading={5}>${dayAheadKPIs.avgLmp.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Total Day-Ahead Energy (MWh)"> + <Typography.Title heading={5}>{dayAheadKPIs.totalEnergy.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Max Day-Ahead Congestion ($)"> + <Typography.Title heading={5}>${dayAheadKPIs.maxCongestion.toFixed(2)}</Typography.Title> + </Card> + </Col> + <Col xs={12} md={6} style={{ marginTop: 10}}> + <Card style={{ textAlign: 'center' }} title="Avg Day-AheadLoss ($)"> + <Typography.Title heading={5}>${dayAheadKPIs.avgLoss.toFixed(2)}</Typography.Title> + </Card> + </Col> + </Row> + <Row gutter={24} style={{ marginTop: 20 }}> + <Col + xs={24} + sm={24} + md={24} + lg={12} + > + <Card + style={{ marginBottom: 20 }} + bodyStyle={{ padding: 0 }} + title='Real-Time Market Data' + > + <div style={{ padding: 20 }}> + <VChart spec={getChartSpec(fiveMinData, 'fiveMin')} /> + </div> + </Card> + </Col> + <Col + xs={24} + sm={24} + md={24} + lg={12} + > + <Card + style={{ marginBottom: 20 }} + bodyStyle={{ padding: 0 }} + title='Day-Ahead Market Data' + > + <div style={{ padding: 20 }}> + <VChart spec={getChartSpec(dayAheadData, 'dayAhead')} /> + </div> + </Card> + </Col> + </Row> + </> + )} + </div> + ); +} + +export default MarketDataPage; |