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 && (
+ }
+ onClick={applySliderChanges}
+ >
+ Apply
+
+ )}
+
@@ -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