aboutsummaryrefslogtreecommitdiff
path: root/client/src/MarketDataPage.jsx
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2025-04-27 22:44:20 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2025-04-27 22:44:20 -0600
commit37787895d56888ab44362252f21fb05c05e97250 (patch)
tree5ff7eaababa6fffd5dc950920f521c5f242010af /client/src/MarketDataPage.jsx
parentf32142947b853076889801913d47b8c2c0f4f456 (diff)
reorganize
Diffstat (limited to 'client/src/MarketDataPage.jsx')
-rw-r--r--client/src/MarketDataPage.jsx320
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;