From 5244aff586df93de2b9973de9c9046e4ad6163bd Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sun, 27 Apr 2025 21:30:56 -0600 Subject: improve styling --- client/src/MarketDataPage.jsx | 385 +++++++++++++++++++----------------------- 1 file changed, 172 insertions(+), 213 deletions(-) (limited to 'client/src/MarketDataPage.jsx') diff --git a/client/src/MarketDataPage.jsx b/client/src/MarketDataPage.jsx index 52ca47c..4da1969 100644 --- a/client/src/MarketDataPage.jsx +++ b/client/src/MarketDataPage.jsx @@ -32,254 +32,213 @@ function TimeAgo({ timestamp, prefix }) { }; 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 [dayAheadData, setDayAheadData] = useState([]); + const [loading, setLoading] = useState(true); + const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null); + const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null); + const [now, setNow] = useState(new Date()); - 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.'); - } - }; + 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 initialize = async () => { - setLoading(true); - await Promise.all([fetchFiveMinData(), fetchDayAheadData()]); - setLoading(false); + 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.'); + } + }; - initVChartArcoTheme({ - defaultMode: 'light', - isWatchingMode: true - }); - }; + const initialize = async () => { + setLoading(true); + await Promise.all([fetchFiveMinData(), fetchDayAheadData()]); + setLoading(false); - initialize(); + initVChartArcoTheme({ + defaultMode: 'light', + isWatchingMode: true + }); + }; - 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); + initialize(); - return () => { - clearInterval(fiveMinInterval); - clearInterval(dayAheadInterval); - clearInterval(timer); - }; - }, [selectedMarket]); + const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); + const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); + const timer = setInterval(() => setNow(new Date()), 1000); - 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 }); - } - } + 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 - }; - + 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 - }; + 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]} - +
+ + 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)} - - - - - - -
- -
-
- - - -
- -
-
- -
- +
+ + +
+ + + {[ + { 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) => ( + + + ${item.value.toFixed(2)} + + + ))} + + + + {[ + { 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) => ( + + + ${item.value.toFixed(2)} + + + ))} + + + + + +
+ +
+
+ + + +
+ +
+
+ +
+ )} + +
); } -- cgit v1.2.3