Compare commits

...

1 Commits

7 changed files with 1165 additions and 4 deletions

View File

@ -8170,8 +8170,15 @@
"trigger_variable": null,
"auto_started": true
}
},
{
"timestamp": "2025-08-16T00:53:19.221218",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
}
],
"last_updated": "2025-08-15T23:05:23.049195",
"total_entries": 678
"last_updated": "2025-08-16T00:53:19.221218",
"total_entries": 679
}

View File

@ -0,0 +1,430 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Box, Text, Switch, FormLabel, HStack, useColorModeValue } from '@chakra-ui/react';
// Historical Chart.js Plot Component with Zoom and Pan
const ChartjsHistoricalPlot = ({ session, height = '400px' }) => {
const canvasRef = useRef(null);
const chartRef = useRef(null);
const sessionDataRef = useRef({
sessionId: null,
currentTimeRange: { start: null, end: null },
pendingDataRequest: false
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [dataPointsCount, setDataPointsCount] = useState(0);
const [isZoomEnabled, setIsZoomEnabled] = useState(true);
const [currentRange, setCurrentRange] = useState('');
const bgColor = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.600', 'gray.300');
// Check if Chart.js is available
const checkChartDependencies = useCallback(() => {
try {
if (typeof window === 'undefined' || !window.Chart) {
console.error('❌ Chart.js not loaded');
return false;
}
return true;
} catch (error) {
console.error('❌ Chart.js dependency check failed:', error);
return false;
}
}, []);
// Destroy chart helper
const destroyChart = useCallback(() => {
if (chartRef.current) {
try {
chartRef.current.destroy();
} catch (error) {
console.warn('Chart destroy error (non-critical):', error);
}
chartRef.current = null;
}
}, []);
// Create historical chart
const createChart = useCallback(() => {
if (!canvasRef.current || !checkChartDependencies()) {
setError('Chart.js not available');
return;
}
destroyChart();
const ctx = canvasRef.current.getContext('2d');
if (!ctx) {
setError('Cannot get canvas context');
return;
}
try {
// Initialize empty datasets
const datasets = (session.config.variables || []).map((variable, index) => ({
label: variable.name || variable,
data: [],
borderColor: `hsl(${(index * 137.5) % 360}, 70%, 50%)`,
backgroundColor: `hsla(${(index * 137.5) % 360}, 70%, 50%, 0.1)`,
borderWidth: 2,
fill: session.config.stacked || false,
tension: session.config.line_tension || 0.4,
pointRadius: session.config.point_radius || 1,
pointHoverRadius: session.config.point_hover_radius || 4,
stepped: session.config.stepped || false
}));
const config = {
type: 'line',
data: { datasets },
options: {
responsive: true,
maintainAspectRatio: false,
animation: false, // Disable animations for better performance
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
padding: 10
}
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: function(context) {
if (context[0] && context[0].parsed && context[0].parsed.x) {
return new Date(context[0].parsed.x).toLocaleString();
}
return '';
}
}
},
zoom: isZoomEnabled && window.Chart?.registry?.plugins?.get?.('zoom') ? {
pan: {
enabled: true,
mode: 'x',
onPanComplete: handlePanComplete
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true,
},
mode: 'x',
onZoomComplete: handleZoomComplete
}
} : {}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'MM/DD HH:mm',
day: 'MM/DD',
week: 'MM/DD',
month: 'MM/YY',
quarter: 'MM/YY',
year: 'YYYY'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'Value'
},
min: session.config.y_min,
max: session.config.y_max
}
}
}
};
chartRef.current = new window.Chart(ctx, config);
// Register chart controls
if (session.onChartReady) {
session.onChartReady({
loadHistoricalRange: loadHistoricalRange,
refreshData: refreshCurrentRange,
getTimeRange: getCurrentTimeRange,
resetZoom: resetZoom
});
}
console.log(`📈 Historical chart created for session ${session.session_id}`);
setError(null);
} catch (error) {
console.error('Error creating historical chart:', error);
setError(`Chart creation failed: ${error.message}`);
}
}, [session, isZoomEnabled]);
// Handle zoom completion
const handleZoomComplete = useCallback(({ chart }) => {
const xScale = chart.scales.x;
const startTime = xScale.min;
const endTime = xScale.max;
updateCurrentRange(startTime, endTime);
requestAdditionalData(startTime, endTime);
}, []);
// Handle pan completion
const handlePanComplete = useCallback(({ chart }) => {
const xScale = chart.scales.x;
const startTime = xScale.min;
const endTime = xScale.max;
updateCurrentRange(startTime, endTime);
requestAdditionalData(startTime, endTime);
}, []);
// Update current range display
const updateCurrentRange = (startTime, endTime) => {
const start = new Date(startTime);
const end = new Date(endTime);
const range = `${start.toLocaleString()} - ${end.toLocaleString()}`;
setCurrentRange(range);
// Update session time range if callback available
if (session.onTimeRangeChange) {
session.onTimeRangeChange(startTime, endTime);
}
};
// Request additional data for the visible range
const requestAdditionalData = useCallback(async (startTime, endTime) => {
if (!session.onDataRequest || sessionDataRef.current.pendingDataRequest) {
return;
}
try {
sessionDataRef.current.pendingDataRequest = true;
setIsLoading(true);
const data = await session.onDataRequest(startTime, endTime);
if (data && chartRef.current) {
updateChartData(data);
}
} catch (error) {
console.error('Error requesting historical data:', error);
} finally {
sessionDataRef.current.pendingDataRequest = false;
setIsLoading(false);
}
}, [session]);
// Load specific time range
const loadHistoricalRange = useCallback(async (startTime, endTime) => {
if (!session.onDataRequest || !chartRef.current) {
return;
}
try {
setIsLoading(true);
setError(null);
const data = await session.onDataRequest(startTime, endTime);
if (data) {
// Clear existing data
chartRef.current.data.datasets.forEach(dataset => {
dataset.data = [];
});
updateChartData(data);
// Set chart view to the requested range
if (chartRef.current.scales?.x) {
chartRef.current.scales.x.options.min = startTime;
chartRef.current.scales.x.options.max = endTime;
}
chartRef.current.update('none');
updateCurrentRange(startTime, endTime);
}
} catch (error) {
console.error('Error loading historical range:', error);
setError(`Failed to load data: ${error.message}`);
} finally {
setIsLoading(false);
}
}, [session]);
// Update chart with new data from the optimized API response
const updateChartData = useCallback((response) => {
if (!chartRef.current || !response) return;
let totalPoints = 0;
try {
// Use the optimized chart_data format from the API
const chartData = response.chart_data || {};
chartRef.current.data.datasets.forEach((dataset, index) => {
const variableName = session.config.variables[index]?.name || session.config.variables[index];
const variableData = chartData[variableName];
if (variableData && Array.isArray(variableData)) {
// Data is already in {x, y} format for Chart.js
const newPoints = variableData.map(point => ({
x: new Date(point.x).getTime(),
y: point.y
}));
// Merge with existing data and remove duplicates
const allPoints = [...dataset.data, ...newPoints];
const uniquePoints = allPoints.filter((point, index, arr) =>
arr.findIndex(p => p.x === point.x) === index
);
// Sort by timestamp
dataset.data = uniquePoints.sort((a, b) => a.x - b.x);
totalPoints += dataset.data.length;
}
});
chartRef.current.update('none');
setDataPointsCount(totalPoints);
} catch (error) {
console.error('Error updating chart data:', error);
}
}, [session]);
// Refresh current visible range
const refreshCurrentRange = useCallback(() => {
if (!chartRef.current?.scales?.x) return;
const xScale = chartRef.current.scales.x;
const startTime = xScale.min;
const endTime = xScale.max;
if (startTime && endTime) {
loadHistoricalRange(startTime, endTime);
}
}, [loadHistoricalRange]);
// Get current time range
const getCurrentTimeRange = useCallback(() => {
if (!chartRef.current?.scales?.x) return null;
const xScale = chartRef.current.scales.x;
return {
start: xScale.min,
end: xScale.max
};
}, []);
// Reset zoom to show all data
const resetZoom = useCallback(() => {
if (chartRef.current && chartRef.current.resetZoom) {
chartRef.current.resetZoom();
}
}, []);
// Initialize chart
useEffect(() => {
createChart();
return destroyChart;
}, [createChart, destroyChart]);
// Handle session changes
useEffect(() => {
sessionDataRef.current.sessionId = session.session_id;
}, [session.session_id]);
// Load initial data when component mounts
useEffect(() => {
if (session.timeRange && session.timeRange.startDate && session.timeRange.endDate) {
const startDateTime = new Date(`${session.timeRange.startDate}T${session.timeRange.startTime || '00:00'}:00`);
const endDateTime = new Date(`${session.timeRange.endDate}T${session.timeRange.endTime || '23:59'}:00`);
loadHistoricalRange(startDateTime.getTime(), endDateTime.getTime());
}
}, [session.timeRange, loadHistoricalRange]);
if (error) {
return (
<Box
height={height}
bg={bgColor}
display="flex"
alignItems="center"
justifyContent="center"
borderRadius="md"
border="1px solid"
borderColor="red.200"
>
<Text color="red.500" textAlign="center">
{error}
</Text>
</Box>
);
}
return (
<Box height={height} bg={bgColor} borderRadius="md" position="relative">
{/* Controls */}
<HStack spacing={4} mb={2} fontSize="sm">
<FormLabel mb={0} fontSize="xs">
<Switch
size="sm"
isChecked={isZoomEnabled}
onChange={(e) => setIsZoomEnabled(e.target.checked)}
mr={2}
/>
Zoom/Pan
</FormLabel>
{dataPointsCount > 0 && (
<Text color={textColor} fontSize="xs">
{dataPointsCount.toLocaleString()} data points
</Text>
)}
{isLoading && (
<Text color="blue.500" fontSize="xs">
Loading...
</Text>
)}
</HStack>
{/* Current range display */}
{currentRange && (
<Text fontSize="xs" color={textColor} mb={2}>
Viewing: {currentRange}
</Text>
)}
{/* Chart canvas */}
<Box height="calc(100% - 60px)">
<canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
</Box>
</Box>
);
};
export default ChartjsHistoricalPlot;

View File

@ -0,0 +1,325 @@
import React, { useState, useEffect } from 'react'
import {
Box,
VStack,
HStack,
Text,
Button,
Card,
CardBody,
CardHeader,
Heading,
useColorModeValue,
Badge,
Divider,
Grid,
GridItem,
Alert,
AlertIcon,
useToast,
Spinner,
Center
} from '@chakra-ui/react'
import { TimeIcon, AddIcon } from '@chakra-ui/icons'
import PlotHistoricalSession from './PlotHistoricalSession'
import * as api from '../services/api'
/**
* PlotHistoricalManager - Manages historical plot sessions
* Allows users to select plot definitions and create historical analysis sessions
*/
export default function PlotHistoricalManager() {
const [plotDefinitions, setPlotDefinitions] = useState([])
const [plotVariables, setPlotVariables] = useState([])
const [activeSessions, setActiveSessions] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const toast = useToast()
const cardBg = useColorModeValue('white', 'gray.800')
const borderColor = useColorModeValue('gray.200', 'gray.600')
// Load plot definitions and variables on mount
useEffect(() => {
loadPlotConfigurations()
}, [])
const loadPlotConfigurations = async () => {
try {
setLoading(true)
setError(null)
// Load plot definitions
const plotDefsResponse = await api.getConfig('plot-definitions')
const plotDefs = plotDefsResponse.data?.plots || []
// Load plot variables
const plotVarsResponse = await api.getConfig('plot-variables')
const plotVars = plotVarsResponse.data?.plot_variables || []
setPlotDefinitions(plotDefs)
setPlotVariables(plotVars)
} catch (err) {
console.error('Error loading plot configurations:', err)
setError('Failed to load plot configurations')
toast({
title: 'Error Loading Plots',
description: 'Failed to load plot definitions and variables',
status: 'error',
duration: 5000,
isClosable: true
})
} finally {
setLoading(false)
}
}
// Get variables for a specific plot
const getVariablesForPlot = (plotId) => {
const plotVarConfig = plotVariables.find(pv => pv.plot_id === plotId)
return plotVarConfig?.variables || []
}
// Create a new historical session
const createSession = (plotDefinition) => {
const sessionId = `historical_${plotDefinition.id}_${Date.now()}`
const variables = getVariablesForPlot(plotDefinition.id)
if (variables.length === 0) {
toast({
title: 'No Variables Configured',
description: `Plot "${plotDefinition.name}" has no variables configured`,
status: 'warning',
duration: 5000,
isClosable: true
})
return
}
const newSession = {
id: sessionId,
plotDefinition,
variables,
createdAt: new Date()
}
setActiveSessions(prev => [...prev, newSession])
toast({
title: 'Historical Session Created',
description: `Created session for "${plotDefinition.name}"`,
status: 'success',
duration: 3000,
isClosable: true
})
}
// Remove a session
const removeSession = (sessionId) => {
setActiveSessions(prev => prev.filter(session => session.id !== sessionId))
toast({
title: 'Session Removed',
description: 'Historical plot session removed',
status: 'info',
duration: 3000,
isClosable: true
})
}
if (loading) {
return (
<Center py={10}>
<VStack spacing={4}>
<Spinner size="lg" />
<Text>Loading plot configurations...</Text>
</VStack>
</Center>
)
}
if (error) {
return (
<Alert status="error" borderRadius="md">
<AlertIcon />
<VStack align="start" spacing={2}>
<Text fontWeight="medium">Configuration Error</Text>
<Text fontSize="sm">{error}</Text>
<Button size="sm" onClick={loadPlotConfigurations}>
Retry Loading
</Button>
</VStack>
</Alert>
)
}
return (
<VStack spacing={6} align="stretch">
{/* Header */}
<Card>
<CardHeader>
<VStack spacing={3} align="start">
<HStack>
<TimeIcon boxSize={6} color="blue.500" />
<Heading size="lg">Historical Plot Analysis</Heading>
</HStack>
<Text color="gray.600" fontSize="md">
Create historical analysis sessions from CSV data using existing plot definitions
</Text>
<Divider />
<HStack spacing={4}>
<Badge colorScheme="blue" variant="subtle">
{plotDefinitions.length} Plot{plotDefinitions.length !== 1 ? 's' : ''} Available
</Badge>
<Badge colorScheme="green" variant="subtle">
{activeSessions.length} Active Session{activeSessions.length !== 1 ? 's' : ''}
</Badge>
</HStack>
</VStack>
</CardHeader>
</Card>
{/* Plot Selection Grid */}
{plotDefinitions.length === 0 ? (
<Alert status="info" borderRadius="md">
<AlertIcon />
<VStack align="start" spacing={2}>
<Text fontWeight="medium">No Plot Definitions Found</Text>
<Text fontSize="sm">
Please configure plot definitions in the Configuration tab first.
</Text>
</VStack>
</Alert>
) : (
<Card>
<CardHeader>
<Heading size="md">Available Plot Definitions</Heading>
<Text fontSize="sm" color="gray.600" mt={1}>
Select a plot definition to create a historical analysis session
</Text>
</CardHeader>
<CardBody>
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}>
{plotDefinitions.map((plotDef) => {
const variables = getVariablesForPlot(plotDef.id)
const hasSession = activeSessions.some(s => s.plotDefinition.id === plotDef.id)
return (
<Card
key={plotDef.id}
variant="outline"
bg={cardBg}
borderColor={borderColor}
cursor="pointer"
transition="all 0.2s"
_hover={{
borderColor: 'blue.300',
transform: 'translateY(-2px)',
shadow: 'md'
}}
>
<CardHeader pb={2}>
<VStack align="start" spacing={2}>
<HStack justify="space-between" w="full">
<Text fontWeight="bold" fontSize="md">
{plotDef.name}
</Text>
<Badge
colorScheme={variables.length > 0 ? "green" : "gray"}
variant="subtle"
>
{variables.length} vars
</Badge>
</HStack>
{plotDef.description && (
<Text fontSize="sm" color="gray.600" noOfLines={2}>
{plotDef.description}
</Text>
)}
</VStack>
</CardHeader>
<CardBody pt={0}>
<VStack spacing={3} align="stretch">
{variables.length > 0 && (
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>
Variables:
</Text>
<HStack spacing={1} flexWrap="wrap">
{variables.slice(0, 3).map((variable, idx) => (
<Badge key={idx} size="sm" variant="outline">
{variable.name}
</Badge>
))}
{variables.length > 3 && (
<Badge size="sm" variant="outline" colorScheme="gray">
+{variables.length - 3} more
</Badge>
)}
</HStack>
</Box>
)}
<Button
size="sm"
colorScheme="blue"
variant={hasSession ? "outline" : "solid"}
leftIcon={<AddIcon />}
isDisabled={variables.length === 0}
onClick={() => createSession(plotDef)}
>
{hasSession ? 'Create Another' : 'Create Session'}
</Button>
{variables.length === 0 && (
<Text fontSize="xs" color="red.500">
No variables configured
</Text>
)}
</VStack>
</CardBody>
</Card>
)
})}
</Grid>
</CardBody>
</Card>
)}
{/* Active Sessions */}
{activeSessions.length > 0 && (
<VStack spacing={4} align="stretch">
<Divider />
<Heading size="md">Active Historical Sessions</Heading>
{activeSessions.map((session) => (
<PlotHistoricalSession
key={session.id}
plotDefinition={session.plotDefinition}
plotVariables={session.variables}
onRemove={() => removeSession(session.id)}
/>
))}
</VStack>
)}
{/* Empty State for Sessions */}
{activeSessions.length === 0 && plotDefinitions.length > 0 && (
<Card variant="outline" borderStyle="dashed">
<CardBody>
<VStack spacing={4} py={8}>
<TimeIcon boxSize={12} color="gray.400" />
<Text color="gray.500" fontSize="lg" fontWeight="medium">
No Active Historical Sessions
</Text>
<Text color="gray.400" fontSize="sm" textAlign="center">
Select a plot definition above to create your first historical analysis session
</Text>
</VStack>
</CardBody>
</Card>
)}
</VStack>
)
}

View File

@ -0,0 +1,370 @@
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'
import {
Box,
VStack,
HStack,
Text,
Button,
Card,
CardBody,
CardHeader,
Heading,
useColorModeValue,
Badge,
IconButton,
Divider,
Spacer,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
FormControl,
FormLabel,
Switch,
Grid,
GridItem,
Flex,
useToast,
Checkbox,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
useDisclosure,
Input,
Select,
Alert,
AlertIcon,
Spinner
} from '@chakra-ui/react'
import { SettingsIcon, ViewIcon, CloseIcon, TimeIcon } from '@chakra-ui/icons'
import ChartjsHistoricalPlot from './ChartjsHistoricalPlot.jsx'
import * as api from '../services/api'
/**
* PlotHistoricalSession - Individual historical Chart.js plot component
* Loads data from CSV files with time range selection and zoom/pan capabilities
*/
export default function PlotHistoricalSession({
plotDefinition,
plotVariables = [],
onRemove
}) {
const [showSettings, setShowSettings] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const { isOpen: isFullscreen, onOpen: openFullscreen, onClose: closeFullscreen } = useDisclosure()
// Historical-specific state
const [timeRange, setTimeRange] = useState({
startDate: '',
endDate: '',
startTime: '00:00',
endTime: '23:59'
})
const [availableDateRange, setAvailableDateRange] = useState({
minDate: '',
maxDate: ''
})
const [loadingData, setLoadingData] = useState(false)
const [localConfig, setLocalConfig] = useState({
time_window: plotDefinition.time_window || 3600, // seconds for historical view
y_min: plotDefinition.y_min,
y_max: plotDefinition.y_max,
// Visual style properties
line_tension: plotDefinition.line_tension !== undefined ? plotDefinition.line_tension : 0.4,
stepped: plotDefinition.stepped || false,
stacked: plotDefinition.stacked || false,
point_radius: plotDefinition.point_radius !== undefined ? plotDefinition.point_radius : 1,
point_hover_radius: plotDefinition.point_hover_radius !== undefined ? plotDefinition.point_hover_radius : 4
})
const chartControlsRef = useRef(null)
const toast = useToast()
const cardBg = useColorModeValue('white', 'gray.700')
const borderColor = useColorModeValue('gray.200', 'gray.600')
const muted = useColorModeValue('gray.600', 'gray.300')
const settingsBg = useColorModeValue('gray.50', 'gray.600')
// Enhanced session object for ChartjsHistoricalPlot
const enhancedSession = useMemo(() => ({
session_id: `historical_${plotDefinition.id}`,
plot_id: plotDefinition.id,
name: plotDefinition.name,
variables_count: plotVariables.length,
isFullscreen: isFullscreen,
isHistorical: true,
timeRange: timeRange,
config: {
...plotDefinition,
...localConfig,
variables: plotVariables
},
onChartReady: (controls) => {
chartControlsRef.current = controls
},
onTimeRangeChange: handleChartTimeRangeChange,
onDataRequest: handleDataRequest
}), [
plotDefinition,
plotVariables,
localConfig,
isFullscreen,
timeRange
])
// Initialize with last 24 hours
useEffect(() => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
setTimeRange({
startDate: yesterday.toISOString().split('T')[0],
endDate: now.toISOString().split('T')[0],
startTime: yesterday.toTimeString().substr(0, 5),
endTime: now.toTimeString().substr(0, 5)
})
// Set available range (last 30 days)
setAvailableDateRange({
minDate: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
maxDate: now.toISOString().split('T')[0]
})
}, [])
// Handle time range changes from chart zoom/pan
const handleChartTimeRangeChange = useCallback((newStartTime, newEndTime) => {
const startDate = new Date(newStartTime)
const endDate = new Date(newEndTime)
setTimeRange({
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
startTime: startDate.toTimeString().substr(0, 5),
endTime: endDate.toTimeString().substr(0, 5)
})
}, [])
// Handle data requests from chart (zoom/pan events)
const handleDataRequest = useCallback(async (startTime, endTime) => {
try {
const response = await api.getHistoricalDataOptimized({
variables: plotVariables.map(v => v.name),
start_time: new Date(startTime).toISOString(),
end_time: new Date(endTime).toISOString()
})
return response?.success ? response : null
} catch (error) {
console.error('Error loading historical data:', error)
return null
}
}, [plotVariables])
const handleTimeRangeSubmit = () => {
if (chartControlsRef.current?.loadHistoricalRange) {
const startDateTime = new Date(`${timeRange.startDate}T${timeRange.startTime}:00`)
const endDateTime = new Date(`${timeRange.endDate}T${timeRange.endTime}:00`)
chartControlsRef.current.loadHistoricalRange(
startDateTime.getTime(),
endDateTime.getTime()
)
}
}
const formatDateRange = () => {
if (!timeRange.startDate || !timeRange.endDate) return 'No range selected'
const start = new Date(`${timeRange.startDate}T${timeRange.startTime}`)
const end = new Date(`${timeRange.endDate}T${timeRange.endTime}`)
if (timeRange.startDate === timeRange.endDate) {
return `${timeRange.startDate} ${timeRange.startTime} - ${timeRange.endTime}`
}
return `${start.toLocaleDateString()} ${timeRange.startTime} - ${end.toLocaleDateString()} ${timeRange.endTime}`
}
const refreshData = () => {
if (chartControlsRef.current?.refreshData) {
chartControlsRef.current.refreshData()
}
}
return (
<>
<Card bg={cardBg} borderColor={borderColor} mb={4}>
<CardHeader pb={2}>
<Flex align="center">
<Box mr={3} fontSize="2xl">🕰</Box>
<VStack spacing={0} align="start" flex={1}>
<Heading size="md">{plotDefinition.name}</Heading>
<HStack spacing={4} fontSize="sm" color={muted}>
<Text>{plotVariables.length} variables</Text>
<Text>Historical Mode</Text>
<Text>{formatDateRange()}</Text>
</HStack>
</VStack>
<HStack spacing={2}>
<IconButton
aria-label="Settings"
icon={<SettingsIcon />}
size="sm"
variant={showSettings ? "solid" : "ghost"}
onClick={() => setShowSettings(!showSettings)}
/>
<IconButton
aria-label="Fullscreen"
icon={<ViewIcon />}
size="sm"
variant="ghost"
onClick={openFullscreen}
/>
<IconButton
aria-label="Remove"
icon={<CloseIcon />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={onRemove}
/>
</HStack>
</Flex>
</CardHeader>
<Collapse in={showSettings} animateOpacity>
<Box px={6} pb={4} bg={settingsBg} borderRadius="md" mx={6} mb={4}>
<VStack spacing={4} align="stretch">
<Text fontWeight="medium" fontSize="sm">Historical Data Controls</Text>
{/* Time Range Controls */}
<Grid templateColumns="1fr 1fr" gap={4}>
<FormControl>
<FormLabel fontSize="sm">Start Date</FormLabel>
<Input
type="date"
size="sm"
value={timeRange.startDate}
min={availableDateRange.minDate}
max={availableDateRange.maxDate}
onChange={(e) => setTimeRange(prev => ({...prev, startDate: e.target.value}))}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="sm">End Date</FormLabel>
<Input
type="date"
size="sm"
value={timeRange.endDate}
min={availableDateRange.minDate}
max={availableDateRange.maxDate}
onChange={(e) => setTimeRange(prev => ({...prev, endDate: e.target.value}))}
/>
</FormControl>
</Grid>
<Grid templateColumns="1fr 1fr" gap={4}>
<FormControl>
<FormLabel fontSize="sm">Start Time</FormLabel>
<Input
type="time"
size="sm"
value={timeRange.startTime}
onChange={(e) => setTimeRange(prev => ({...prev, startTime: e.target.value}))}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="sm">End Time</FormLabel>
<Input
type="time"
size="sm"
value={timeRange.endTime}
onChange={(e) => setTimeRange(prev => ({...prev, endTime: e.target.value}))}
/>
</FormControl>
</Grid>
<HStack spacing={2}>
<Button
size="sm"
colorScheme="blue"
onClick={handleTimeRangeSubmit}
isLoading={loadingData}
leftIcon={<TimeIcon />}
>
Load Time Range
</Button>
<Button
size="sm"
variant="outline"
onClick={refreshData}
isLoading={isRefreshing}
>
Refresh
</Button>
</HStack>
{availableDateRange.minDate && (
<Text fontSize="xs" color={muted}>
Available data: {availableDateRange.minDate} to {availableDateRange.maxDate}
</Text>
)}
</VStack>
</Box>
</Collapse>
<CardBody pt={0}>
{loadingData ? (
<Flex align="center" justify="center" py={8}>
<Spinner size="lg" mr={4} />
<VStack spacing={2} align="start">
<Text fontSize="lg" fontWeight="medium">Loading Historical Data...</Text>
<Text fontSize="sm" color={muted}>Scanning CSV files for available data</Text>
</VStack>
</Flex>
) : (
<Box height="400px">
<ChartjsHistoricalPlot session={enhancedSession} height="400px" />
</Box>
)}
</CardBody>
</Card>
{/* Fullscreen Modal */}
<Modal isOpen={isFullscreen} onClose={closeFullscreen} size="full">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Flex align="center">
<Box mr={3} fontSize="2xl">🕰</Box>
<VStack spacing={0} align="start">
<Heading size="lg">{plotDefinition.name}</Heading>
<Text fontSize="sm" color={muted}>Historical Analysis - {formatDateRange()}</Text>
</VStack>
</Flex>
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<Box height="calc(100vh - 120px)">
<ChartjsHistoricalPlot session={enhancedSession} height="100%" />
</Box>
</ModalBody>
</ModalContent>
</Modal>
</>
)
}

View File

@ -56,6 +56,7 @@ import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import PlotManager from '../components/PlotManager'
import PlotHistoricalManager from '../components/PlotHistoricalManager'
import allWidgets from '../components/widgets/AllWidgets'
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate'
import CsvFileBrowser from '../components/CsvFileBrowser'
@ -1453,8 +1454,9 @@ function DashboardContent() {
<Tab>🔧 Configuration</Tab>
<Tab>📊 Datasets</Tab>
<Tab>📈 Plotting</Tab>
<Tab><EFBFBD> CSV Files</Tab>
<Tab><EFBFBD>📋 Events</Tab>
<Tab>🕰 Historical Plots</Tab>
<Tab>📄 CSV Files</Tab>
<Tab>📋 Events</Tab>
</TabList>
<TabPanels>
@ -1480,6 +1482,11 @@ function DashboardContent() {
<PlotManager />
</TabPanel>
<TabPanel p={0} pt={4}>
{/* Historical Plot Management Section */}
<PlotHistoricalManager />
</TabPanel>
<TabPanel p={0} pt={4}>
{/* CSV File Browser Section */}
<CsvFileBrowser />

View File

@ -287,4 +287,26 @@ export async function openCsvInExcel(filePath) {
return toJsonOrThrow(res)
}
// Configuration aliases for compatibility
export async function getConfig(configId) {
return readConfig(configId)
}
// Enhanced historical data loading with time range support
export async function getHistoricalDataOptimized(variables, options = {}) {
const requestBody = {
variables: variables,
...options
}
const res = await fetch(`${BASE_URL}/api/plots/historical`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
})
return toJsonOrThrow(res)
}

View File