From 86b4add6abf349f69c33507c6dfb569b22ac8601 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 16 Aug 2025 01:01:10 +0200 Subject: [PATCH] feat: Implement historical plot management with session creation, data loading, and enhanced charting capabilities --- application_events.json | 11 +- .../src/components/ChartjsHistoricalPlot.jsx | 430 ++++++++++++++++++ .../src/components/PlotHistoricalManager.jsx | 325 +++++++++++++ .../src/components/PlotHistoricalSession.jsx | 370 +++++++++++++++ frontend/src/pages/Dashboard.jsx | 11 +- frontend/src/services/api.js | 22 + frontend/src/services/api_backup.js | 0 7 files changed, 1165 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/ChartjsHistoricalPlot.jsx create mode 100644 frontend/src/components/PlotHistoricalManager.jsx create mode 100644 frontend/src/components/PlotHistoricalSession.jsx create mode 100644 frontend/src/services/api_backup.js diff --git a/application_events.json b/application_events.json index 8e4fca0..427fe0c 100644 --- a/application_events.json +++ b/application_events.json @@ -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 } \ No newline at end of file diff --git a/frontend/src/components/ChartjsHistoricalPlot.jsx b/frontend/src/components/ChartjsHistoricalPlot.jsx new file mode 100644 index 0000000..1081c22 --- /dev/null +++ b/frontend/src/components/ChartjsHistoricalPlot.jsx @@ -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 ( + + + ❌ {error} + + + ); + } + + return ( + + {/* Controls */} + + + setIsZoomEnabled(e.target.checked)} + mr={2} + /> + Zoom/Pan + + + {dataPointsCount > 0 && ( + + {dataPointsCount.toLocaleString()} data points + + )} + + {isLoading && ( + + Loading... + + )} + + + {/* Current range display */} + {currentRange && ( + + Viewing: {currentRange} + + )} + + {/* Chart canvas */} + + + + + ); +}; + +export default ChartjsHistoricalPlot; \ No newline at end of file diff --git a/frontend/src/components/PlotHistoricalManager.jsx b/frontend/src/components/PlotHistoricalManager.jsx new file mode 100644 index 0000000..435b852 --- /dev/null +++ b/frontend/src/components/PlotHistoricalManager.jsx @@ -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 ( +
+ + + Loading plot configurations... + +
+ ) + } + + if (error) { + return ( + + + + Configuration Error + {error} + + + + ) + } + + return ( + + {/* Header */} + + + + + + Historical Plot Analysis + + + Create historical analysis sessions from CSV data using existing plot definitions + + + + + {plotDefinitions.length} Plot{plotDefinitions.length !== 1 ? 's' : ''} Available + + + {activeSessions.length} Active Session{activeSessions.length !== 1 ? 's' : ''} + + + + + + + {/* Plot Selection Grid */} + {plotDefinitions.length === 0 ? ( + + + + No Plot Definitions Found + + Please configure plot definitions in the Configuration tab first. + + + + ) : ( + + + Available Plot Definitions + + Select a plot definition to create a historical analysis session + + + + + {plotDefinitions.map((plotDef) => { + const variables = getVariablesForPlot(plotDef.id) + const hasSession = activeSessions.some(s => s.plotDefinition.id === plotDef.id) + + return ( + + + + + + {plotDef.name} + + 0 ? "green" : "gray"} + variant="subtle" + > + {variables.length} vars + + + {plotDef.description && ( + + {plotDef.description} + + )} + + + + + {variables.length > 0 && ( + + + Variables: + + + {variables.slice(0, 3).map((variable, idx) => ( + + {variable.name} + + ))} + {variables.length > 3 && ( + + +{variables.length - 3} more + + )} + + + )} + + + + {variables.length === 0 && ( + + No variables configured + + )} + + + + ) + })} + + + + )} + + {/* Active Sessions */} + {activeSessions.length > 0 && ( + + + Active Historical Sessions + + {activeSessions.map((session) => ( + removeSession(session.id)} + /> + ))} + + )} + + {/* Empty State for Sessions */} + {activeSessions.length === 0 && plotDefinitions.length > 0 && ( + + + + + + No Active Historical Sessions + + + Select a plot definition above to create your first historical analysis session + + + + + )} + + ) +} diff --git a/frontend/src/components/PlotHistoricalSession.jsx b/frontend/src/components/PlotHistoricalSession.jsx new file mode 100644 index 0000000..6daa92f --- /dev/null +++ b/frontend/src/components/PlotHistoricalSession.jsx @@ -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 ( + <> + + + + 🕰️ + + {plotDefinition.name} + + {plotVariables.length} variables + Historical Mode + {formatDateRange()} + + + + + } + size="sm" + variant={showSettings ? "solid" : "ghost"} + onClick={() => setShowSettings(!showSettings)} + /> + } + size="sm" + variant="ghost" + onClick={openFullscreen} + /> + } + size="sm" + variant="ghost" + colorScheme="red" + onClick={onRemove} + /> + + + + + + + + Historical Data Controls + + {/* Time Range Controls */} + + + Start Date + setTimeRange(prev => ({...prev, startDate: e.target.value}))} + /> + + + + End Date + setTimeRange(prev => ({...prev, endDate: e.target.value}))} + /> + + + + + + Start Time + setTimeRange(prev => ({...prev, startTime: e.target.value}))} + /> + + + + End Time + setTimeRange(prev => ({...prev, endTime: e.target.value}))} + /> + + + + + + + + + {availableDateRange.minDate && ( + + Available data: {availableDateRange.minDate} to {availableDateRange.maxDate} + + )} + + + + + + {loadingData ? ( + + + + Loading Historical Data... + Scanning CSV files for available data + + + ) : ( + + + + )} + + + + {/* Fullscreen Modal */} + + + + + + 🕰️ + + {plotDefinition.name} + Historical Analysis - {formatDateRange()} + + + + + + + + + + + + + ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index d678c76..a965113 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -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() { 🔧 Configuration 📊 Datasets 📈 Plotting - � CSV Files - �📋 Events + 🕰️ Historical Plots + 📄 CSV Files + 📋 Events @@ -1480,6 +1482,11 @@ function DashboardContent() { + + {/* Historical Plot Management Section */} + + + {/* CSV File Browser Section */} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 9d10859..fdb4949 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -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) +} diff --git a/frontend/src/services/api_backup.js b/frontend/src/services/api_backup.js new file mode 100644 index 0000000..e69de29