UpGraded tempreture charts version 2.0, using Echarts library, optomized rendering and added proper data selection options

This commit is contained in:
2025-09-21 03:26:48 +03:00
parent e052afde3d
commit 531dfd8715
8 changed files with 1573 additions and 746 deletions

View File

@@ -0,0 +1,367 @@
import React, { useMemo } from 'react';
import ReactECharts from 'echarts-for-react';
import { useTheme } from '@mui/material/styles';
import { Box, Typography } from '@mui/material';
interface DataPoint {
currentTimestamp: Date;
futureTimestamp: Date;
currentValue: number;
predictedValue: number;
}
interface MonitoringChartProps {
data: DataPoint[];
height?: number;
zoomRange?: [number, number];
onZoomChange?: (range: [number, number]) => void;
chartTitle: string;
unit: string;
colors?: {
current: string;
predicted: string;
};
}
const MonitoringChart: React.FC<MonitoringChartProps> = React.memo(({
data,
height = 400,
zoomRange,
onZoomChange,
chartTitle,
unit,
colors
}) => {
const theme = useTheme();
const commonSeriesSettings = {
sampling: 'lttb',
showSymbol: false,
smooth: 0.3,
animation: false,
emphasis: {
disabled: true
}
};
const option = useMemo(() => {
if (!data || data.length === 0) {
return {};
}
const currentData = data.map(item => [
item.currentTimestamp.getTime(),
Number(item.currentValue.toFixed(2))
]);
const predictedData = data.map(item => [
item.futureTimestamp.getTime(),
Number(item.predictedValue.toFixed(2))
]);
const allValues = [...data.map(d => d.currentValue), ...data.map(d => d.predictedValue)];
const minValue = Math.min(...allValues);
const maxValue = Math.max(...allValues);
const valuePadding = (maxValue - minValue) * 0.1;
const currentColor = colors?.current || theme.palette.primary.main;
const predictedColor = colors?.predicted || theme.palette.warning.main;
return {
animation: false,
progressive: 500,
progressiveThreshold: 1000,
renderer: 'canvas',
title: {
text: chartTitle,
textStyle: {
fontSize: 18,
fontWeight: 'bold',
color: theme.palette.text.primary,
fontFamily: 'Montserrat, sans-serif'
},
left: 'center',
top: 10
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: currentColor,
borderWidth: 1,
textStyle: {
color: theme.palette.text.primary,
fontFamily: 'Montserrat, sans-serif'
},
formatter: function (params: any) {
const time = new Date(params[0].value[0]).toLocaleString();
let html = `<div style="margin-bottom: 8px; font-weight: bold;">${time}</div>`;
params.forEach((param: any) => {
html += `
<div style="display: flex; align-items: center; margin-bottom: 4px;">
<span style="display: inline-block; margin-right: 8px; border-radius: 50%; width: 10px; height: 10px; background-color: ${param.color};"></span>
<span style="font-weight: 500;">${param.seriesName}:</span>
<span style="margin-left: 8px; color: ${param.color}; font-weight: bold;">${param.value[1]}${unit}</span>
</div>
`;
});
return html;
}
},
legend: {
data: ['Current Value', 'Predicted Value'],
top: 40,
textStyle: {
fontFamily: 'Montserrat, sans-serif',
fontSize: 12,
color: theme.palette.text.secondary
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '25%',
containLabel: true
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none',
title: {
zoom: 'Zoom',
back: 'Reset Zoom'
}
},
restore: {
title: 'Reset'
},
saveAsImage: {
title: 'Save'
}
},
right: 15,
top: 5
},
dataZoom: [
{
type: 'slider',
show: true,
xAxisIndex: [0],
start: zoomRange?.[0] ?? 0,
end: zoomRange?.[1] ?? 100,
height: 20,
bottom: 0,
borderColor: theme.palette.divider,
fillerColor: `${currentColor}1A`, // 10% opacity
textStyle: {
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif'
},
handleStyle: {
color: currentColor
}
},
{
type: 'inside',
xAxisIndex: [0],
start: 0,
end: 100,
zoomOnMouseWheel: 'shift'
}
],
xAxis: {
type: 'time',
boundaryGap: false,
axisLabel: {
formatter: function (value: number) {
const date = new Date(value);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
},
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif'
},
axisLine: {
lineStyle: {
color: theme.palette.divider
}
},
splitLine: {
show: true,
lineStyle: {
color: theme.palette.divider,
type: 'dashed',
opacity: 0.5
}
}
},
yAxis: {
type: 'value',
name: chartTitle,
nameTextStyle: {
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif',
fontWeight: 'bold'
},
min: Math.max(0, minValue - valuePadding),
max: maxValue + valuePadding,
axisLabel: {
formatter: `{value}${unit}`,
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif'
},
axisLine: {
lineStyle: {
color: theme.palette.divider
}
},
splitLine: {
lineStyle: {
color: theme.palette.divider,
type: 'dashed',
opacity: 0.3
}
}
},
series: [
{
...commonSeriesSettings,
name: 'Current Value',
type: 'line',
data: currentData,
lineStyle: {
color: currentColor,
width: 2,
join: 'round'
},
itemStyle: {
color: currentColor,
opacity: 0.8
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: `${currentColor}33` // 20% opacity
}, {
offset: 1,
color: `${currentColor}05` // 2% opacity
}]
}
}
},
{
...commonSeriesSettings,
name: 'Predicted Value',
type: 'line',
data: predictedData,
lineStyle: {
color: predictedColor,
width: 2,
type: 'dashed'
},
itemStyle: {
color: predictedColor,
opacity: 0.8
}
}
]
};
}, [data, theme, chartTitle, unit, colors, zoomRange]);
if (!data || data.length === 0) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height={height}
bgcolor={theme.palette.background.paper}
borderRadius={2}
boxShadow={2}
>
<Typography
variant="h6"
color="text.secondary"
fontFamily="Montserrat, sans-serif"
>
No data available
</Typography>
</Box>
);
}
return (
<Box
sx={{
bgcolor: theme.palette.background.paper,
borderRadius: 2,
boxShadow: 3,
overflow: 'hidden'
}}
>
<ReactECharts
option={option}
style={{ height: `${height}px`, width: '100%' }}
opts={{
renderer: 'canvas',
width: 'auto',
height: 'auto',
}}
notMerge={true}
lazyUpdate={true}
theme={theme.palette.mode}
loadingOption={{
color: theme.palette.primary.main,
maskColor: 'rgba(255, 255, 255, 0.8)',
text: 'Loading...'
}}
onEvents={{
datazoom: (params: any) => {
if (onZoomChange && params.batch?.[0]) {
onZoomChange([params.batch[0].start, params.batch[0].end]);
}
}
}}
/>
</Box>
);
}, (prevProps, nextProps) => {
// Always re-render if data length changes
if (prevProps.data.length !== nextProps.data.length) return false;
// Always re-render if essential props change
if (prevProps.height !== nextProps.height) return false;
if (prevProps.chartTitle !== nextProps.chartTitle) return false;
if (prevProps.unit !== nextProps.unit) return false;
if (prevProps.zoomRange?.[0] !== nextProps.zoomRange?.[0] ||
prevProps.zoomRange?.[1] !== nextProps.zoomRange?.[1]) return false;
// Deep compare the last few data points
const compareCount = 5; // Compare last 5 points for smooth transitions
for (let i = 1; i <= compareCount; i++) {
const prevItem = prevProps.data[prevProps.data.length - i];
const nextItem = nextProps.data[nextProps.data.length - i];
if (!prevItem || !nextItem) return false;
if (prevItem.currentTimestamp.getTime() !== nextItem.currentTimestamp.getTime() ||
prevItem.currentValue !== nextItem.currentValue ||
prevItem.predictedValue !== nextItem.predictedValue) {
return false;
}
}
return true;
});
MonitoringChart.displayName = 'MonitoringChart';
export default MonitoringChart;