forked from BLC/AyposWeb
upgraded the line charts
This commit is contained in:
1840
package-lock.json
generated
1840
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
"@mui/icons-material": "^5.15.3",
|
||||
"@mui/lab": "^5.0.0-alpha.165",
|
||||
"@mui/material": "^5.15.3",
|
||||
"@mui/x-charts": "^8.9.0",
|
||||
"@mui/x-tree-view": "^7.26.0",
|
||||
"@types/plotly.js": "^2.35.2",
|
||||
"axios": "^1.8.4",
|
||||
@@ -30,7 +31,7 @@
|
||||
"react-google-charts": "^5.2.1",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"recharts": "^2.15.1"
|
||||
"recharts": "^2.15.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip } from '@mui/material';
|
||||
import Plot from 'react-plotly.js';
|
||||
import { Layout, PlotData } from 'plotly.js';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import { config } from '../config/env';
|
||||
|
||||
interface DataItem {
|
||||
now_timestamp: string;
|
||||
future_timestamp: string;
|
||||
power: string;
|
||||
power_future_15min: string;
|
||||
power_future_min: string;
|
||||
positive_3p: string;
|
||||
negative_3p: string;
|
||||
positive_7p: string;
|
||||
@@ -21,79 +20,27 @@ const API_BASE_URL = config.apiUrl;
|
||||
const Maintenance = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartData, setChartData] = useState<Partial<PlotData>[]>([]);
|
||||
const [data, setData] = useState<DataItem[]>([]);
|
||||
const [currentFlag, setCurrentFlag] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
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);
|
||||
|
||||
const traces: Partial<PlotData>[] = [
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.now_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.power)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Current Power',
|
||||
line: { color: '#2196f3', width: 2 },
|
||||
marker: { size: 6, symbol: 'circle' }
|
||||
},
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.future_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.power_future_15min)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Predicted (15min)',
|
||||
line: { color: '#4caf50', width: 2, dash: 'dot' },
|
||||
marker: { size: 6, symbol: 'circle' }
|
||||
},
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.future_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.positive_3p)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines' as const,
|
||||
name: '+3% Positive',
|
||||
line: { color: '#2ca02c', width: 1.5 },
|
||||
showlegend: true,
|
||||
},
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.future_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.negative_3p)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines' as const,
|
||||
name: '-3% Negative',
|
||||
line: { color: '#d62728', width: 1.5 },
|
||||
showlegend: true,
|
||||
},
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.future_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.positive_7p)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines' as const,
|
||||
name: '+7% Positive',
|
||||
line: { color: '#9467bd', width: 1.5 },
|
||||
showlegend: true,
|
||||
},
|
||||
{
|
||||
x: last20Data.map((item: DataItem) => item.future_timestamp),
|
||||
y: last20Data.map((item: DataItem) => parseFloat(item.negative_7p)),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines' as const,
|
||||
name: '-7% Negative',
|
||||
line: { color: '#8c564b', width: 1.5 },
|
||||
showlegend: true,
|
||||
}
|
||||
];
|
||||
setChartData(traces);
|
||||
setData(last20Data);
|
||||
console.log('Fetched data:', last20Data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,58 +49,88 @@ const Maintenance = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const layout: Partial<Layout> = {
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'Time',
|
||||
font: { size: 14, color: '#666', family: undefined }
|
||||
},
|
||||
type: 'date',
|
||||
gridcolor: '#eee',
|
||||
tickfont: { size: 12, family: undefined, color: '#666' },
|
||||
showgrid: true,
|
||||
gridwidth: 1,
|
||||
rangeslider: { visible: true }
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Power (W)',
|
||||
font: { size: 14, color: '#666', family: undefined }
|
||||
},
|
||||
gridcolor: '#eee',
|
||||
tickfont: { size: 12, family: undefined, color: '#666' },
|
||||
showgrid: true,
|
||||
gridwidth: 1,
|
||||
rangemode: 'tozero' as const,
|
||||
fixedrange: false,
|
||||
range: [0, Math.max(...chartData.flatMap(trace => trace.y as number[]).filter(Boolean)) * 1.1]
|
||||
},
|
||||
showlegend: true,
|
||||
legend: {
|
||||
orientation: 'h',
|
||||
y: -0.2,
|
||||
x: 0.5,
|
||||
xanchor: 'center',
|
||||
yanchor: 'top',
|
||||
font: {
|
||||
size: 12,
|
||||
family: theme.typography.fontFamily,
|
||||
color: theme.palette.text.secondary
|
||||
},
|
||||
bgcolor: 'rgba(255, 255, 255, 0)',
|
||||
bordercolor: 'rgba(255, 255, 255, 0)'
|
||||
},
|
||||
margin: { t: 60, b: 100, l: 60, r: 20 },
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
hovermode: 'closest',
|
||||
modebar: {
|
||||
bgcolor: 'rgba(255, 255, 255, 0)',
|
||||
color: theme.palette.text.secondary,
|
||||
activecolor: theme.palette.primary.main
|
||||
// Process data for charts
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Processed chart data:', processedData);
|
||||
console.log('Data validation:', {
|
||||
hasCurrentPower: processedData.some(d => d.currentPower > 0),
|
||||
hasPredictedPower: processedData.some(d => d.predictedPower > 0),
|
||||
currentPowerRange: [Math.min(...processedData.map(d => d.currentPower)), Math.max(...processedData.map(d => d.currentPower))],
|
||||
predictedPowerRange: [Math.min(...processedData.map(d => d.predictedPower)), Math.max(...processedData.map(d => d.predictedPower))],
|
||||
rawDataSample: data.slice(0, 2).map(item => ({
|
||||
power: item.power,
|
||||
power_future_min: item.power_future_min,
|
||||
parsedCurrent: parseFloat(item.power),
|
||||
parsedPredicted: parseFloat(item.power_future_min)
|
||||
}))
|
||||
});
|
||||
return processedData;
|
||||
};
|
||||
|
||||
const chartData = prepareChartData();
|
||||
|
||||
// Extract data arrays for charts
|
||||
const currentTimestamps = chartData.map(item => item.currentTimestamp);
|
||||
const futureTimestamps = chartData.map(item => item.futureTimestamp);
|
||||
|
||||
const currentPowerData = chartData.map(item => item.currentPower);
|
||||
const predictedPowerData = chartData.map(item => item.predictedPower);
|
||||
const positive3pData = chartData.map(item => item.positive3p);
|
||||
const negative3pData = chartData.map(item => item.negative3p);
|
||||
const positive7pData = chartData.map(item => item.positive7p);
|
||||
const negative7pData = chartData.map(item => item.negative7p);
|
||||
|
||||
// Debug logging
|
||||
console.log('Chart arrays:', {
|
||||
currentTimestamps: currentTimestamps.length,
|
||||
futureTimestamps: futureTimestamps.length,
|
||||
currentPower: currentPowerData.length,
|
||||
predictedPower: predictedPowerData.length,
|
||||
positive3p: positive3pData.length,
|
||||
negative3p: negative3pData.length,
|
||||
positive7p: positive7pData.length,
|
||||
negative7p: negative7pData.length,
|
||||
});
|
||||
console.log('Sample timestamps:', {
|
||||
current: currentTimestamps.slice(0, 3),
|
||||
future: futureTimestamps.slice(0, 3),
|
||||
currentPower: currentPowerData.slice(0, 3),
|
||||
predictedPower: predictedPowerData.slice(0, 3),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
|
||||
<AppBar
|
||||
@@ -221,23 +198,69 @@ const Maintenance = () => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ height: 'calc(100vh - 200px)', minHeight: '500px' }}>
|
||||
<Plot
|
||||
data={chartData}
|
||||
layout={layout}
|
||||
config={{
|
||||
responsive: true,
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'power_consumption_chart',
|
||||
height: 1000,
|
||||
width: 1500,
|
||||
scale: 2
|
||||
<LineChart
|
||||
height={500}
|
||||
skipAnimation={false}
|
||||
series={[
|
||||
{
|
||||
data: currentPowerData,
|
||||
label: 'Current Power',
|
||||
color: '#028a4a', // B'GREEN brand color
|
||||
showMark: true,
|
||||
curve: 'linear'
|
||||
},
|
||||
{
|
||||
data: predictedPowerData,
|
||||
label: 'Predicted (3min)',
|
||||
color: '#ff9800',
|
||||
showMark: true,
|
||||
curve: 'linear'
|
||||
},
|
||||
{
|
||||
data: positive3pData,
|
||||
label: '+3% Positive',
|
||||
color: '#2ca02c',
|
||||
showMark: false
|
||||
},
|
||||
{
|
||||
data: negative3pData,
|
||||
label: '-3% Negative',
|
||||
color: '#d62728',
|
||||
showMark: false
|
||||
},
|
||||
{
|
||||
data: positive7pData,
|
||||
label: '+7% Positive',
|
||||
color: '#9467bd',
|
||||
showMark: false
|
||||
},
|
||||
{
|
||||
data: negative7pData,
|
||||
label: '-7% Negative',
|
||||
color: '#8c564b',
|
||||
showMark: false
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'time',
|
||||
data: [...currentTimestamps, ...futureTimestamps],
|
||||
valueFormatter: (value: Date) => value.toLocaleTimeString(),
|
||||
},
|
||||
]}
|
||||
yAxis={[
|
||||
{
|
||||
width: 50,
|
||||
valueFormatter: (value: number) => `${value} W`,
|
||||
}
|
||||
]}
|
||||
margin={{ right: 24 }}
|
||||
slotProps={{
|
||||
legend: {
|
||||
direction: 'horizontal',
|
||||
position: { vertical: 'top', horizontal: 'center' },
|
||||
},
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -3,19 +3,9 @@ import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, Circular
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import Plot from 'react-plotly.js';
|
||||
import { Layout, PlotData, Config } from 'plotly.js';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import { config } from '../config/env';
|
||||
|
||||
// Extend the Window interface to include Google Charts
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: {
|
||||
charts?: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Define the structure of our data
|
||||
interface ChartData {
|
||||
power: string;
|
||||
@@ -36,8 +26,6 @@ const Temperature = () => {
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [decisionLoading, setDecisionLoading] = useState(false);
|
||||
const [powerZoom, setPowerZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({});
|
||||
const [tempZoom, setTempZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({});
|
||||
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
@@ -45,7 +33,7 @@ const Temperature = () => {
|
||||
});
|
||||
|
||||
// Use refs to keep track of the interval
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const updateIntervalMs = 5000; // 5 seconds refresh rate
|
||||
|
||||
// Create a memoized fetchData function with useCallback
|
||||
@@ -125,216 +113,46 @@ const Temperature = () => {
|
||||
};
|
||||
}, [fetchData]);
|
||||
|
||||
// Process data for Plotly charts
|
||||
const preparePlotlyData = () => {
|
||||
if (!data || data.length === 0) return { powerData: [], tempData: [] };
|
||||
|
||||
const currentTimestamps = data.map(item => new Date(item.now_timestamp));
|
||||
const futureTimestamps = data.map(item => new Date(item.future_timestamp));
|
||||
const currentPower = data.map(item => parseFloat(item.power));
|
||||
const predictedPower = data.map(item => parseFloat(item.power_future_min));
|
||||
const currentTemp = data.map(item => parseFloat(item.env_temp_cur));
|
||||
const predictedTemp = data.map(item => parseFloat(item.env_temp_min));
|
||||
|
||||
// Calculate min and max values for range sliders
|
||||
const powerMin = Math.min(...currentPower, ...predictedPower);
|
||||
const powerMax = Math.max(...currentPower, ...predictedPower);
|
||||
const tempMin = Math.min(...currentTemp, ...predictedTemp);
|
||||
const tempMax = Math.max(...currentTemp, ...predictedTemp);
|
||||
|
||||
const powerData: Partial<PlotData>[] = [
|
||||
{
|
||||
x: currentTimestamps,
|
||||
y: currentPower,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
name: 'Current Power',
|
||||
line: { color: theme.palette.primary.main, width: 2 },
|
||||
marker: { size: 6 }
|
||||
},
|
||||
{
|
||||
x: futureTimestamps,
|
||||
y: predictedPower,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
name: 'Predicted Power',
|
||||
line: { color: theme.palette.success.main, width: 2, dash: 'dash' },
|
||||
marker: { size: 6 }
|
||||
},
|
||||
// Range slider trace
|
||||
{
|
||||
x: [...currentTimestamps, ...futureTimestamps],
|
||||
y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5),
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Y Range',
|
||||
yaxis: 'y2',
|
||||
line: { color: 'transparent' },
|
||||
showlegend: false,
|
||||
hoverinfo: 'skip' as const
|
||||
}
|
||||
];
|
||||
|
||||
const tempData: Partial<PlotData>[] = [
|
||||
{
|
||||
x: currentTimestamps,
|
||||
y: currentTemp,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
name: 'Current Temperature',
|
||||
line: { color: theme.palette.primary.main, width: 2 },
|
||||
marker: { size: 6 }
|
||||
},
|
||||
{
|
||||
x: futureTimestamps,
|
||||
y: predictedTemp,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
name: 'Predicted Temperature',
|
||||
line: { color: theme.palette.success.main, width: 2, dash: 'dash' },
|
||||
marker: { size: 6 }
|
||||
},
|
||||
// Range slider trace
|
||||
{
|
||||
x: [...currentTimestamps, ...futureTimestamps],
|
||||
y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5),
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Y Range',
|
||||
yaxis: 'y2',
|
||||
line: { color: 'transparent' },
|
||||
showlegend: false,
|
||||
hoverinfo: 'skip' as const
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
powerData,
|
||||
tempData,
|
||||
ranges: {
|
||||
power: { min: powerMin, max: powerMax },
|
||||
temp: { min: tempMin, max: tempMax }
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const { powerData, tempData, ranges } = preparePlotlyData();
|
||||
|
||||
// Common layout settings for both charts
|
||||
const commonLayoutSettings: Partial<Layout> = {
|
||||
showlegend: true,
|
||||
legend: {
|
||||
orientation: 'h',
|
||||
y: -0.2,
|
||||
x: 0.5,
|
||||
xanchor: 'center',
|
||||
yanchor: 'top',
|
||||
font: {
|
||||
size: 12,
|
||||
family: theme.typography.fontFamily,
|
||||
color: theme.palette.text.secondary
|
||||
},
|
||||
bgcolor: 'rgba(255, 255, 255, 0)',
|
||||
bordercolor: 'rgba(255, 255, 255, 0)'
|
||||
},
|
||||
margin: { t: 60, b: 100, l: 60, r: 60 }, // Increased right margin for Y-axis range slider
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
hovermode: 'closest',
|
||||
xaxis: {
|
||||
type: 'date',
|
||||
gridcolor: theme.palette.divider,
|
||||
tickfont: { size: 12, color: theme.palette.text.secondary },
|
||||
showgrid: true,
|
||||
rangeslider: { visible: true }
|
||||
},
|
||||
yaxis2: {
|
||||
overlaying: 'y',
|
||||
side: 'right',
|
||||
showgrid: false,
|
||||
zeroline: false,
|
||||
showticklabels: false,
|
||||
range: [0, 1],
|
||||
rangeslider: {
|
||||
visible: true,
|
||||
thickness: 0.1,
|
||||
bgcolor: 'rgba(0,0,0,0)',
|
||||
bordercolor: theme.palette.divider
|
||||
}
|
||||
// Process data for charts
|
||||
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;
|
||||
}
|
||||
|
||||
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),
|
||||
currentTemp: parseFloat(item.env_temp_cur),
|
||||
predictedTemp: parseFloat(item.env_temp_min),
|
||||
}));
|
||||
};
|
||||
|
||||
// Add handlers for zoom events
|
||||
const handlePowerZoom = (event: any) => {
|
||||
if (event['xaxis.range[0]']) {
|
||||
setPowerZoom({
|
||||
xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()],
|
||||
yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']]
|
||||
});
|
||||
}
|
||||
};
|
||||
const chartData = prepareChartData();
|
||||
|
||||
const handleTempZoom = (event: any) => {
|
||||
if (event['xaxis.range[0]']) {
|
||||
setTempZoom({
|
||||
xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()],
|
||||
yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']]
|
||||
});
|
||||
}
|
||||
};
|
||||
// Extract data arrays for charts
|
||||
const currentTimestamps = chartData.map(item => item.currentTimestamp);
|
||||
const futureTimestamps = chartData.map(item => item.futureTimestamp);
|
||||
const currentPowerData = chartData.map(item => item.currentPower);
|
||||
const predictedPowerData = chartData.map(item => item.predictedPower);
|
||||
const currentTempData = chartData.map(item => item.currentTemp);
|
||||
const predictedTempData = chartData.map(item => item.predictedTemp);
|
||||
|
||||
// Modify the power layout to use preserved zoom
|
||||
const powerLayout: Partial<Layout> = {
|
||||
...commonLayoutSettings,
|
||||
yaxis: {
|
||||
title: 'Power (W)',
|
||||
gridcolor: theme.palette.divider,
|
||||
tickfont: { size: 12, color: theme.palette.text.secondary },
|
||||
titlefont: { size: 14, color: theme.palette.text.primary },
|
||||
showgrid: true,
|
||||
rangemode: 'tozero',
|
||||
fixedrange: false,
|
||||
range: powerZoom.yRange || (ranges ? [ranges.power.min * 0.9, ranges.power.max * 1.1] : undefined)
|
||||
},
|
||||
xaxis: {
|
||||
...commonLayoutSettings.xaxis,
|
||||
range: powerZoom.xRange ? [new Date(powerZoom.xRange[0]), new Date(powerZoom.xRange[1])] : undefined
|
||||
}
|
||||
};
|
||||
|
||||
// Modify the temperature layout to use preserved zoom
|
||||
const tempLayout: Partial<Layout> = {
|
||||
...commonLayoutSettings,
|
||||
yaxis: {
|
||||
title: 'Temperature (°C)',
|
||||
gridcolor: theme.palette.divider,
|
||||
tickfont: { size: 12, color: theme.palette.text.secondary },
|
||||
titlefont: { size: 14, color: theme.palette.text.primary },
|
||||
showgrid: true,
|
||||
rangemode: 'tozero',
|
||||
fixedrange: false,
|
||||
range: tempZoom.yRange || (ranges ? [ranges.temp.min * 0.9, ranges.temp.max * 1.1] : undefined)
|
||||
},
|
||||
xaxis: {
|
||||
...commonLayoutSettings.xaxis,
|
||||
range: tempZoom.xRange ? [new Date(tempZoom.xRange[0]), new Date(tempZoom.xRange[1])] : undefined
|
||||
}
|
||||
};
|
||||
|
||||
// Common Plotly config with additional modebar buttons
|
||||
const plotConfig: Partial<Config> = {
|
||||
responsive: true,
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'] as ('lasso2d' | 'select2d')[],
|
||||
toImageButtonOptions: {
|
||||
format: 'png' as const,
|
||||
filename: 'temperature_monitoring',
|
||||
height: 1000,
|
||||
width: 1500,
|
||||
scale: 2
|
||||
}
|
||||
};
|
||||
// Debug logging
|
||||
console.log('Chart data:', chartData);
|
||||
console.log('Raw data length:', data.length);
|
||||
|
||||
// Handle temperature decision
|
||||
const handleTemperatureDecision = async (approval: boolean) => {
|
||||
@@ -378,7 +196,7 @@ const Temperature = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
|
||||
<AppBar
|
||||
<AppBar
|
||||
position="static"
|
||||
elevation={0}
|
||||
sx={{
|
||||
@@ -545,18 +363,46 @@ const Temperature = () => {
|
||||
Loading power data...
|
||||
</Typography>
|
||||
</Box>
|
||||
) : data.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
|
||||
<Typography variant="h6" color="text.secondary">No power data available</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ height: 400 }}>
|
||||
<Plot
|
||||
data={powerData}
|
||||
layout={powerLayout}
|
||||
config={plotConfig}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onRelayout={handlePowerZoom}
|
||||
<LineChart
|
||||
height={400}
|
||||
skipAnimation={false}
|
||||
series={[
|
||||
{
|
||||
data: predictedPowerData,
|
||||
label: 'Predicted Power (3min)',
|
||||
color: '#ff9800',
|
||||
showMark: false,
|
||||
curve: 'linear'
|
||||
},
|
||||
{
|
||||
data: currentPowerData,
|
||||
label: 'Current Power',
|
||||
color: '#028a4a', // B'GREEN brand color
|
||||
showMark: false
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'time',
|
||||
data: [...currentTimestamps, ...futureTimestamps],
|
||||
valueFormatter: (value: Date) => value.toLocaleTimeString(),
|
||||
},
|
||||
]}
|
||||
yAxis={[
|
||||
{
|
||||
width: 50,
|
||||
valueFormatter: (value: number) => `${value} W`,
|
||||
}
|
||||
]}
|
||||
margin={{ right: 24 }}
|
||||
slotProps={{
|
||||
legend: {
|
||||
direction: 'horizontal',
|
||||
position: { vertical: 'top', horizontal: 'center' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -626,18 +472,45 @@ const Temperature = () => {
|
||||
Loading temperature data...
|
||||
</Typography>
|
||||
</Box>
|
||||
) : data.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
|
||||
<Typography variant="h6" color="text.secondary">No temperature data available</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ height: 400 }}>
|
||||
<Plot
|
||||
data={tempData}
|
||||
layout={tempLayout}
|
||||
config={plotConfig}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onRelayout={handleTempZoom}
|
||||
<LineChart
|
||||
height={400}
|
||||
skipAnimation={false}
|
||||
series={[
|
||||
{
|
||||
data: predictedTempData,
|
||||
label: 'Predicted Temperature (3min)',
|
||||
color: '#ff9800',
|
||||
showMark: false
|
||||
},
|
||||
{
|
||||
data: currentTempData,
|
||||
label: 'Current Temperature',
|
||||
color: '#028a4a', // B'GREEN brand color
|
||||
showMark: false
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'time',
|
||||
data: [...currentTimestamps, ...futureTimestamps],
|
||||
valueFormatter: (value: Date) => value.toLocaleTimeString(),
|
||||
},
|
||||
]}
|
||||
yAxis={[
|
||||
{
|
||||
width: 50,
|
||||
valueFormatter: (value: number) => `${value} °C`,
|
||||
}
|
||||
]}
|
||||
margin={{ right: 24 }}
|
||||
slotProps={{
|
||||
legend: {
|
||||
direction: 'horizontal',
|
||||
position: { vertical: 'top', horizontal: 'center' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
25
src/plotly.d.ts
vendored
25
src/plotly.d.ts
vendored
@@ -1,25 +0,0 @@
|
||||
declare module 'react-plotly.js' {
|
||||
import * as Plotly from 'plotly.js';
|
||||
import * as React from 'react';
|
||||
|
||||
interface PlotParams {
|
||||
data: Plotly.Data[];
|
||||
layout?: Partial<Plotly.Layout>;
|
||||
frames?: Plotly.Frame[];
|
||||
config?: Partial<Plotly.Config>;
|
||||
onClick?: (event: Plotly.PlotMouseEvent) => void;
|
||||
onHover?: (event: Plotly.PlotMouseEvent) => void;
|
||||
onUnHover?: (event: Plotly.PlotMouseEvent) => void;
|
||||
onSelected?: (event: Plotly.PlotSelectionEvent) => void;
|
||||
onDeselect?: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
debug?: boolean;
|
||||
useResizeHandler?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
class Plot extends React.Component<PlotParams> {}
|
||||
export default Plot;
|
||||
}
|
||||
Reference in New Issue
Block a user