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 = 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 = `
${time}
`; params.forEach((param: any) => { html += `
${param.seriesName}: ${param.value[1]}°C
`; }); 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 ( No temperature data available ); } return ( { if (onZoomChange && params.batch?.[0]) { onZoomChange([params.batch[0].start, params.batch[0].end]); } } }} /> ); }, (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;