u
This commit is contained in:
109
uiserver/src/components/Chart.js
vendored
Normal file
109
uiserver/src/components/Chart.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Paper, Typography, Box, CircularProgress } from '@mui/material';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export default function Chart({ selectedChannels = [], channelConfig = null }) {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const theme = useTheme();
|
||||
|
||||
// Determine effective channels list
|
||||
const effectiveChannels = channelConfig
|
||||
? channelConfig.map(c => c.id)
|
||||
: selectedChannels;
|
||||
|
||||
const fetchData = () => {
|
||||
// Only fetch if selection exists
|
||||
if (effectiveChannels.length === 0) {
|
||||
setData([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionStr = effectiveChannels.join(',');
|
||||
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
fetch(`/api/readings?selection=${encodeURIComponent(selectionStr)}&since=${since}`)
|
||||
.then(res => res.json())
|
||||
.then(rows => {
|
||||
const timeMap = new Map();
|
||||
|
||||
rows.forEach(row => {
|
||||
const id = `${row.device}:${row.channel}`;
|
||||
if (!effectiveChannels.includes(id)) return;
|
||||
|
||||
const time = new Date(row.timestamp).getTime();
|
||||
if (!timeMap.has(time)) {
|
||||
timeMap.set(time, { time: new Date(row.timestamp) });
|
||||
}
|
||||
const entry = timeMap.get(time);
|
||||
|
||||
let val = row.value;
|
||||
if (row.data_type === 'json' && !val) {
|
||||
val = null;
|
||||
}
|
||||
entry[id] = val;
|
||||
});
|
||||
|
||||
const sortedData = Array.from(timeMap.values()).sort((a, b) => a.time - b.time);
|
||||
setData(sortedData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to fetch data", err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedChannels, channelConfig]);
|
||||
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
if (effectiveChannels.length === 0) return <Box sx={{ p: 4 }}><Typography>No channels selected.</Typography></Box>;
|
||||
|
||||
const series = effectiveChannels.map(id => {
|
||||
// Find alias if config exists
|
||||
let label = id;
|
||||
if (channelConfig) {
|
||||
const item = channelConfig.find(c => c.id === id);
|
||||
if (item && item.alias) label = item.alias;
|
||||
}
|
||||
|
||||
return {
|
||||
dataKey: id,
|
||||
label: label,
|
||||
connectNulls: true,
|
||||
showMark: false,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '80vh', p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Last 24 Hours</Typography>
|
||||
<Paper sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<LineChart
|
||||
dataset={data}
|
||||
series={series}
|
||||
xAxis={[{
|
||||
dataKey: 'time',
|
||||
scaleType: 'time',
|
||||
valueFormatter: (date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}]}
|
||||
slotProps={{
|
||||
legend: {
|
||||
direction: 'row',
|
||||
position: { vertical: 'top', horizontal: 'middle' },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user