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,344 @@
import React, { useMemo } from 'react';
import ReactECharts from 'echarts-for-react';
import { useTheme } from '@mui/material/styles';
import { Box, Typography } from '@mui/material';
interface TemperatureDataPoint {
currentTimestamp: Date;
futureTimestamp: Date;
currentTemp: number;
predictedTemp: number;
}
interface TemperatureEChartProps {
data: TemperatureDataPoint[];
height?: number;
zoomRange?: [number, number];
onZoomChange?: (range: [number, number]) => void;
}
const TemperatureEChart: React.FC<TemperatureEChartProps> = React.memo(({ data, height = 400, zoomRange, onZoomChange }) => {
const theme = useTheme();
const option = useMemo(() => {
if (!data || data.length === 0) {
return {};
}
// Prepare data for ECharts
const currentTempData = data.map(item => [
item.currentTimestamp.getTime(),
Number(item.currentTemp.toFixed(2))
]);
const predictedTempData = data.map(item => [
item.futureTimestamp.getTime(),
Number(item.predictedTemp.toFixed(2))
]);
// Calculate temperature range for better axis scaling
const allTemps = [...data.map(d => d.currentTemp), ...data.map(d => d.predictedTemp)];
const minTemp = Math.min(...allTemps);
const maxTemp = Math.max(...allTemps);
const tempPadding = (maxTemp - minTemp) * 0.1;
const commonSeriesSettings = {
sampling: 'lttb',
showSymbol: false,
smooth: 0.3,
animation: false,
emphasis: {
disabled: true
}
};
return {
animation: false,
progressive: 500,
progressiveThreshold: 1000,
renderer: 'canvas',
title: {
text: 'Environmental Temperature Monitoring',
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: theme.palette.primary.main,
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]}°C</span>
</div>
`;
});
return html;
}
},
legend: {
data: ['Current Temperature', 'Predicted Temperature'],
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: 'rgba(2, 138, 74, 0.1)',
textStyle: {
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif'
},
handleStyle: {
color: theme.palette.primary.main
}
},
{
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: 'Temperature (°C)',
nameTextStyle: {
color: theme.palette.text.secondary,
fontFamily: 'Montserrat, sans-serif',
fontWeight: 'bold'
},
min: Math.max(0, minTemp - tempPadding),
max: maxTemp + tempPadding,
axisLabel: {
formatter: '{value}°C',
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
}
}
},
// Optimize throttling and rendering
throttle: 70,
silent: true, // Reduce event overhead when not needed
series: [
{
...commonSeriesSettings,
name: 'Current Temperature',
type: 'line',
data: currentTempData,
lineStyle: {
color: theme.palette.primary.main,
width: 2,
join: 'round'
},
itemStyle: {
color: theme.palette.primary.main,
opacity: 0.8
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: `rgba(2, 138, 74, 0.2)`
}, {
offset: 1,
color: `rgba(2, 138, 74, 0.02)`
}]
}
}
},
{
...commonSeriesSettings,
name: 'Predicted Temperature',
type: 'line',
data: predictedTempData,
lineStyle: {
color: theme.palette.warning.main,
width: 2,
type: 'dashed'
},
itemStyle: {
color: theme.palette.warning.main,
opacity: 0.8
}
}
]
};
}, [data, theme]);
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 temperature 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) => {
// Custom comparison to prevent unnecessary rerenders
if (prevProps.height !== nextProps.height) return false;
if (prevProps.zoomRange?.[0] !== nextProps.zoomRange?.[0] ||
prevProps.zoomRange?.[1] !== nextProps.zoomRange?.[1]) return false;
if (prevProps.data.length !== nextProps.data.length) return false;
// Only update if the last data point has changed
const prevLast = prevProps.data[prevProps.data.length - 1];
const nextLast = nextProps.data[nextProps.data.length - 1];
if (prevLast && nextLast) {
return prevLast.currentTimestamp.getTime() === nextLast.currentTimestamp.getTime() &&
prevLast.currentTemp === nextLast.currentTemp &&
prevLast.predictedTemp === nextLast.predictedTemp;
}
return false;
});
TemperatureEChart.displayName = 'TemperatureEChart';
export default TemperatureEChart;