aboutsummaryrefslogtreecommitdiff
path: root/client/src/components/MarketDataPage.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/components/MarketDataPage.jsx')
-rw-r--r--client/src/components/MarketDataPage.jsx257
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>