From 531dfd8715334d11388c3c47b984c1ae2b5793e2 Mon Sep 17 00:00:00 2001 From: k3n9achi Date: Sun, 21 Sep 2025 03:26:48 +0300 Subject: [PATCH] UpGraded tempreture charts version 2.0, using Echarts library, optomized rendering and added proper data selection options --- .github/copilot-instructions.md | 125 +++ package-lock.json | 70 +- package.json | 3 + src/components/Charts/MonitoringChart.tsx | 367 ++++++++ src/components/Charts/PowerEChart.tsx | 347 +++++++ .../Charts/TemperatureDashboard.tsx | 202 ++++ src/components/Charts/TemperatureEChart.tsx | 344 +++++++ src/pages/Temperature.tsx | 861 +++--------------- 8 files changed, 1573 insertions(+), 746 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 src/components/Charts/MonitoringChart.tsx create mode 100644 src/components/Charts/PowerEChart.tsx create mode 100644 src/components/Charts/TemperatureDashboard.tsx create mode 100644 src/components/Charts/TemperatureEChart.tsx diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0c2d672 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,125 @@ +# Copilot Instructions for AyposWeb + +## Project Overview +AyposWeb is a React-based monitoring system for virtual machine infrastructure with real-time data visualization. It provides VM migration advice, environmental monitoring, stress testing, and preventive maintenance features through a Material-UI interface. + +## Tech Stack & Architecture +- **Frontend**: React 18 + TypeScript + Vite +- **UI Framework**: Material-UI (MUI) v5 with custom B'GREEN branding +- **Charts**: Multiple libraries (Plotly.js, Recharts, Chart.js, MUI X-Charts) +- **Routing**: React Router v6 with declarative routes in `App.tsx` +- **State**: React hooks with custom polling patterns (no external state management) +- **Build**: Vite with standard TypeScript config +- **Deployment**: Docker + nginx or Vercel with API proxy + +## Core Architecture Patterns + +### Service Layer Pattern +Services use singleton pattern with axios HTTP client: +```typescript +// All services follow this pattern in src/services/ +class ServiceName { + private static instance: ServiceName; + public static getInstance(): ServiceName { /* singleton */ } +} +``` + +### Smart Polling Hook Pattern +Custom hooks in `src/components/Migration/hooks.ts` implement adaptive polling: +- Starts with fast intervals (5s) when data changes +- Gradually increases to slower intervals (30s) when stable +- Always use `useSmartPolling` for real-time data fetching + +### Environment Configuration +```typescript +// src/config/env.ts handles dev/prod API routing +const getApiUrl = (): string => { + if (import.meta.env.PROD) return '/api'; // Vercel proxy + return import.meta.env.VITE_API_URL || 'fallback-url'; +}; +``` + +## Component Organization + +### Layout Structure +- `MainLayout` provides responsive sidebar with mobile menu toggle +- `Sidebar` uses custom styled MUI components with B'GREEN theming +- Pages in `src/pages/` are route components that compose feature components + +### Material-UI Customization +```typescript +// src/theme.ts defines B'GREEN brand colors +primary: { main: '#028a4a' } // B'GREEN green +``` +- Use `styled()` for component-specific styling +- Custom `StyledListItemButton` pattern for navigation +- Responsive breakpoints with `useMediaQuery(theme.breakpoints.down('md'))` + +### Chart Component Patterns +Multiple charting libraries used for different purposes: +- **Plotly.js**: Complex scientific visualizations in migration analysis +- **Recharts**: Simple bar/line charts for resource distribution +- **MUI X-Charts**: Native MUI integration for basic charts +- **Chart.js**: Time-series data with date-fns adapter + +## Data Flow Patterns + +### API Integration +```typescript +// Services handle all external API calls +const response = await axios.post(`${BASE_URL}/endpoint`, data, { + headers: { 'Content-Type': 'application/json' } +}); +``` + +### Type Safety +- Strict TypeScript with comprehensive interfaces in `src/types/` +- Migration-specific types in `src/components/Migration/types.ts` +- Monitoring types shared across services in `src/types/monitoring.ts` + +### Error Handling +- Services log requests for debugging: `console.log('Starting operation with config:', config)` +- Use try/catch with proper error propagation to UI components + +## Development Workflows + +### Build Commands +```bash +npm run dev # Vite dev server on :5173 +npm run build # TypeScript compilation + Vite build +npm run preview # Preview production build +``` + +### Environment Setup +- Development: Uses `VITE_API_URL` environment variable +- Production: API calls routed through `/api` proxy (see `vercel.json`) +- No `.env.example` - check README for environment variables + +### Key Files to Understand +- `src/App.tsx`: Route definitions and global providers +- `src/components/Layout/MainLayout.tsx`: Responsive layout logic +- `src/services/monitoringService.ts`: Main API service patterns +- `src/components/Migration/hooks.ts`: Custom polling and data fetching +- `src/theme.ts`: B'GREEN brand implementation + +## Project-Specific Conventions + +### Import Organization +1. React/third-party imports +2. MUI component imports +3. Local component imports +4. Service/utility imports +5. Asset imports (images/icons) + +### Component Naming +- Page components: PascalCase in `src/pages/` +- Feature components: PascalCase in feature folders under `src/components/` +- Service files: camelCase with "Service" suffix + +### Styling Approach +- Use MUI's `styled()` over CSS-in-JS or external CSS +- Custom styled components follow `Styled{ComponentName}` naming +- Responsive design with MUI breakpoint system +- B'GREEN brand colors defined in theme, not hardcoded + +When adding new features, follow the established service layer pattern, use TypeScript interfaces for all data structures, and implement smart polling for real-time data when appropriate. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 32b2c6a..cd63448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,14 @@ "@mui/material": "^5.15.3", "@mui/x-charts": "^8.9.0", "@mui/x-tree-view": "^7.26.0", + "@types/echarts": "^4.9.22", "@types/plotly.js": "^2.35.2", "axios": "^1.8.4", "chart.js": "^4.5.0", "chartjs-adapter-date-fns": "^3.0.0", "date-fns": "^3.6.0", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "lucide-react": "^0.525.0", "plotly.js": "^3.0.1", "plotly.js-dist": "^3.0.1", @@ -2717,6 +2720,15 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/echarts": { + "version": "4.9.22", + "resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.22.tgz", + "integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==", + "license": "MIT", + "dependencies": { + "@types/zrender": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2842,6 +2854,12 @@ "@types/geojson": "*" } }, + "node_modules/@types/zrender": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/zrender/-/zrender-4.0.6.tgz", + "integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -4068,6 +4086,36 @@ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "license": "ISC" }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz", + "integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/electron-to-chromium": { "version": "1.5.190", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", @@ -4584,7 +4632,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -7182,6 +7229,12 @@ "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", "license": "MIT" }, + "node_modules/size-sensor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", + "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==", + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7879,6 +7932,21 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" } } } diff --git a/package.json b/package.json index a51727f..15a6153 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,14 @@ "@mui/material": "^5.15.3", "@mui/x-charts": "^8.9.0", "@mui/x-tree-view": "^7.26.0", + "@types/echarts": "^4.9.22", "@types/plotly.js": "^2.35.2", "axios": "^1.8.4", "chart.js": "^4.5.0", "chartjs-adapter-date-fns": "^3.0.0", "date-fns": "^3.6.0", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "lucide-react": "^0.525.0", "plotly.js": "^3.0.1", "plotly.js-dist": "^3.0.1", diff --git a/src/components/Charts/MonitoringChart.tsx b/src/components/Charts/MonitoringChart.tsx new file mode 100644 index 0000000..31ba2ff --- /dev/null +++ b/src/components/Charts/MonitoringChart.tsx @@ -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 = 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 = `
${time}
`; + params.forEach((param: any) => { + html += ` +
+ + ${param.seriesName}: + ${param.value[1]}${unit} +
+ `; + }); + 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 ( + + + No data available + + + ); + } + + return ( + + { + if (onZoomChange && params.batch?.[0]) { + onZoomChange([params.batch[0].start, params.batch[0].end]); + } + } + }} + /> + + ); +}, (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; \ No newline at end of file diff --git a/src/components/Charts/PowerEChart.tsx b/src/components/Charts/PowerEChart.tsx new file mode 100644 index 0000000..ee38b91 --- /dev/null +++ b/src/components/Charts/PowerEChart.tsx @@ -0,0 +1,347 @@ +import React, { useMemo } from 'react'; +import ReactECharts from 'echarts-for-react'; +import { Box, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import type { Theme } from '@mui/material/styles'; +import type { EChartsOption } from 'echarts'; + +interface PowerDataPoint { + currentTimestamp: Date; + futureTimestamp: Date; + currentPower: number; + predictedPower: number; +} + +interface PowerEChartProps { + data: PowerDataPoint[]; + height?: number; + zoomRange?: [number, number]; + onZoomChange?: (range: [number, number]) => void; +} + +const PowerEChart = ({ data, height = 400, zoomRange, onZoomChange }: PowerEChartProps): JSX.Element => { + const theme = useTheme(); + + const option = useMemo(() => { + if (!data || data.length === 0) { + return {}; + } + + const currentPowerData = data.map(item => [ + item.currentTimestamp.getTime(), + Number(item.currentPower.toFixed(2)) + ]); + + const predictedPowerData = data.map(item => [ + item.futureTimestamp.getTime(), + Number(item.predictedPower.toFixed(2)) + ]); + + const allPowers = [...data.map(d => d.currentPower), ...data.map(d => d.predictedPower)]; + const minPower = Math.min(...allPowers); + const maxPower = Math.max(...allPowers); + const powerPadding = (maxPower - minPower) * 0.1; + const avgCurrentPower = data.reduce((sum, d) => sum + d.currentPower, 0) / data.length; + const avgPredictedPower = data.reduce((sum, d) => sum + d.predictedPower, 0) / data.length; + const efficiencyGain = ((avgCurrentPower - avgPredictedPower) / avgCurrentPower * 100).toFixed(1); + + return { + backgroundColor: theme.palette.background.paper, + title: { + text: 'Power Consumption Analysis', + subtext: `Efficiency Optimization: ${efficiencyGain}% reduction predicted`, + textStyle: { + fontSize: 16, + color: theme.palette.text.primary, + fontFamily: theme.typography.fontFamily + }, + subtextStyle: { + fontSize: 12, + color: theme.palette.success.main, + fontFamily: theme.typography.fontFamily + }, + left: 'center', + top: 10 + }, + grid: { + left: '4%', + right: '4%', + bottom: '12%', + top: '15%', + containLabel: true + }, + legend: { + data: ['Current Power', 'Predicted Power'], + bottom: '2%', + left: 'center', + textStyle: { + color: theme.palette.text.secondary, + fontSize: 12, + fontFamily: theme.typography.fontFamily + }, + icon: 'circle', + itemWidth: 10, + itemHeight: 10, + itemGap: 35, + selectedMode: false, + z: 100 + }, + tooltip: { + trigger: 'axis', + backgroundColor: 'rgba(255, 255, 255, 0.95)', + borderColor: theme.palette.primary.main, + borderWidth: 1, + textStyle: { + color: theme.palette.text.primary, + fontFamily: theme.typography.fontFamily + }, + formatter: (params: any) => { + const time = new Date(params[0].value[0]).toLocaleTimeString(); + let html = `
${time}
`; + params.forEach((param: any) => { + html += ` +
+ + ${param.seriesName}: + ${param.value[1]} W +
+ `; + }); + return html; + } + }, + dataZoom: [ + { + type: 'slider', + show: true, + xAxisIndex: [0], + start: zoomRange?.[0] ?? 0, + end: zoomRange?.[1] ?? 100, + height: 20, + bottom: 40, + left: '10%', + right: '10%', + z: 90, + backgroundColor: 'transparent', + borderColor: theme.palette.divider, + fillerColor: 'rgba(0, 136, 255, 0.05)', + textStyle: { + color: theme.palette.text.secondary, + fontFamily: theme.typography.fontFamily, + fontSize: 11 + }, + handleStyle: { + color: theme.palette.primary.main, + borderColor: theme.palette.background.paper, + borderWidth: 1, + shadowBlur: 2, + shadowColor: 'rgba(0,0,0,0.2)', + opacity: 0.8 + }, + selectedDataBackground: { + lineStyle: { + color: '#0088ff', + width: 1 + }, + areaStyle: { + color: 'rgba(0, 136, 255, 0.2)' + } + }, + emphasis: { + handleLabel: { + show: true + }, + handleStyle: { + borderWidth: 3, + shadowBlur: 6, + shadowColor: 'rgba(0,0,0,0.2)' + } + } + }, + { + type: 'inside', + xAxisIndex: [0], + start: 0, + end: 100, + zoomOnMouseWheel: 'shift', + moveOnMouseMove: true + } + ], + xAxis: { + type: 'time', + boundaryGap: false, + axisTick: { show: false }, + axisLabel: { + formatter: (value: number) => { + const date = new Date(value); + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false + }).replace(/^24/, '00'); + }, + color: theme.palette.text.secondary, + fontFamily: theme.typography.fontFamily + }, + 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)', + nameLocation: 'middle', + nameGap: 50, + min: Math.max(0, minPower - powerPadding), + max: maxPower + powerPadding, + axisLabel: { + color: theme.palette.text.secondary, + fontFamily: theme.typography.fontFamily, + formatter: '{value} W' + }, + splitLine: { + lineStyle: { + color: theme.palette.divider, + opacity: 0.3 + } + } + }, + series: [ + { + name: 'Current Power', + type: 'line', + data: currentPowerData, + smooth: true, + symbol: 'circle', + symbolSize: 4, + showSymbol: false, + lineStyle: { + width: 2, + color: '#0088ff', + shadowColor: 'rgba(0,0,0,0.2)', + shadowBlur: 3, + cap: 'round' + }, + itemStyle: { + color: '#0088ff', + borderWidth: 2, + borderColor: '#fff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: 'rgba(0, 136, 255, 0.15)' + }, { + offset: 1, + color: 'rgba(0, 136, 255, 0.02)' + }] + } + } + }, + { + name: 'Predicted Power', + type: 'line', + data: predictedPowerData, + smooth: true, + symbol: 'circle', + symbolSize: 4, + showSymbol: false, + lineStyle: { + width: 2, + type: 'dashed', + color: '#00cc88', + shadowColor: 'rgba(0,0,0,0.1)', + shadowBlur: 3, + cap: 'round' + }, + itemStyle: { + color: '#00cc88', + borderWidth: 2, + borderColor: '#fff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: 'rgba(0, 204, 136, 0.1)' + }, { + offset: 1, + color: 'rgba(0, 204, 136, 0.02)' + }] + } + } + } + ] + }; + }, [data, theme]); + + if (!data || data.length === 0) { + return ( + + + No power consumption data available + + + ); + } + + return ( + + { + if (onZoomChange && params.batch?.[0]) { + onZoomChange([params.batch[0].start, params.batch[0].end]); + } + } + }} + /> + + ); +}; + +export default PowerEChart; \ No newline at end of file diff --git a/src/components/Charts/TemperatureDashboard.tsx b/src/components/Charts/TemperatureDashboard.tsx new file mode 100644 index 0000000..2acbe73 --- /dev/null +++ b/src/components/Charts/TemperatureDashboard.tsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { Box, Grid, Card, CardContent, Typography, Chip, LinearProgress } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import TrendingDownIcon from '@mui/icons-material/TrendingDown'; +import PowerIcon from '@mui/icons-material/Power'; +import ThermostatIcon from '@mui/icons-material/Thermostat'; +import SavingsIcon from '@mui/icons-material/Savings'; +import InsightsIcon from '@mui/icons-material/Insights'; + +interface DashboardData { + currentTimestamp: Date; + futureTimestamp: Date; + currentPower: number; + predictedPower: number; + currentTemp: number; + predictedTemp: number; +} + +interface TemperatureDashboardProps { + data: DashboardData[]; +} + +const TemperatureDashboard: React.FC = ({ data }) => { + const theme = useTheme(); + + if (!data || data.length === 0) { + return null; + } + + const latest = data[data.length - 1]; + const avgCurrentPower = data.reduce((sum, d) => sum + d.currentPower, 0) / data.length; + const avgPredictedPower = data.reduce((sum, d) => sum + d.predictedPower, 0) / data.length; + const avgCurrentTemp = data.reduce((sum, d) => sum + d.currentTemp, 0) / data.length; + const avgPredictedTemp = data.reduce((sum, d) => sum + d.predictedTemp, 0) / data.length; + + const powerEfficiency = ((avgCurrentPower - avgPredictedPower) / avgCurrentPower * 100); + const tempOptimization = avgCurrentTemp - avgPredictedTemp; + const costSavings = powerEfficiency * 0.12; // Assuming $0.12/kWh + + const metrics = [ + { + title: 'Power Optimization', + value: `${powerEfficiency.toFixed(1)}%`, + subtitle: 'Efficiency Gain', + icon: , + color: theme.palette.success.main, + trend: powerEfficiency > 0 ? 'up' : 'down', + progress: Math.min(Math.abs(powerEfficiency), 100) + }, + { + title: 'Temperature Control', + value: `${Math.abs(tempOptimization).toFixed(2)}°C`, + subtitle: tempOptimization > 0 ? 'Reduction' : 'Increase', + icon: , + color: tempOptimization > 0 ? theme.palette.success.main : theme.palette.warning.main, + trend: tempOptimization > 0 ? 'down' : 'up', + progress: Math.min(Math.abs(tempOptimization) * 20, 100) + }, + { + title: 'Cost Savings', + value: `$${Math.abs(costSavings).toFixed(2)}`, + subtitle: 'Per Hour', + icon: , + color: theme.palette.success.main, + trend: costSavings > 0 ? 'up' : 'down', + progress: Math.min(Math.abs(costSavings) * 10, 100) + }, + { + title: 'Current Status', + value: `${latest.currentPower.toFixed(0)}W`, + subtitle: `${latest.currentTemp.toFixed(1)}°C`, + icon: , + color: theme.palette.primary.main, + trend: 'neutral', + progress: 85 + } + ]; + + return ( + + + Real-time Performance Analytics + + + + {metrics.map((metric, index) => ( + + + + + + {metric.icon} + + + + {metric.value} + + + {metric.trend !== 'neutral' && ( + : } + label={metric.trend === 'up' ? '+' : '-'} + size="small" + sx={{ + bgcolor: metric.trend === 'up' ? + `${theme.palette.success.main}20` : + `${theme.palette.error.main}20`, + color: metric.trend === 'up' ? + theme.palette.success.main : + theme.palette.error.main, + '& .MuiChip-icon': { + color: 'inherit' + } + }} + /> + )} + + + + {metric.title} + + + + {metric.subtitle} + + + + + + + ))} + + + ); +}; + +export default TemperatureDashboard; \ No newline at end of file diff --git a/src/components/Charts/TemperatureEChart.tsx b/src/components/Charts/TemperatureEChart.tsx new file mode 100644 index 0000000..ce8926b --- /dev/null +++ b/src/components/Charts/TemperatureEChart.tsx @@ -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 = 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; \ No newline at end of file diff --git a/src/pages/Temperature.tsx b/src/pages/Temperature.tsx index f90b614..fa3a7fb 100644 --- a/src/pages/Temperature.tsx +++ b/src/pages/Temperature.tsx @@ -1,38 +1,8 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; -import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip as MuiTooltip, Chip, Button, Snackbar, Alert, Slider } from '@mui/material'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import CancelIcon from '@mui/icons-material/Cancel'; -import { monitoringService } from '../services/monitoringService'; -import { MonitoringStatus } from '../types/monitoring'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip as ChartTooltip, - Legend, - TimeScale -} from 'chart.js'; -import 'chartjs-adapter-date-fns'; -import { Line } from 'react-chartjs-2'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; +import { Box, Paper, Grid } from '@mui/material'; import { config } from '../config/env'; +import MonitoringChart from '../components/Charts/MonitoringChart'; -// Register Chart.js components -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - ChartTooltip, - Legend, - TimeScale -); - -// Define the structure of our data interface ChartData { power: string; flag: string; @@ -43,740 +13,141 @@ interface ChartData { power_future_min: string; } -const API_BASE_URL = config.apiUrl; +type TimeRange = 20 | 50 | 100; const Temperature = () => { - const theme = useTheme(); const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(null); - const [refreshing, setRefreshing] = useState(false); - const [decisionLoading, setDecisionLoading] = useState(false); - const [windowSize, setWindowSize] = useState(20); - const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ - open: false, - message: '', - severity: 'success' - }); - - // Monitoring status state - const [monitoringStatus, setMonitoringStatus] = useState(null); - - // Use refs to keep track of the interval - const intervalRef = useRef | null>(null); - const updateIntervalMs = 5000; // 5 seconds refresh rate + const [timeRange, setTimeRange] = useState(20); + const [powerZoom, setPowerZoom] = useState<[number, number]>([0, 100]); + const [tempZoom, setTempZoom] = useState<[number, number]>([0, 100]); + + const [isLoading, setIsLoading] = useState(false); + const lastFetchRef = useRef(0); + const THROTTLE_INTERVAL = 2000; // Minimum 2 seconds between updates + + // Reset zoom when timeRange changes + useEffect(() => { + setPowerZoom([0, 100]); + setTempZoom([0, 100]); + setData([]); // Reset data when timeRange changes + }, [timeRange]); + + const fetchData = useCallback(async () => { + const now = Date.now(); + if (now - lastFetchRef.current < THROTTLE_INTERVAL || isLoading) { + return; + } - // Create a memoized fetchData function with useCallback - const fetchData = useCallback(async (showLoadingIndicator = false) => { try { - if (showLoadingIndicator) { - setRefreshing(true); - } - - const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/temperature/20`); + setIsLoading(true); + lastFetchRef.current = now; + + const response = await fetch(`${config.apiUrl}/prom/get_chart_data/temperature/${timeRange}`); const result = await response.json(); - if (result.data && result.data.length > 0) { - // Sort by timestamp first to ensure we get the latest data - const sortedData = [...result.data].sort((a, b) => - new Date(b.now_timestamp).getTime() - new Date(a.now_timestamp).getTime() - ); + if (result.data?.length > 0) { + const newData = [...result.data] + .filter((item: ChartData) => + item.now_timestamp && + item.future_timestamp && + item.power && + item.power_future_min && + item.env_temp_cur && + item.env_temp_min + ) + .sort((a, b) => new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime()); - // Log the most recent flag - console.log('Most recent flag:', sortedData[0].flag); - - // Filter valid data points - const validData = sortedData.filter((item: any) => - item.now_timestamp && - item.future_timestamp && - item.power && - item.power_future_min && - item.env_temp_cur && - item.env_temp_min - ); - - // Limit to last 20 records but maintain chronological order - const last20Data = validData.slice(-20).sort((a, b) => - new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime() - ); - - console.log(`Data updated at ${new Date().toLocaleTimeString()}:`, { - totalRecords: result.data.length, - validRecords: validData.length, - displayedRecords: last20Data.length, - latestFlag: last20Data[last20Data.length - 1]?.flag - }); - - setData(last20Data); - setLastUpdated(new Date()); + // Replace data completely when timeRange changes + setData(newData.slice(-timeRange)); } } catch (error) { console.error('Error fetching data:', error); } finally { - setLoading(false); - setRefreshing(false); + setIsLoading(false); } - }, []); + }, [timeRange, isLoading]); - // Manual refresh handler - const handleRefresh = () => { - fetchData(true); - }; - - // Set up the interval for real-time updates useEffect(() => { - // Initial fetch - fetchData(true); - - // Set up interval for auto-refresh - intervalRef.current = setInterval(() => { - console.log(`Auto-refreshing data at ${new Date().toLocaleTimeString()}`); - fetchData(false); - }, updateIntervalMs); - - // Cleanup function to clear the interval when component unmounts - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }; + fetchData(); + const interval = setInterval(fetchData, 5000); + return () => clearInterval(interval); }, [fetchData]); - // Set up monitoring status polling - useEffect(() => { - // Start polling for monitoring status - monitoringService.startStatusPolling((status) => { - setMonitoringStatus(status); - }, 5000); // Poll every 5 seconds - - // Cleanup function to stop polling when component unmounts - return () => { - monitoringService.stopStatusPolling(); - }; - }, []); - - // 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, - currentTemp: 25 + Math.random() * 5, - predictedTemp: 28 + Math.random() * 5, - })); - return fallbackData; - } - - const processedData = data.map(item => ({ + // Memoize the transformed data to prevent unnecessary recalculations + const { temperatureData, powerData } = useMemo(() => ({ + temperatureData: 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), - currentTemp: parseFloat(item.env_temp_cur), - predictedTemp: parseFloat(item.env_temp_min), - })); - - // 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 - } - }); - - return slidingData; - }; - - const chartData = prepareChartData(); - - // Prepare Chart.js data structures - const powerChartData = { - 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 Power (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 - } - ] - }; - - const temperatureChartData = { - datasets: [ - { - label: 'Current Temperature', - data: chartData.map(item => ({ - x: item.currentTimestamp, - y: item.currentTemp - })), - borderColor: '#028a4a', - backgroundColor: '#028a4a', - pointBackgroundColor: '#028a4a', - pointBorderColor: '#028a4a', - pointRadius: 0, - pointHoverRadius: 6, - tension: 0.4, - fill: false - }, - { - label: 'Predicted Temperature (Dynamic)', - data: chartData.map(item => ({ - x: item.futureTimestamp, - y: item.predictedTemp - })), - borderColor: '#ff9800', - backgroundColor: '#ff9800', - pointBackgroundColor: '#ff9800', - pointBorderColor: '#ff9800', - pointRadius: 0, - pointHoverRadius: 6, - 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)}`; - } - } - } - }, - scales: { - x: { - type: 'time' as const, - time: { - displayFormats: { - minute: 'HH:mm:ss' - } - }, - title: { - display: true, - text: 'Time' - } - }, - y: { - title: { - display: true, - text: 'Value' - } - } - }, - interaction: { - mode: 'nearest' as const, - axis: 'x' as const, - intersect: false - } - }; - - // Debug logging - console.log('Chart.js data structures:', { powerChartData, temperatureChartData }); - console.log('Raw data length:', data.length); - - // Handle temperature decision - const handleTemperatureDecision = async (approval: boolean) => { - try { - setDecisionLoading(true); - const response = await fetch(`${API_BASE_URL}/prom/temperature/decisions?approval=${approval}`, { - method: 'POST', - headers: { - 'accept': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`Failed to send temperature decision: ${response.statusText}`); - } - - const result = await response.json(); - setAlert({ - open: true, - message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`, - severity: 'success' - }); - - // Refresh data after decision - await fetchData(true); - } catch (error) { - console.error('Error sending temperature decision:', error); - setAlert({ - open: true, - message: error instanceof Error ? error.message : 'Failed to send temperature decision', - severity: 'error' - }); - } finally { - setDecisionLoading(false); - } - }; - - const handleCloseAlert = () => { - setAlert(prev => ({ ...prev, open: false })); - }; + currentValue: parseFloat(item.env_temp_cur), + predictedValue: parseFloat(item.env_temp_min), + })), + powerData: data.map(item => ({ + currentTimestamp: new Date(item.now_timestamp), + futureTimestamp: new Date(item.future_timestamp), + currentValue: parseFloat(item.power), + predictedValue: parseFloat(item.power_future_min), + })), + }), [data]); return ( - - - - - - Environmental Temperature & Power Monitoring - - - - - {lastUpdated && ( - - Last updated: {lastUpdated.toLocaleTimeString()} - - )} - - - - - - - - - - - - - - {/* Power Chart */} - - - - - Power Consumption - - {data.length > 0 && ( - - )} - - - {refreshing && ( - - - - )} - - {loading ? ( - - - - Loading power data... - - - ) : ( - - - - )} - - - - {/* Temperature Chart */} - - - - - Environmental Temperature - - {data.length > 0 && ( - - )} - - - {refreshing && ( - - - - )} - - {loading ? ( - - - - Loading temperature data... - - - ) : ( - - - - )} - - - - - - {/* Chart Controls */} - - - - - Chart Settings - - - Configure chart display and temperature change proposals - - - - {/* Temperature Change Proposal Section */} - - - Temperature Change Proposal - - - {(!monitoringStatus?.statuses?.environmental?.is_running || !monitoringStatus?.statuses?.preventive?.is_running) ? ( - - - - Waiting for services... - - - ) : data.length > 0 && chartData.some(item => !isNaN(item.currentTemp) && item.currentTemp > 0) ? ( - - - - - ) : ( - - No temperature data available - - )} - - - - - - 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((range) => ( + setTimeRange(range as TimeRange)} + sx={{ + px: 2, + py: 1, + border: '1px solid rgba(0, 0, 0, 0.08)', + cursor: 'pointer', + bgcolor: timeRange === range ? 'primary.main' : 'background.paper', + color: timeRange === range ? 'primary.contrastText' : 'text.primary', + '&:hover': { + bgcolor: timeRange === range ? 'primary.dark' : 'action.hover' + }, + transition: 'all 0.2s' + }} + > + {range} points + + ))} - - {/* Snackbar for alerts */} - - - {alert.message} - - + + + + + + + + + + + + ); };