import React, { useEffect, useState, useContext } 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 (
{prefix}: {formatTimeAgo()}
);
}
function MarketDataPage() {
const { selectedMarket } = useContext(MarketContext);
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 [now, setNow] = useState(new Date());
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); // 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);
};
}, [selectedMarket]);
const getChartSpec = (data, title, zoom) => ({
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: zoom?.start ?? (title === 'fiveMin' ? 0.9 : 0.05),
end: zoom?.end ?? 1,
onChange: (e) => {
if (title === 'fiveMin') {
setFiveMinZoom({ start: e.start, end: e.end });
} else {
setDayAheadZoom({ start: e.start, end: e.end });
}
}
}
]
});
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 (
Market Data for {MARKET_FULL_NAMES[selectedMarket]}
{loading ? (
) : (
<>
${fiveMinKPIs.avgLmp.toFixed(2)}
{fiveMinKPIs.totalEnergy.toFixed(2)}
${fiveMinKPIs.maxCongestion.toFixed(2)}
${fiveMinKPIs.avgLoss.toFixed(2)}
${dayAheadKPIs.avgLmp.toFixed(2)}
{dayAheadKPIs.totalEnergy.toFixed(2)}
${dayAheadKPIs.maxCongestion.toFixed(2)}
${dayAheadKPIs.avgLoss.toFixed(2)}
>
)}
);
}
export default MarketDataPage;