diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-28 00:20:28 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2025-04-28 00:20:28 -0600 |
commit | d418dd77f23ab24e91dfebad94832f9496eb567d (patch) | |
tree | 063698ec71e71a8c0c9c79814c4b1e575bb8a1c3 /client/src/components/MarketDataPage.jsx | |
parent | 6ca0b8d9488bb16a118d00aadba1b6c7955f71b6 (diff) |
Diffstat (limited to 'client/src/components/MarketDataPage.jsx')
-rw-r--r-- | client/src/components/MarketDataPage.jsx | 257 |
1 files changed, 161 insertions, 96 deletions
diff --git a/client/src/components/MarketDataPage.jsx b/client/src/components/MarketDataPage.jsx index 9d98269..eed970f 100644 --- a/client/src/components/MarketDataPage.jsx +++ b/client/src/components/MarketDataPage.jsx @@ -1,10 +1,10 @@ -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'; +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: // @@ -22,7 +22,7 @@ function TimeAgo({ timestamp, prefix }) { }, []); const formatTimeAgo = () => { - if (!timestamp) return ''; + if (!timestamp) return ""; const seconds = Math.floor((now - timestamp) / 1000); if (seconds < 60) return `${seconds} sec ago`; const minutes = Math.floor(seconds / 60); @@ -32,7 +32,10 @@ function TimeAgo({ timestamp, prefix }) { }; return ( - <Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 8 }}> + <Typography.Text + type="secondary" + style={{ fontSize: 12, display: "block", marginTop: 8 }} + > {prefix}: {formatTimeAgo()} </Typography.Text> ); @@ -41,7 +44,7 @@ function TimeAgo({ timestamp, prefix }) { function MarketDataPage() { const { selectedMarket } = useContext(MarketContext); const fiveMinChartRef = useRef(null); - const dayAheadChartRef = 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([]); @@ -52,38 +55,42 @@ function MarketDataPage() { 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 }); - } - }; + 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 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.'); + 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 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.'); + Message.error("Failed to load day-ahead market data."); } }; @@ -93,8 +100,8 @@ function MarketDataPage() { setLoading(false); initVChartArcoTheme({ - defaultMode: 'light', - isWatchingMode: true + defaultMode: "light", + isWatchingMode: true, }); }; @@ -111,79 +118,102 @@ function MarketDataPage() { }; }, [selectedMarket]); - const fiveMinSpec = useMemo(() => ({ - type: 'line', + const fiveMinSpec = useMemo( + () => ({ + type: "line", data: { - values: fiveMinData.map(d => ({ + values: fiveMinData.map((d) => ({ timestamp: new Date(d.timestamp).toLocaleString(), LMP: d.lmp, Energy: d.energy, Congestion: d.congestion, - Loss: d.loss + Loss: d.loss, })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] + transforms: [ + { + type: "fold", + options: { + key: "name", + value: "value", + fields: ["LMP", "Energy", "Congestion", "Loss"], + }, + }, + ], }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', + xField: "timestamp", + yField: "value", + seriesField: "name", smooth: true, - legend: { position: 'top' }, + legend: { position: "top" }, tooltip: { formatter: (datum) => ({ name: datum.name, - value: datum.value.toFixed(2) - }) + value: datum.value.toFixed(2), + }), }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: fiveMinZoom.start, - end: fiveMinZoom.end - }] - }), [fiveMinData, fiveMinZoom]); // <== DEPENDENCIES + dataZoom: [ + { + orient: "bottom", + height: 20, + start: fiveMinZoom.start, + end: fiveMinZoom.end, + }, + ], + }), + [fiveMinData, fiveMinZoom], + ); // <== DEPENDENCIES - const dayAheadSpec = useMemo(() => ({ - type: 'line', + const dayAheadSpec = useMemo( + () => ({ + type: "line", data: { - values: dayAheadData.map(d => ({ + values: dayAheadData.map((d) => ({ timestamp: new Date(d.timestamp).toLocaleString(), LMP: d.lmp, Energy: d.energy, Congestion: d.congestion, - Loss: d.loss + Loss: d.loss, })), - transforms: [{ - type: 'fold', - options: { key: 'name', value: 'value', fields: ['LMP', 'Energy', 'Congestion', 'Loss'] } - }] + transforms: [ + { + type: "fold", + options: { + key: "name", + value: "value", + fields: ["LMP", "Energy", "Congestion", "Loss"], + }, + }, + ], }, - xField: 'timestamp', - yField: 'value', - seriesField: 'name', + xField: "timestamp", + yField: "value", + seriesField: "name", smooth: true, - legend: { position: 'top' }, + legend: { position: "top" }, tooltip: { formatter: (datum) => ({ name: datum.name, - value: datum.value.toFixed(2) - }) + value: datum.value.toFixed(2), + }), }, - dataZoom: [{ - orient: 'bottom', - height: 20, - start: dayAheadZoom.start, - end: dayAheadZoom.end - }] - }), [dayAheadData, dayAheadZoom]); + 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 }; + 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 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 }; }; @@ -193,32 +223,44 @@ function MarketDataPage() { return ( <div className="page-container"> - <Typography.Title heading={3} style={{ textAlign: 'center', marginBottom: 20 }} className="market-page-title" > + <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' }} /> + <Spin style={{ display: "block", margin: "80px auto" }} /> ) : ( <> - <Row gutter={24} style={{ marginBottom: 30 }}> + <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 } + { 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', + textAlign: "center", borderRadius: 12, - boxShadow: '0 4px 12px rgba(0,0,0,0.05)' + boxShadow: "0 4px 12px rgba(0,0,0,0.05)", }} title={item.label} > - <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title> + <Typography.Title heading={5}> + ${item.value.toFixed(2)} + </Typography.Title> </Card> </Col> ))} @@ -226,22 +268,33 @@ function MarketDataPage() { <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 } + { + 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', + textAlign: "center", borderRadius: 12, - boxShadow: '0 4px 12px rgba(0,0,0,0.05)' + boxShadow: "0 4px 12px rgba(0,0,0,0.05)", }} title={item.label} > - <Typography.Title heading={5}>${item.value.toFixed(2)}</Typography.Title> + <Typography.Title heading={5}> + ${item.value.toFixed(2)} + </Typography.Title> </Card> </Col> ))} @@ -249,24 +302,36 @@ function MarketDataPage() { <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 }}> + <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')} - /> + 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 }}> + <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')} - /> + spec={dayAheadSpec} + ref={dayAheadChartRef} + onDataZoom={(e) => handleZoomChange(e.detail, "dayAhead")} + /> </div> </Card> </Col> |