From 25b47c9f86d11a3881f677388650e0bb9bc94f8c Mon Sep 17 00:00:00 2001 From: k3n9achi Date: Sun, 21 Sep 2025 04:31:28 +0300 Subject: [PATCH] updated mainteanace charts --- src/components/Charts/MaintenanceChart.tsx | 440 ++++++++++++++ .../Charts/MaintenanceChart.tsx.bak | 441 +++++++++++++++ .../Charts/MaintenanceChart.tsx.new | 446 +++++++++++++++ src/pages/Maintenance.tsx | 535 +++++------------- 4 files changed, 1484 insertions(+), 378 deletions(-) create mode 100644 src/components/Charts/MaintenanceChart.tsx create mode 100644 src/components/Charts/MaintenanceChart.tsx.bak create mode 100644 src/components/Charts/MaintenanceChart.tsx.new diff --git a/src/components/Charts/MaintenanceChart.tsx b/src/components/Charts/MaintenanceChart.tsx new file mode 100644 index 0000000..23a564f --- /dev/null +++ b/src/components/Charts/MaintenanceChart.tsx @@ -0,0 +1,440 @@ +import React, { useMemo } from 'react'; +import ReactECharts from 'echarts-for-react'; +import { useTheme } from '@mui/material/styles'; +import { Box, Typography } from '@mui/material'; + +interface MaintenanceDataPoint { + currentTimestamp: Date; + futureTimestamp: Date; + currentPower: number; + predictedPower: number; + positive3p: number; + negative3p: number; + positive7p: number; + negative7p: number; +} + +interface MaintenanceChartProps { + data: MaintenanceDataPoint[]; + height?: number; + zoomRange?: [number, number]; + onZoomChange?: (range: [number, number]) => void; +} + +const MaintenanceChart: React.FC = React.memo(({ + data, + height = 400, + zoomRange, + onZoomChange +}) => { + const theme = useTheme(); + + const commonSeriesSettings = { + sampling: 'lttb', + animation: false, + emphasis: { + focus: 'self', + scale: 1, + itemStyle: { + borderWidth: 3, + shadowBlur: 10, + shadowColor: 'rgba(0,0,0,0.2)' + }, + lineStyle: { + width: 4 + } + }, + blur: { + lineStyle: { + opacity: 0.8 + }, + itemStyle: { + opacity: 0.8 + } + } + }; + + const option = useMemo(() => { + if (!data || data.length === 0) { + return {}; + } + + const currentData = data.map(item => [ + item.currentTimestamp.getTime(), + Number(item.currentPower.toFixed(2)) + ]); + + const predictedData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.predictedPower.toFixed(2)) + ]); + + const positive3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive3p.toFixed(2)) + ]); + + const negative3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative3p.toFixed(2)) + ]); + + const positive7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive7p.toFixed(2)) + ]); + + const negative7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative7p.toFixed(2)) + ]); + + const allValues = [ + ...data.map(d => d.currentPower), + ...data.map(d => d.predictedPower), + ...data.map(d => d.positive3p), + ...data.map(d => d.negative3p), + ...data.map(d => d.positive7p), + ...data.map(d => d.negative7p) + ]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const valuePadding = (maxValue - minValue) * 0.1; + + return { + animation: false, + progressive: 500, + progressiveThreshold: 1000, + renderer: 'canvas', + 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]}W +
+ `; + }); + return html; + } + }, + legend: { + data: ['Current Power', 'Predicted Power', '±3% Threshold', '±7% Threshold'], + bottom: '60px', + left: 'center', + padding: [8, 16], + itemGap: 32, + textStyle: { + fontFamily: 'Montserrat, sans-serif', + fontSize: 13, + fontWeight: 500, + color: theme.palette.text.secondary + }, + icon: 'circle', + itemHeight: 10, + itemWidth: 10, + selectedMode: false, + backgroundColor: 'transparent' + }, + grid: { + left: '3%', + right: '3%', + bottom: '100px', + top: '20px', + containLabel: true + }, + toolbox: { + right: '20px', + top: '10px', + feature: { + dataZoom: { + yAxisIndex: 'none', + title: { + zoom: 'Zoom', + back: 'Reset Zoom' + } + }, + restore: { + title: 'Reset' + }, + saveAsImage: { + title: 'Save' + } + } + }, + dataZoom: [ + { + type: 'slider', + show: true, + xAxisIndex: [0], + start: zoomRange?.[0] ?? 0, + end: zoomRange?.[1] ?? 100, + height: 30, + bottom: 20, + 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', + fontSize: 12 + }, + axisLine: { + lineStyle: { + color: theme.palette.divider + } + }, + splitLine: { + show: true, + lineStyle: { + color: theme.palette.divider, + type: 'dashed', + opacity: 0.5 + } + } + }, + yAxis: { + type: 'value', + name: 'Power (W)', + nameTextStyle: { + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif', + fontSize: 13, + fontWeight: 'bold', + padding: [0, 0, 8, 0] + }, + min: Math.max(0, minValue - valuePadding), + max: maxValue + valuePadding, + axisLabel: { + formatter: '{value}W', + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif', + fontSize: 12 + }, + axisLine: { + lineStyle: { + color: theme.palette.divider + } + }, + splitLine: { + lineStyle: { + color: theme.palette.divider, + type: 'dashed', + opacity: 0.3 + } + } + }, + series: [ + { + ...commonSeriesSettings, + name: 'Current Power', + type: 'line', + data: currentData, + smooth: true, + lineStyle: { + color: theme.palette.primary.main, + width: 3 + }, + itemStyle: { + color: theme.palette.primary.main, + borderWidth: 2, + borderColor: '#fff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: `rgba(2, 138, 74, 0.3)` + }, { + offset: 1, + color: `rgba(2, 138, 74, 0.05)` + }] + } + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: 'Predicted Power', + type: 'line', + data: predictedData, + smooth: true, + lineStyle: { + color: theme.palette.warning.main, + width: 3, + type: 'dashed' + }, + itemStyle: { + color: theme.palette.warning.main, + borderWidth: 2, + borderColor: '#fff' + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: '±3% Threshold', + type: 'line', + data: positive3pData.concat(negative3pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.warning.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.warning.light, + opacity: 0.1 + }, + symbol: 'none' + }, + { + ...commonSeriesSettings, + name: '±7% Threshold', + type: 'line', + data: positive7pData.concat(negative7pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.error.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.error.light, + opacity: 0.1 + }, + symbol: 'none' + } + ] + }; + }, [data, theme, zoomRange]); + + if (!data || data.length === 0) { + return ( + + + No maintenance data available + + + ); + } + + return ( + + { + if (onZoomChange && params.batch?.[0]) { + onZoomChange([params.batch[0].start, params.batch[0].end]); + } + } + }} + /> + + ); +}, (prevProps, nextProps) => { + 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; + + 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.currentPower !== nextItem.currentPower || + prevItem.predictedPower !== nextItem.predictedPower) { + return false; + } + } + + return true; +}); + +MaintenanceChart.displayName = 'MaintenanceChart'; + +export default MaintenanceChart; diff --git a/src/components/Charts/MaintenanceChart.tsx.bak b/src/components/Charts/MaintenanceChart.tsx.bak new file mode 100644 index 0000000..f6e4541 --- /dev/null +++ b/src/components/Charts/MaintenanceChart.tsx.bak @@ -0,0 +1,441 @@ +import React, { useMemo } from 'react'; +import ReactECharts from 'echarts-for-react'; +import { useTheme } from '@mui/material/styles'; +import { Box, Typography } from '@mui/material'; + fontWeight: 500, + color: theme.palette.text.secondary + }, + icon: 'circle', + itemHeight: 10, + itemWidth: 10, + selectedMode: false + },t { Box, Typography } from '@mui/material'; + +interface MaintenanceDataPoint { + currentTimestamp: Date; + futureTimestamp: Date; + currentPower: number; + predictedPower: number; + positive3p: number; + negative3p: number; + positive7p: number; + negative7p: number; +} + +interface MaintenanceChartProps { + data: MaintenanceDataPoint[]; + height?: number; + zoomRange?: [number, number]; + onZoomChange?: (range: [number, number]) => void; +} + +const MaintenanceChart: React.FC = React.memo(({ + data, + height = 400, + zoomRange, + onZoomChange +}) => { + const theme = useTheme(); + + const commonSeriesSettings = { + sampling: 'lttb', + animation: false, + emphasis: { + focus: 'series', + itemStyle: { + borderWidth: 3, + shadowBlur: 10 + } + } + }; + + const option = useMemo(() => { + if (!data || data.length === 0) { + return {}; + } + + const currentData = data.map(item => [ + item.currentTimestamp.getTime(), + Number(item.currentPower.toFixed(2)) + ]); + + const predictedData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.predictedPower.toFixed(2)) + ]); + + const positive3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive3p.toFixed(2)) + ]); + + const negative3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative3p.toFixed(2)) + ]); + + const positive7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive7p.toFixed(2)) + ]); + + const negative7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative7p.toFixed(2)) + ]); + + const allValues = [ + ...data.map(d => d.currentPower), + ...data.map(d => d.predictedPower), + ...data.map(d => d.positive3p), + ...data.map(d => d.negative3p), + ...data.map(d => d.positive7p), + ...data.map(d => d.negative7p) + ]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const valuePadding = (maxValue - minValue) * 0.1; + + return { + animation: false, + progressive: 500, + progressiveThreshold: 1000, + renderer: 'canvas', + title: { + text: 'Preventive Maintenance Monitoring', + subtext: 'Power consumption with predictive thresholds', + textStyle: { + fontSize: 20, + fontWeight: 'bold', + color: theme.palette.text.primary, + fontFamily: 'Montserrat, sans-serif' + }, + subtextStyle: { + fontSize: 14, + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif' + }, + left: '5%', + top: 0, + padding: [0, 0, 12, 0] + }, + 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]}W +
+ `; + }); + return html; + } + }, + legend: { + data: ['Current Power', 'Predicted Power', '+3% Threshold', '-3% Threshold', '+7% Threshold', '-7% Threshold'], + top: 40, + textStyle: { + fontFamily: 'Montserrat, sans-serif', + fontSize: 12, + color: theme.palette.text.secondary + } + }, + grid: { + left: '5%', + right: '5%', + bottom: '12%', + top: '15%', + 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: 'Power (W)', + nameTextStyle: { + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif', + fontWeight: 'bold' + }, + min: Math.max(0, minValue - valuePadding), + max: maxValue + valuePadding, + axisLabel: { + formatter: '{value}W', + 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 Power', + type: 'line', + data: currentData, + smooth: true, + lineStyle: { + color: theme.palette.primary.main, + width: 3 + }, + itemStyle: { + color: theme.palette.primary.main, + borderWidth: 2, + borderColor: '#fff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: `rgba(2, 138, 74, 0.3)` + }, { + offset: 1, + color: `rgba(2, 138, 74, 0.05)` + }] + } + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: 'Predicted Power', + type: 'line', + data: predictedData, + smooth: true, + lineStyle: { + color: theme.palette.warning.main, + width: 3, + type: 'dashed' + }, + itemStyle: { + color: theme.palette.warning.main, + borderWidth: 2, + borderColor: '#fff' + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: '±3% Threshold', + type: 'line', + data: positive3pData.concat(negative3pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.warning.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.warning.light, + opacity: 0.1 + }, + symbol: 'none' + }, + { + ...commonSeriesSettings, + name: '±7% Threshold', + type: 'line', + data: positive7pData.concat(negative7pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.error.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.error.light, + opacity: 0.1 + }, + symbol: 'none' + } + ] + }; + }, [data, theme, zoomRange]); + + if (!data || data.length === 0) { + return ( + + + No maintenance data available + + + ); + } + + return ( + + { + if (onZoomChange && params.batch?.[0]) { + onZoomChange([params.batch[0].start, params.batch[0].end]); + } + } + }} + /> + + ); +}, (prevProps, nextProps) => { + 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; + + 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.currentPower !== nextItem.currentPower || + prevItem.predictedPower !== nextItem.predictedPower) { + return false; + } + } + + return true; +}); + +MaintenanceChart.displayName = 'MaintenanceChart'; + +export default MaintenanceChart; \ No newline at end of file diff --git a/src/components/Charts/MaintenanceChart.tsx.new b/src/components/Charts/MaintenanceChart.tsx.new new file mode 100644 index 0000000..04571c7 --- /dev/null +++ b/src/components/Charts/MaintenanceChart.tsx.new @@ -0,0 +1,446 @@ +import React, { useMemo } from 'react'; +import ReactECharts from 'echarts-for-react'; +import { useTheme } from '@mui/material/styles'; +import { Box, Typography } from '@mui/material'; + +interface MaintenanceDataPoint { + currentTimestamp: Date; + futureTimestamp: Date; + currentPower: number; + predictedPower: number; + positive3p: number; + negative3p: number; + positive7p: number; + negative7p: number; +} + +interface MaintenanceChartProps { + data: MaintenanceDataPoint[]; + height?: number; + zoomRange?: [number, number]; + onZoomChange?: (range: [number, number]) => void; +} + +const MaintenanceChart: React.FC = React.memo(({ + data, + height = 400, + zoomRange, + onZoomChange +}) => { + const theme = useTheme(); + + const commonSeriesSettings = { + sampling: 'lttb', + animation: false, + emphasis: { + focus: 'series', + itemStyle: { + borderWidth: 3, + shadowBlur: 10 + } + } + }; + + const option = useMemo(() => { + if (!data || data.length === 0) { + return {}; + } + + const currentData = data.map(item => [ + item.currentTimestamp.getTime(), + Number(item.currentPower.toFixed(2)) + ]); + + const predictedData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.predictedPower.toFixed(2)) + ]); + + const positive3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive3p.toFixed(2)) + ]); + + const negative3pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative3p.toFixed(2)) + ]); + + const positive7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.positive7p.toFixed(2)) + ]); + + const negative7pData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.negative7p.toFixed(2)) + ]); + + const allValues = [ + ...data.map(d => d.currentPower), + ...data.map(d => d.predictedPower), + ...data.map(d => d.positive3p), + ...data.map(d => d.negative3p), + ...data.map(d => d.positive7p), + ...data.map(d => d.negative7p) + ]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const valuePadding = (maxValue - minValue) * 0.1; + + return { + animation: false, + progressive: 500, + progressiveThreshold: 1000, + renderer: 'canvas', + title: { + text: 'Preventive Maintenance Monitoring', + subtext: 'Power consumption with predictive thresholds', + textStyle: { + fontSize: 20, + fontWeight: 'bold', + color: theme.palette.text.primary, + fontFamily: 'Montserrat, sans-serif' + }, + subtextStyle: { + fontSize: 14, + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif' + }, + left: '5%', + top: 0, + padding: [0, 0, 12, 0] + }, + 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]}W +
+ `; + }); + return html; + } + }, + legend: { + data: ['Current Power', 'Predicted Power', '\u00b13% Threshold', '\u00b17% Threshold'], + top: 'top', + right: '5%', + padding: [8, 16], + itemGap: 24, + textStyle: { + fontFamily: 'Montserrat, sans-serif', + fontSize: 13, + fontWeight: 500, + color: theme.palette.text.secondary + }, + icon: 'circle', + itemHeight: 10, + itemWidth: 10, + selectedMode: false + }, + grid: { + left: '5%', + right: '5%', + bottom: '12%', + top: '15%', + containLabel: true + }, + toolbox: { + right: 90, + top: 0, + feature: { + dataZoom: { + yAxisIndex: 'none', + title: { + zoom: 'Zoom', + back: 'Reset Zoom' + } + }, + restore: { + title: 'Reset' + }, + saveAsImage: { + title: 'Save' + } + } + }, + dataZoom: [ + { + type: 'slider', + show: true, + xAxisIndex: [0], + start: zoomRange?.[0] ?? 0, + end: zoomRange?.[1] ?? 100, + height: 20, + bottom: 10, + 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', + fontSize: 12 + }, + axisLine: { + lineStyle: { + color: theme.palette.divider + } + }, + splitLine: { + show: true, + lineStyle: { + color: theme.palette.divider, + type: 'dashed', + opacity: 0.5 + } + } + }, + yAxis: { + type: 'value', + name: 'Power (W)', + nameTextStyle: { + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif', + fontSize: 13, + fontWeight: 'bold', + padding: [0, 0, 8, 0] + }, + min: Math.max(0, minValue - valuePadding), + max: maxValue + valuePadding, + axisLabel: { + formatter: '{value}W', + color: theme.palette.text.secondary, + fontFamily: 'Montserrat, sans-serif', + fontSize: 12 + }, + axisLine: { + lineStyle: { + color: theme.palette.divider + } + }, + splitLine: { + lineStyle: { + color: theme.palette.divider, + type: 'dashed', + opacity: 0.3 + } + } + }, + series: [ + { + ...commonSeriesSettings, + name: 'Current Power', + type: 'line', + data: currentData, + smooth: true, + lineStyle: { + color: theme.palette.primary.main, + width: 3 + }, + itemStyle: { + color: theme.palette.primary.main, + borderWidth: 2, + borderColor: '#fff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: `rgba(2, 138, 74, 0.3)` + }, { + offset: 1, + color: `rgba(2, 138, 74, 0.05)` + }] + } + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: 'Predicted Power', + type: 'line', + data: predictedData, + smooth: true, + lineStyle: { + color: theme.palette.warning.main, + width: 3, + type: 'dashed' + }, + itemStyle: { + color: theme.palette.warning.main, + borderWidth: 2, + borderColor: '#fff' + }, + symbol: 'circle', + symbolSize: 6, + showSymbol: true + }, + { + ...commonSeriesSettings, + name: '\u00b13% Threshold', + type: 'line', + data: positive3pData.concat(negative3pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.warning.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.warning.light, + opacity: 0.1 + }, + symbol: 'none' + }, + { + ...commonSeriesSettings, + name: '\u00b17% Threshold', + type: 'line', + data: positive7pData.concat(negative7pData.reverse()), + smooth: true, + lineStyle: { + color: theme.palette.error.light, + width: 1, + type: 'dashed' + }, + areaStyle: { + color: theme.palette.error.light, + opacity: 0.1 + }, + symbol: 'none' + } + ] + }; + }, [data, theme, zoomRange]); + + if (!data || data.length === 0) { + return ( + + + No maintenance data available + + + ); + } + + return ( + + { + if (onZoomChange && params.batch?.[0]) { + onZoomChange([params.batch[0].start, params.batch[0].end]); + } + } + }} + /> + + ); +}, (prevProps, nextProps) => { + 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; + + 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.currentPower !== nextItem.currentPower || + prevItem.predictedPower !== nextItem.predictedPower) { + return false; + } + } + + return true; +}); + +MaintenanceChart.displayName = 'MaintenanceChart'; + +export default MaintenanceChart; \ No newline at end of file diff --git a/src/pages/Maintenance.tsx b/src/pages/Maintenance.tsx index a4358e9..bdd67aa 100644 --- a/src/pages/Maintenance.tsx +++ b/src/pages/Maintenance.tsx @@ -1,31 +1,7 @@ -import { useState, useEffect } from 'react'; -import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip, Slider} from '@mui/material'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - TimeScale -} from 'chart.js'; -import 'chartjs-adapter-date-fns'; -import { Line } from 'react-chartjs-2'; +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { Box, Paper, Typography, AppBar, Toolbar, Chip } from '@mui/material'; import { config } from '../config/env'; - -// Register Chart.js components -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - TimeScale -); +import MaintenanceChart from '../components/Charts/MaintenanceChart'; interface DataItem { now_timestamp: string; @@ -39,380 +15,183 @@ interface DataItem { flag: string; } -const API_BASE_URL = config.apiUrl; - const Maintenance = () => { - const theme = useTheme(); - const [data, setData] = useState([]); const [currentFlag, setCurrentFlag] = useState(''); - const [, setLoading] = useState(true); - const [windowSize, setWindowSize] = useState(20); + const [isLoading, setIsLoading] = useState(false); + const [windowSize, setWindowSize] = useState<20 | 50 | 100>(20); + const [zoomRange, setZoomRange] = useState<[number, number]>([0, 100]); + + const lastFetchRef = useRef(0); + const THROTTLE_INTERVAL = 2000; // Minimum 2 seconds between updates + + // Reset zoom when window size changes + useEffect(() => { + setZoomRange([0, 100]); + setData([]); // Reset data when window size changes + }, [windowSize]); + + const fetchData = useCallback(async () => { + const now = Date.now(); + if (now - lastFetchRef.current < THROTTLE_INTERVAL || isLoading) { + return; + } + + try { + setIsLoading(true); + lastFetchRef.current = now; + + const response = await fetch(`${config.apiUrl}/prom/get_chart_data/maintenance/${windowSize}`); + const result = await response.json(); + + if (result.data && result.data.length > 0) { + const newData = [...result.data] + .filter((item: DataItem) => + item.now_timestamp && + item.future_timestamp && + item.power && + item.power_future_min && + item.positive_3p && + item.negative_3p && + item.positive_7p && + item.negative_7p + ) + .sort((a, b) => new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime()); + + setCurrentFlag(newData[newData.length - 1].flag); + setData(newData.slice(-windowSize)); + } + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setIsLoading(false); + } + }, [windowSize, isLoading]); useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/maintenance/20`); - const result = await response.json(); - - if (result.data && result.data.length > 0) { - const last20Data = result.data.slice(-20); - setCurrentFlag(last20Data[last20Data.length - 1].flag); - setData(last20Data); - console.log('Fetched data:', last20Data); - } - } catch (error) { - console.error('Error fetching data:', error); - } finally { - setLoading(false); - } - }; - fetchData(); const interval = setInterval(fetchData, 5000); return () => clearInterval(interval); - }, []); + }, [fetchData]); - // Process data for charts with sliding window - const prepareChartData = () => { - if (!data || data.length === 0) { - console.log('No data available, using fallback data'); - // Fallback data for testing - const now = new Date(); - const fallbackData = Array.from({ length: 10 }, (_, i) => ({ - currentTimestamp: new Date(now.getTime() - (9 - i) * 60000), // 1 minute intervals - futureTimestamp: new Date(now.getTime() - (9 - i) * 60000 + 3 * 60000), // 3 minutes in the future - currentPower: 95 + Math.random() * 10, - predictedPower: 115 + Math.random() * 10, - positive3p: 118 + Math.random() * 5, - negative3p: 112 + Math.random() * 5, - positive7p: 123 + Math.random() * 5, - negative7p: 107 + Math.random() * 5, - })); - return fallbackData; - } - - const processedData = data.map(item => { - const currentPower = parseFloat(item.power) || 0; - const predictedPower = parseFloat(item.power_future_min) || currentPower * 1.1; // Fallback to 10% higher than current - - return { - currentTimestamp: new Date(item.now_timestamp), - futureTimestamp: new Date(item.future_timestamp), - currentPower: currentPower, - predictedPower: predictedPower, - positive3p: parseFloat(item.positive_3p) || predictedPower * 1.03, - negative3p: parseFloat(item.negative_3p) || predictedPower * 0.97, - positive7p: parseFloat(item.positive_7p) || predictedPower * 1.07, - negative7p: parseFloat(item.negative_7p) || predictedPower * 0.93, - }; - }); - - // Apply sliding window - show only last N records - const slidingData = processedData.slice(-windowSize); - - console.log('Processed chart data:', { - totalRecords: processedData.length, - showingRecords: slidingData.length, - timeRange: { - start: slidingData[0]?.currentTimestamp, - end: slidingData[slidingData.length - 1]?.currentTimestamp - } - }); - - console.log('Data validation:', { - hasCurrentPower: slidingData.some(d => d.currentPower > 0), - hasPredictedPower: slidingData.some(d => d.predictedPower > 0), - currentPowerRange: [Math.min(...slidingData.map(d => d.currentPower)), Math.max(...slidingData.map(d => d.currentPower))], - predictedPowerRange: [Math.min(...slidingData.map(d => d.predictedPower)), Math.max(...slidingData.map(d => d.predictedPower))], - rawDataSample: data.slice(-2).map(item => ({ - power: item.power, - power_future_min: item.power_future_min, - parsedCurrent: parseFloat(item.power), - parsedPredicted: parseFloat(item.power_future_min) - })) - }); - - return slidingData; - }; - - const chartData = prepareChartData(); - - // Prepare Chart.js data structure - const chartJsData = { - datasets: [ - { - label: 'Current Power', - data: chartData.map(item => ({ - x: item.currentTimestamp, - y: item.currentPower - })), - borderColor: '#028a4a', - backgroundColor: '#028a4a', - pointBackgroundColor: '#028a4a', - pointBorderColor: '#028a4a', - pointRadius: 0, - pointHoverRadius: 6, - tension: 0.4, - fill: false - }, - { - label: 'Predicted (Dynamic)', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.predictedPower - })), - borderColor: '#ff9800', - backgroundColor: '#ff9800', - pointBackgroundColor: '#ff9800', - pointBorderColor: '#ff9800', - pointRadius: 0, - pointHoverRadius: 6, - tension: 0.4, - fill: false - }, - { - label: '+3% Positive', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.positive3p - })), - borderColor: '#ffb400', - backgroundColor: 'rgba(255, 180, 0, 0.1)', - pointRadius: 0, - tension: 0.4, - fill: false - }, - { - label: '-3% Negative', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.negative3p - })), - borderColor: '#ffb400', - backgroundColor: 'rgba(255, 180, 0, 0.1)', - pointRadius: 0, - tension: 0.4, - fill: false - }, - { - label: '+7% Positive', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.positive7p - })), - borderColor: '#FF1744', - backgroundColor: 'rgba(255, 23, 68, 0.1)', - pointRadius: 0, - tension: 0.4, - fill: false - }, - { - label: '-7% Negative', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.negative7p - })), - borderColor: '#FF1744', - backgroundColor: 'rgba(255, 23, 68, 0.1)', - pointRadius: 0, - tension: 0.4, - fill: false - } - ] - }; - - // Chart.js options - const chartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top' as const, - labels: { - usePointStyle: true, - padding: 20, - font: { - size: 12, - weight: 'normal' as const - } - } - }, - tooltip: { - mode: 'index' as const, - intersect: false, - callbacks: { - title: function(context: any) { - const date = new Date(context[0].parsed.x); - return date.toLocaleTimeString(); - }, - label: function(context: any) { - return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} W`; - } - } - } - }, - scales: { - x: { - type: 'time' as const, - time: { - displayFormats: { - minute: 'HH:mm:ss' - } - }, - title: { - display: true, - text: 'Time' - } - }, - y: { - title: { - display: true, - text: 'Power (W)' - }, - ticks: { - callback: function(value: any) { - return `${value} W`; - } - } - } - }, - interaction: { - mode: 'nearest' as const, - axis: 'x' as const, - intersect: false - } - }; - - // Debug logging - console.log('Chart.js data structure:', chartJsData); + // Transform data for the maintenance chart + const chartData = useMemo(() => { + return data.map(item => ({ + currentTimestamp: new Date(item.now_timestamp), + futureTimestamp: new Date(item.future_timestamp), + currentPower: parseFloat(item.power), + predictedPower: parseFloat(item.power_future_min), + positive3p: parseFloat(item.positive_3p), + negative3p: parseFloat(item.negative_3p), + positive7p: parseFloat(item.positive_7p), + negative7p: parseFloat(item.negative_7p) + })); + }, [data]); return ( - - + - - - Preventive Maintenance - - - {currentFlag && ( - theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' + }} + > + + + + Preventive Maintenance Monitoring + + + Real-time power consumption with predictive indicators + + + + - )} - - - - + + + - - - + - - - - - {/* Chart Controls */} - - - Chart Settings - - - - Records to show: - - setWindowSize(value as number)} - min={5} - max={50} - step={5} - marks={[ - { value: 5, label: '5' }, - { value: 20, label: '20' }, - { value: 50, label: '50' } - ]} - sx={{ flex: 1, maxWidth: 200 }} - /> - - {windowSize} - - - - - - + {[20, 50, 100].map((size) => ( + setWindowSize(size as 20 | 50 | 100)} + sx={{ + px: 2, + py: 1, + borderRadius: 1.5, + border: 1, + borderColor: windowSize === size ? 'primary.main' : 'divider', + cursor: 'pointer', + bgcolor: windowSize === size ? 'primary.main' : 'transparent', + color: windowSize === size ? 'primary.contrastText' : 'text.primary', + fontSize: '0.875rem', + fontWeight: 500, + '&:hover': { + bgcolor: windowSize === size ? 'primary.dark' : 'action.hover', + borderColor: windowSize === size ? 'primary.dark' : 'primary.main' + }, + transition: theme => theme.transitions.create(['all'], { + duration: 200 + }) + }} + > + {size} points + + ))} + + + + ); }; -export default Maintenance; +export default Maintenance; \ No newline at end of file