diff --git a/application_events.json b/application_events.json index 1cea627..44bdd26 100644 --- a/application_events.json +++ b/application_events.json @@ -8628,8 +8628,172 @@ "activated_datasets": 2, "total_datasets": 3 } + }, + { + "timestamp": "2025-08-16T16:13:19.529732", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-16T16:13:19.594211", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-16T16:13:19.607075", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 2, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-16T16:13:19.619074", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-16T16:14:53.763412", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-16T16:14:53.829817", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-16T16:14:53.839819", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 2, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-16T16:14:53.851597", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-16T16:20:52.922366", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-16T16:20:52.988919", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-16T16:20:52.999920", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 2, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-16T16:20:53.010921", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-16T16:41:27.411359", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-16T16:41:27.475416", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-16T16:41:27.485418", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 2, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-16T16:41:27.495418", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } } ], - "last_updated": "2025-08-16T16:01:24.342184", - "total_entries": 724 + "last_updated": "2025-08-16T16:41:27.495418", + "total_entries": 740 } \ No newline at end of file diff --git a/frontend/src/components/ChartjsHistoricalPlot.jsx b/frontend/src/components/ChartjsHistoricalPlot.jsx index 6648c50..93d93f4 100644 --- a/frontend/src/components/ChartjsHistoricalPlot.jsx +++ b/frontend/src/components/ChartjsHistoricalPlot.jsx @@ -1,5 +1,5 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'; -import { Box, Text, Switch, FormLabel, HStack, useColorModeValue, Alert, AlertIcon } from '@chakra-ui/react'; +import { Box, Text, Switch, FormLabel, HStack, VStack, useColorModeValue, Alert, AlertIcon } from '@chakra-ui/react'; // Global dependencies check - run once let dependenciesChecked = false; @@ -77,6 +77,15 @@ const ChartjsHistoricalPlot = ({ // Create/Update chart when data changes useEffect(() => { + console.log('📊 ChartjsHistoricalPlot useEffect triggered:', { + hasData: historicalData && historicalData.length > 0, + dataLength: historicalData?.length || 0, + chartExists: !!chartRef.current, + sessionId: session?.id, + configChanged: JSON.stringify(config), + zoomEnabled: isZoomEnabled + }); + if (!historicalData || historicalData.length === 0) { setDataPointsCount(0); return; @@ -94,6 +103,18 @@ const ChartjsHistoricalPlot = ({ setCurrentTimeRange(timeRange); }, [timeRange]); + // Cleanup chart on unmount + useEffect(() => { + console.log('📊 ChartjsHistoricalPlot mounted'); + return () => { + console.log('📊 ChartjsHistoricalPlot unmounting - destroying chart'); + if (chartRef.current) { + chartRef.current.destroy(); + chartRef.current = null; + } + }; + }, []); + // Clean up chart on unmount useEffect(() => { return () => { @@ -115,58 +136,93 @@ const ChartjsHistoricalPlot = ({ } try { - // Destroy existing chart - if (chartRef.current) { - chartRef.current.destroy(); - chartRef.current = null; - } - + console.log(`📊 createOrUpdateChart called - Chart exists? ${!!chartRef.current}`); console.log(`📊 Processing ${historicalData.length} historical data points for variables:`, session?.variables); // Process historical data into Chart.js format const processedData = processHistoricalData(historicalData, session?.variables || []); setDataPointsCount(historicalData.length); - if (processedData.datasets.length === 0) { - console.log('📊 No datasets created from data'); - setError('No valid data to display'); - return; + // Siempre crear el gráfico, incluso si no hay datasets visibles + // Esto permite al usuario hacer pan/zoom para encontrar datos + if (processedData.datasets.length === 0 && historicalData.length === 0) { + console.log('📊 No data available - creating empty chart for navigation'); + // Crear datasets vacíos para todas las variables + const emptyDatasets = (session?.variables || []).map((variable, index) => ({ + label: variable, + data: [], + borderColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4], + backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4] + '20', + borderWidth: 2, + fill: false + })); + processedData.datasets = emptyDatasets; + } else if (processedData.datasets.length === 0 && historicalData.length > 0) { + console.log('📊 Data available but not visible in current range - allowing navigation'); + // Los datos existen pero pueden estar fuera del rango visible + // Crear datasets para permitir navegación + const emptyDatasets = (session?.variables || []).map((variable, index) => ({ + label: variable, + data: [], + borderColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4], + backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4] + '20', + borderWidth: 2, + fill: false + })); + processedData.datasets = emptyDatasets; } - console.log(`📊 Created ${processedData.datasets.length} datasets:`, processedData.datasets.map(d => ({ label: d.label, points: d.data.length }))); - - // Log sample data for debugging - processedData.datasets.forEach((dataset, index) => { - if (dataset.data.length > 0) { - console.log(`📊 Dataset ${index} (${dataset.label}):`, { - totalPoints: dataset.data.length, - firstPoint: dataset.data[0], - lastPoint: dataset.data[dataset.data.length - 1], - samplePoints: dataset.data.slice(0, 3) + // Check if chart exists + if (chartRef.current) { + // UPDATE existing chart data (faster, preserves zoom/pan) + console.log(`📊 Updating existing chart with ${processedData.datasets.length} datasets`); + + chartRef.current.data.datasets = processedData.datasets; + + // Update time range for axis if we have time range data + const minTime = currentTimeRange?.start ? new Date(currentTimeRange.start) : null; + const maxTime = currentTimeRange?.end ? new Date(currentTimeRange.end) : null; + + if (minTime && maxTime && chartRef.current.options.scales?.x) { + console.log('📊 Updating chart time range:', { + minTime: minTime.toISOString(), + maxTime: maxTime.toISOString() }); + chartRef.current.options.scales.x.min = minTime; + chartRef.current.options.scales.x.max = maxTime; } - }); + + // Update without animation for better performance + chartRef.current.update('none'); + + console.log(`📊 Chart data updated (preserving zoom/pan state)`); + } else { + // CREATE new chart (first time only) + console.log(`📊 Creating new chart with ${processedData.datasets.length} datasets`); - // Create chart configuration - const chartConfig = createChartConfig(processedData, config, isZoomEnabled); - - console.log('📊 Chart config created:', { - datasetCount: chartConfig.data.datasets.length, - hasData: chartConfig.data.datasets.some(d => d.data.length > 0), - decimationEnabled: chartConfig.options.plugins.decimation?.enabled - }); + // Create chart configuration + const chartConfig = createChartConfig(processedData, config, isZoomEnabled); + + console.log('📊 Chart config created:', { + datasetCount: chartConfig.data.datasets.length, + hasData: chartConfig.data.datasets.some(d => d.data.length > 0), + decimationEnabled: chartConfig.options.plugins.decimation?.enabled + }); - // Create new chart - const ctx = canvasRef.current.getContext('2d'); - chartRef.current = new window.Chart(ctx, chartConfig); + // Create new chart + const ctx = canvasRef.current.getContext('2d'); + chartRef.current = new window.Chart(ctx, chartConfig); + console.log('📊 Chart reference created:', !!chartRef.current); - // Setup zoom/pan event listeners - if (isZoomEnabled && chartRef.current) { - setupZoomPanEvents(chartRef.current); + // Setup zoom/pan event listeners + if (isZoomEnabled && chartRef.current) { + setupZoomPanEvents(chartRef.current); + } + + console.log(`📊 Historical chart created for session ${sessionDataRef.current.sessionId} with ${processedData.datasets.length} datasets`); } setError(null); - console.log(`📊 Historical chart created for session ${sessionDataRef.current.sessionId} with ${processedData.datasets.length} datasets`); } catch (error) { console.error('Error creating historical chart:', error); @@ -408,7 +464,8 @@ const ChartjsHistoricalPlot = ({ ); } - if (!historicalData || historicalData.length === 0) { + // Solo mostrar mensaje de "no data" si nunca se han cargado datos y el chart no existe + if ((!historicalData || historicalData.length === 0) && !chartRef.current) { return ( - No historical data available + + No historical data available + + Use the time selector to navigate to data + + ); } @@ -460,6 +522,11 @@ const ChartjsHistoricalPlot = ({ (Min/Max decimation active) )} + {dataPointsCount === 0 && chartRef.current && ( + + No data in current view - use pan/zoom to navigate + + )} diff --git a/frontend/src/components/PlotHistoricalSession.jsx b/frontend/src/components/PlotHistoricalSession.jsx index 4c70f75..53271f8 100644 --- a/frontend/src/components/PlotHistoricalSession.jsx +++ b/frontend/src/components/PlotHistoricalSession.jsx @@ -140,7 +140,18 @@ export default function PlotHistoricalSession({ const [showDataPreview, setShowDataPreview] = useState(false) // Configuration state - const [config, setConfig] = useState(session.config || {}) + // Config state (estabilizado para evitar re-renders) + const [config, setConfig] = useState(() => session.config || {}) + + // Estabilizar config para evitar re-renders innecesarios + const stableConfig = useMemo(() => config, [JSON.stringify(config)]) + + // Estabilizar historicalData para evitar re-renders innecesarios + const stableHistoricalData = useMemo(() => historicalData, [ + historicalData?.length, + historicalData?.[0]?.timestamp, + historicalData?.[historicalData.length - 1]?.timestamp + ]) // Helper function to ensure valid Date objects const ensureValidDate = (dateValue, fallback) => { @@ -166,7 +177,7 @@ export default function PlotHistoricalSession({ // Load historical data when derived time range changes useEffect(() => { - if (dateRange) { // Only load after we have date range + if (dateRange && !isLoading) { // Only load after we have date range and not already loading loadHistoricalData() } }, [session.id, derivedTimeRange, dateRange]) @@ -245,6 +256,12 @@ export default function PlotHistoricalSession({ } try { + // Prevent concurrent loads + if (isLoading) { + console.log('📊 Skipping load - already loading') + return + } + setIsLoading(true) setError(null) setLoadingProgress(0) @@ -588,33 +605,45 @@ export default function PlotHistoricalSession({ )} - {!isLoading && !error && ( - - - - )} - - {isLoading && ( - - - - - Loading historical data... - - - {loadingProgress}% complete - - - - )} + {/* Chart container - siempre montado para evitar remounting */} + + + + {/* Loading overlay */} + {isLoading && ( + + + + + Loading historical data... + + + {loadingProgress}% complete + + + + )} + )} diff --git a/frontend/src/components/TimePointSelector.jsx b/frontend/src/components/TimePointSelector.jsx index 23939de..9b3cc54 100644 --- a/frontend/src/components/TimePointSelector.jsx +++ b/frontend/src/components/TimePointSelector.jsx @@ -1,5 +1,6 @@ import { useMemo, useState, useCallback, useRef, useEffect } from "react"; -import { Box, Flex, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, useColorModeValue } from "@chakra-ui/react"; +import { Box, Flex, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, Button, IconButton, useColorModeValue } from "@chakra-ui/react"; +import { CheckIcon } from "@chakra-ui/icons"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; @@ -19,25 +20,42 @@ export default function TimePointSelector({ const [minMs, maxMs] = useMemo(() => [minDate.getTime(), maxDate.getTime()], [minDate, maxDate]); const stepMs = useMemo(() => stepMinutes * 60 * 1000, [stepMinutes]); - // Estado único (Date) + // Estado único (Date) - este es el valor "oficial" const [value, setValue] = useState(() => { // clamp al rango por si initial cae fuera const t = initial.getTime(); return new Date(Math.min(Math.max(t, minMs), maxMs)); }); - // Sincronizar con prop initial cuando cambie externamente (pan/zoom) - useEffect(() => { - const t = initial.getTime(); - const clampedValue = new Date(Math.min(Math.max(t, minMs), maxMs)); - setValue(clampedValue); - }, [initial, minMs, maxMs]); - - const valueMs = value.getTime(); + // Estado temporal del slider (solo para UI, no dispara eventos) + const [sliderValue, setSliderValue] = useState(() => value.getTime()); + + // Estado para detectar si hay cambios pendientes + const [hasPendingChanges, setHasPendingChanges] = useState(false); // Cooldown para evitar múltiples solicitudes const cooldownRef = useRef(null); const lastCallbackValueRef = useRef(null); + const isExternalUpdateRef = useRef(false); // Flag para detectar actualizaciones externas + + // Sincronizar con prop initial cuando cambie externamente (pan/zoom) + useEffect(() => { + const t = initial.getTime(); + const clampedValue = new Date(Math.min(Math.max(t, minMs), maxMs)); + + // Marcar que es una actualización externa + isExternalUpdateRef.current = true; + setValue(clampedValue); + setSliderValue(clampedValue.getTime()); // Sincronizar slider también + setHasPendingChanges(false); // Reset pending changes + + // Reset flag después de un tick + setTimeout(() => { + isExternalUpdateRef.current = false; + }, 0); + }, [initial, minMs, maxMs]); + + const valueMs = value.getTime(); // Redondea al paso del slider const snapToStep = useCallback((ms) => { @@ -74,26 +92,42 @@ export default function TimePointSelector({ }; }, []); - // Cambio desde el DatePicker (sin cooldown, cambio directo) + // Función para aplicar los cambios pendientes del slider + const applySliderChanges = useCallback(() => { + if (!hasPendingChanges) return; + + const newValue = new Date(sliderValue); + setValue(newValue); + setHasPendingChanges(false); + + if (onTimeChange) { + console.log('📊 TimeSelector: Applying slider changes', newValue); + onTimeChange(newValue); + } + }, [sliderValue, hasPendingChanges, onTimeChange]); + + // Cambio desde el DatePicker (inmediato, actualiza ambos valores) const onPick = (d) => { - if (!d) return; + if (!d || isExternalUpdateRef.current) return; + const newValue = new Date(snapToStep(d.getTime())); setValue(newValue); + setSliderValue(newValue.getTime()); + setHasPendingChanges(false); - // DatePicker no necesita cooldown, es cambio directo + // DatePicker es cambio directo if (onTimeChange) { console.log('📊 TimeSelector: DatePicker change (immediate)', newValue); onTimeChange(newValue); } }; - // Cambio desde el Slider (con cooldown) + // Cambio desde el Slider (solo actualiza UI, no dispara eventos) const onSlide = (ms) => { - const newValue = new Date(ms); - setValue(newValue); + if (isExternalUpdateRef.current) return; - // Usar cooldown para el slider - debouncedOnTimeChange(newValue); + setSliderValue(ms); + setHasPendingChanges(Math.abs(ms - value.getTime()) > 1000); // Solo marcar si hay diferencia significativa }; return ( @@ -161,12 +195,25 @@ export default function TimePointSelector({ - Navegar con slider + + Navegar con slider + {hasPendingChanges && ( + + )} + @@ -175,16 +222,23 @@ export default function TimePointSelector({ - - {value.toLocaleString('es-ES', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false - })} - + + + {new Date(sliderValue).toLocaleString('es-ES', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false + })} + + {hasPendingChanges && ( + + Cambios pendientes + + )} + diff --git a/system_state.json b/system_state.json index 38a4cac..d46ed0e 100644 --- a/system_state.json +++ b/system_state.json @@ -3,12 +3,12 @@ "should_connect": true, "should_stream": false, "active_datasets": [ + "DAR", "Test", - "Fast", - "DAR" + "Fast" ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-16T16:01:29.169391", + "last_update": "2025-08-16T16:41:43.854840", "plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe" } \ No newline at end of file