u
This commit is contained in:
167
uiserver/src/components/Chart.js
vendored
167
uiserver/src/components/Chart.js
vendored
@@ -1,9 +1,112 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Box, Paper, Typography, CircularProgress, IconButton } from '@mui/material';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import { useDrawingArea, useYScale, useXScale } from '@mui/x-charts/hooks';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
|
||||
// Custom component to render a horizontal band between two y-values
|
||||
function ReferenceArea({ yMin, yMax, color = 'rgba(76, 175, 80, 0.15)', axisId = 'left' }) {
|
||||
const { left, width } = useDrawingArea();
|
||||
const yScale = useYScale(axisId);
|
||||
|
||||
if (!yScale) return null;
|
||||
|
||||
const y1 = yScale(yMax);
|
||||
const y2 = yScale(yMin);
|
||||
|
||||
if (y1 === undefined || y2 === undefined) return null;
|
||||
|
||||
return (
|
||||
<rect
|
||||
x={left}
|
||||
y={Math.min(y1, y2)}
|
||||
width={width}
|
||||
height={Math.abs(y2 - y1)}
|
||||
fill={color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render vertical time bands every 6 hours aligned to midnight
|
||||
function TimeReferenceAreas({ axisStart, axisEnd, colors }) {
|
||||
const { top, height } = useDrawingArea();
|
||||
const xScale = useXScale();
|
||||
|
||||
if (!xScale) return null;
|
||||
|
||||
// Calculate 6-hour bands aligned to midnight
|
||||
const SIX_HOURS = 6 * 60 * 60 * 1000;
|
||||
const bands = [];
|
||||
|
||||
// Find the first midnight before axisStart
|
||||
const startDate = new Date(axisStart);
|
||||
const midnight = new Date(startDate);
|
||||
midnight.setHours(0, 0, 0, 0);
|
||||
|
||||
// Start from that midnight
|
||||
let bandStart = midnight.getTime();
|
||||
|
||||
while (bandStart < axisEnd) {
|
||||
const bandEnd = bandStart + SIX_HOURS;
|
||||
|
||||
// Only render if band overlaps with visible range
|
||||
if (bandEnd > axisStart && bandStart < axisEnd) {
|
||||
const visibleStart = Math.max(bandStart, axisStart);
|
||||
const visibleEnd = Math.min(bandEnd, axisEnd);
|
||||
|
||||
const x1 = xScale(new Date(visibleStart));
|
||||
const x2 = xScale(new Date(visibleEnd));
|
||||
|
||||
if (x1 !== undefined && x2 !== undefined) {
|
||||
// Determine which 6-hour block (0-3) based on hour of day
|
||||
const hour = new Date(bandStart).getHours();
|
||||
const blockIndex = Math.floor(hour / 6); // 0, 1, 2, or 3
|
||||
const color = colors[blockIndex % colors.length];
|
||||
|
||||
bands.push(
|
||||
<rect
|
||||
key={bandStart}
|
||||
x={Math.min(x1, x2)}
|
||||
y={top}
|
||||
width={Math.abs(x2 - x1)}
|
||||
height={height}
|
||||
fill={color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
bandStart = bandEnd;
|
||||
}
|
||||
|
||||
return <>{bands}</>;
|
||||
}
|
||||
|
||||
// Helper function to calculate Simple Moving Average
|
||||
function calculateSMA(data, channelKey, period) {
|
||||
if (period <= 1 || data.length === 0) return data;
|
||||
|
||||
return data.map((row, i) => {
|
||||
const newRow = { ...row };
|
||||
const values = [];
|
||||
|
||||
// Look back up to 'period' samples
|
||||
for (let j = Math.max(0, i - period + 1); j <= i; j++) {
|
||||
const val = data[j][channelKey];
|
||||
if (val !== null && val !== undefined && !isNaN(val)) {
|
||||
values.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate average if we have values
|
||||
if (values.length > 0) {
|
||||
newRow[channelKey] = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
}
|
||||
|
||||
return newRow;
|
||||
});
|
||||
}
|
||||
|
||||
export default class Chart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -115,9 +218,14 @@ export default class Chart extends Component {
|
||||
|
||||
// Calculate effective end
|
||||
let end = explicitEnd;
|
||||
// If 'until' is null, extend to next point or now
|
||||
// If 'until' is null, extend to next point or now (but never beyond current time)
|
||||
const nowTime = Date.now();
|
||||
if (!end) {
|
||||
end = nextStart || endTimeVal;
|
||||
end = nextStart || Math.min(endTimeVal, nowTime);
|
||||
}
|
||||
// Never extend data beyond the current time
|
||||
if (end > nowTime) {
|
||||
end = nowTime;
|
||||
}
|
||||
|
||||
// Strict Cutoff: Current interval cannot extend past the start of the next interval
|
||||
@@ -169,7 +277,19 @@ export default class Chart extends Component {
|
||||
return row;
|
||||
});
|
||||
|
||||
this.setState({ data: denseData, loading: false });
|
||||
// 4. Apply SMA for channels that have it configured
|
||||
const { channelConfig } = this.props;
|
||||
let processedData = denseData;
|
||||
|
||||
if (channelConfig) {
|
||||
channelConfig.forEach(cfg => {
|
||||
if (cfg.sma && cfg.sma > 1) {
|
||||
processedData = calculateSMA(processedData, cfg.id, cfg.sma);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ data: processedData, loading: false });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
@@ -305,6 +425,32 @@ export default class Chart extends Component {
|
||||
const axisEnd = windowEnd ? windowEnd.getTime() : Date.now();
|
||||
const axisStart = axisEnd - rangeMs;
|
||||
|
||||
// Determine if all visible series are Temperature channels
|
||||
const isTemperatureOnly = visibleChannels.length > 0 && visibleChannels.every(id => {
|
||||
const lcId = id.toLowerCase();
|
||||
return lcId.includes('temp') || lcId.includes('temperature');
|
||||
});
|
||||
|
||||
// Determine if all visible series are Humidity channels
|
||||
const isHumidityOnly = visibleChannels.length > 0 && visibleChannels.every(id => {
|
||||
const lcId = id.toLowerCase();
|
||||
return lcId.includes('humid') || lcId.includes('humidity') || lcId.includes('rh');
|
||||
});
|
||||
|
||||
// Determine if all visible series are Light channels
|
||||
const isLightOnly = visibleChannels.length > 0 && visibleChannels.every(id => {
|
||||
const lcId = id.toLowerCase();
|
||||
return lcId.includes('light');
|
||||
});
|
||||
|
||||
// Colors for 6-hour time bands (midnight, 6am, noon, 6pm)
|
||||
const lightBandColors = [
|
||||
'rgba(0, 0, 0, 0.1)', // 00:00-06:00 - black (night)
|
||||
'rgba(135, 206, 250, 0.1)', // 06:00-12:00 - light blue (morning)
|
||||
'rgba(255, 255, 180, 0.1)', // 12:00-18:00 - light yellow (afternoon)
|
||||
'rgba(255, 200, 150, 0.1)', // 18:00-24:00 - light orange (evening)
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', p: 2, boxSizing: 'border-box' }}>
|
||||
<Paper sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
||||
@@ -367,7 +513,20 @@ export default class Chart extends Component {
|
||||
fillOpacity: series.find(s => s.area)?.fillOpacity ?? 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{/* Green reference band for temperature charts (20-25°C) */}
|
||||
{isTemperatureOnly && (
|
||||
<ReferenceArea yMin={20} yMax={25} color="rgba(76, 175, 80, 0.2)" />
|
||||
)}
|
||||
{/* Green reference band for humidity charts (50-70%) */}
|
||||
{isHumidityOnly && (
|
||||
<ReferenceArea yMin={50} yMax={70} color="rgba(76, 175, 80, 0.2)" />
|
||||
)}
|
||||
{/* Time-based vertical bands for light charts (6-hour intervals) */}
|
||||
{isLightOnly && (
|
||||
<TimeReferenceAreas axisStart={axisStart} axisEnd={axisEnd} colors={lightBandColors} />
|
||||
)}
|
||||
</LineChart>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user