aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2025-04-27 20:12:17 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2025-04-27 20:12:17 -0600
commitba8d8f5cb2ef6a8b42c0952d2ca53da72f69c20d (patch)
tree7a01bf9d72d3b78e45b183e6713c0d71d3c73afd
parentd2a5e2c68c9f5294dd8c430a808e0c4d8318ac57 (diff)
auto update & add KPIs
-rw-r--r--client/src/MarketDataPage.jsx250
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;