📋 Event Logging: Monitor system events, errors, and operational status
-🔍 Real-time Updates: Events are automatically updated as they occur
-📊 Filtering: Filter events by type and time range
-Loading events...
-diff --git a/CHARTJS_STREAMING_INTEGRATION.md b/CHARTJS_STREAMING_INTEGRATION.md new file mode 100644 index 0000000..30e3503 --- /dev/null +++ b/CHARTJS_STREAMING_INTEGRATION.md @@ -0,0 +1,198 @@ +# 📈 Chart.js Plugin Streaming - Integración + +## 🚀 Implementación Exitosa + +Se ha integrado exitosamente **chartjs-plugin-streaming** en la aplicación PLC S7-315 Streamer & Logger para mejorar significativamente el sistema de plotting en tiempo real. + +## 📋 ¿Qué se ha cambiado? + +### 1. **Archivos agregados** +``` +static/js/chartjs-streaming/ +├── chartjs-plugin-streaming.js # 📦 Archivo integrador principal +├── plugin.streaming.js # 🔧 Plugin de streaming +├── plugin.zoom.js # 🔍 Plugin de zoom +├── scale.realtime.js # ⏰ Escala de tiempo real +└── helpers.streaming.js # 🛠️ Utilidades +``` + +### 2. **Archivos modificados** +- `templates/index.html` - Carga de nueva librería +- `static/js/plotting.js` - Sistema de plotting reescrito para streaming + +## 🔧 Características Implementadas + +### ✅ **Ventajas del nuevo sistema:** + +1. **📊 Streaming automático**: Los gráficos se actualizan automáticamente +2. **🔄 Gestión inteligente de memoria**: Elimina datos antiguos automáticamente +3. **⚡ Mejor rendimiento**: Optimizado para datos en tiempo real +4. **🎛️ Controles avanzados**: Pause/Resume, Clear, mejor interactividad +5. **📏 Escalas dinámicas**: Ventana de tiempo deslizante automática +6. **🎨 Configuración flexible**: Duración, frecuencia, límites Y personalizables + +### ✅ **Funcionalidades disponibles:** + +#### **Configuración automática** +```javascript +// Se crea automáticamente con configuración optimizada +const config = ChartStreaming.createStreamingChartConfig({ + duration: 60000, // 60 segundos de ventana + refresh: 500, // Actualizar cada 500ms + frameRate: 30, // 30 FPS + yMin: -100, // Límite inferior Y + yMax: 100 // Límite superior Y +}); +``` + +#### **Controles de streaming** +- **▶️ Start**: Inicia el streaming en tiempo real +- **⏸️ Pause**: Pausa temporalmente el streaming +- **🗑️ Clear**: Limpia todos los datos del gráfico +- **⏹️ Stop**: Detiene completamente el streaming + +#### **Gestión automática de datos** +- Los datos se agregan automáticamente conforme llegan +- Los datos antiguos se eliminan según la configuración TTL +- La ventana de tiempo se desliza automáticamente + +## 🎯 Beneficios para el Usuario + +### **Antes (Sistema manual)** +```javascript +// Gestión manual de escalas y datos +chart.data.datasets = plotData.datasets; +chart.options.scales.x.min = startTime; +chart.options.scales.x.max = endTime; +chart.update('none'); +``` + +### **Ahora (Sistema streaming)** +```javascript +// Automático - solo agregar datos +ChartStreaming.addStreamingData(chart, datasetIndex, { + x: timestamp, + y: value +}); +// El plugin maneja todo lo demás automáticamente +``` + +## 🔧 Configuración por Defecto + +### **Escalas de tiempo real** +- **Duración**: 60 segundos por defecto (configurable por plot) +- **Refresco**: 500ms (datos se obtienen automáticamente del backend) +- **Frame rate**: 30 FPS para animaciones suaves +- **TTL**: Configurable para limpieza automática de datos + +### **Optimizaciones de rendimiento** +- Sin animaciones innecesarias +- Puntos de datos ocultos (solo visible en hover) +- Líneas suaves con tensión optimizada +- Actualización silenciosa ("quiet mode") + +## 📚 API Disponible + +### **Funciones principales:** +```javascript +// Control global +window.ChartStreaming.createStreamingChartConfig(options) +window.ChartStreaming.addStreamingData(chart, datasetIndex, data) +window.ChartStreaming.setStreamingPause(chart, paused) +window.ChartStreaming.clearStreamingData(chart) + +// Control por sesión (PlotManager) +plotManager.setStreamingPause(sessionId, paused) +plotManager.clearStreamingData(sessionId) +plotManager.refreshStreamingData(sessionId, chart) // Automática +``` + +### **Configuración personalizada:** +```javascript +{ + duration: 60000, // Ventana de tiempo en ms + delay: 0, // Retraso en ms + refresh: 500, // Intervalo de actualización en ms + frameRate: 30, // FPS para animaciones + pause: false, // Estado inicial + ttl: undefined, // Tiempo de vida de datos + yMin: undefined, // Límite inferior Y + yMax: undefined, // Límite superior Y + onRefresh: function // Callback para obtener datos +} +``` + +## 🔗 Integración con Backend + +### **Flujo de datos actualizado:** +1. **Backend**: Genera datos en `/api/plots/{sessionId}/data` +2. **Plugin**: Llama automáticamente `onRefresh` cada 500ms +3. **PlotManager**: Obtiene datos del backend en `refreshStreamingData` +4. **Chart**: Se actualiza automáticamente con nuevos datos + +### **Compatibilidad:** +- ✅ Funciona con todos los endpoints existentes +- ✅ Compatible con triggers booleanos +- ✅ Mantiene configuración Y min/max +- ✅ Preserva colores y estilos de variables + +## 🎮 Controles de Usuario + +### **Interfaz actualizada:** +- Los botones **Start/Pause/Clear/Stop** ahora controlan streaming +- **Pause**: Congela la visualización manteniendo datos +- **Clear**: Limpia gráfico pero mantiene configuración +- **Stop**: Pausa streaming y notifica al backend + +### **Retrocompatibilidad:** +- Todas las funciones existentes siguen funcionando +- Los plots existentes se migran automáticamente +- La API del backend no ha cambiado + +## 🔬 Debug y Troubleshooting + +### **Habilitar debug:** +```javascript +// En consola del navegador +enablePlotDebug() +``` + +### **Logs disponibles:** +- Inicialización de datasets de streaming +- Agregado de nuevos puntos de datos +- Control de pause/resume +- Limpieza de datos +- Errores de conexión con backend + +### **Verificar integración:** +```javascript +// En consola del navegador +testPlotSystem() +``` + +## 🚀 Próximos Pasos Recomendados + +1. **✅ Probar con datos reales** del PLC +2. **🎛️ Ajustar configuraciones** según necesidades específicas +3. **📊 Optimizar intervalos** de refresco según carga del sistema +4. **🔧 Personalizar colores** y estilos según preferencias + +## 💡 Tips de Uso + +### **Para mejor rendimiento:** +- Usa intervalos de refresco ≥ 500ms para reducir carga +- Configura TTL para limpiar datos antiguos automáticamente +- Mantén ≤ 10 variables por plot para fluidez óptima + +### **Para debugging:** +- Activa logs de debug cuando desarrolles +- Usa la consola del navegador para inspeccionar +- Verifica conectividad PLC antes de crear plots + +--- + +## 🎉 ¡Integración Completada! + +La aplicación ahora cuenta con un sistema de plotting en tiempo real robusto, eficiente y fácil de usar, potenciado por **chartjs-plugin-streaming**. + +**¡Disfruta de tus gráficos en tiempo real mejorados!** 📈✨ \ No newline at end of file diff --git a/STREAMING_TROUBLESHOOTING.md b/STREAMING_TROUBLESHOOTING.md new file mode 100644 index 0000000..9e996eb --- /dev/null +++ b/STREAMING_TROUBLESHOOTING.md @@ -0,0 +1,247 @@ +# 🔧 Troubleshooting Chart.js Streaming - Guía de Resolución + +## 🚨 Problema Reportado + +**Síntomas:** +- ✅ Status muestra "Active" +- ✅ Variables cambia de 0 a 1 +- ❌ Data Points se mantiene en 0 +- ❌ No se ve ningún plot dentro de la grilla +- ❌ La línea de tiempo no se mueve +- ⚠️ La escala Y cambia pero es lo único que funciona + +## 🔍 Diagnóstico Paso a Paso + +### **Paso 1: Verificar que se cargó chartjs-plugin-streaming** + +Abrir **Consola del Navegador** (F12) y ejecutar: + +```javascript +verifyStreamingIntegration() +``` + +**Resultado esperado:** +``` +🧪 Verificando integración de Chart.js Streaming... +✅ Chart.js cargado: true +✅ ChartStreaming cargado: true +✅ PlotManager cargado: true +✅ Sesiones de streaming activas: 1 +``` + +**Si ChartStreaming cargado: false:** +1. Verificar que `chartjs-plugin-streaming.js` se carga correctamente +2. Revisar errores en la consola +3. Recargar la página + +### **Paso 2: Habilitar Debug Detallado** + +```javascript +enablePlotDebug() +``` + +### **Paso 3: Forzar Actualización de Datos** + +```javascript +forceStreamingUpdate() +``` + +**Buscar en consola:** +``` +📈 Plot plot_13: Fetching data from backend... +📈 Plot plot_13: Received data: {...} +📈 Plot plot_13: Processing X datasets for streaming +``` + +### **Paso 4: Verificar Datos del Backend** + +Ejecutar en consola: + +```javascript +fetch('/api/plots/plot_13/data') + .then(r => r.json()) + .then(data => { + console.log('📊 Backend data:', data); + console.log('📊 Datasets:', data.datasets?.length || 0); + console.log('📊 Data points per dataset:', + data.datasets?.map(d => d.data?.length || 0) || []); + }); +``` + +**Resultado esperado:** +``` +📊 Backend data: {session_id: "plot_13", datasets: [...], data_points_count: X} +📊 Datasets: 1 +📊 Data points per dataset: [5, 8, 12] +``` + +### **Paso 5: Verificar Configuración del Chart** + +```javascript +// Para la sesión activa (ej: plot_13) +const sessionData = plotManager.sessions.get('plot_13'); +console.log('📈 Chart config:', { + hasChart: !!sessionData?.chart, + scaleType: sessionData?.chart?.scales?.x?.type, + hasRealTimeScale: sessionData?.chart?.scales?.x?.constructor?.name, + streamingEnabled: !!sessionData?.chart?.$streaming?.enabled, + datasets: sessionData?.chart?.data?.datasets?.length || 0 +}); +``` + +**Resultado esperado:** +``` +📈 Chart config: { + hasChart: true, + scaleType: "realtime", + hasRealTimeScale: "RealTimeScale", + streamingEnabled: true, + datasets: 1 +} +``` + +## 🛠️ Soluciones Comunes + +### **Problema: ChartStreaming no está cargado** + +**Causa:** El archivo `chartjs-plugin-streaming.js` no se carga correctamente. + +**Solución:** +1. Verificar que el archivo existe en `static/js/chartjs-streaming/chartjs-plugin-streaming.js` +2. Revisar que el HTML incluye: `` +3. Verificar orden de carga (debe ser después de Chart.js y antes de plotting.js) + +### **Problema: Backend devuelve datos pero no aparecen en el chart** + +**Causa:** Error en el procesamiento de datos o timestamps incorrectos. + +**Solución:** +```javascript +// Verificar timestamps de los datos +fetch('/api/plots/plot_13/data') + .then(r => r.json()) + .then(data => { + const firstDataset = data.datasets[0]; + const firstPoint = firstDataset.data[0]; + console.log('📊 First point timestamp:', firstPoint.x); + console.log('📊 Current time:', Date.now()); + console.log('📊 Time difference (sec):', (Date.now() - firstPoint.x) / 1000); + }); +``` + +Si la diferencia de tiempo es muy grande (>60 segundos), el punto puede estar fuera de la ventana de tiempo. + +### **Problema: Escala realtime no funciona** + +**Causa:** La escala no se inicializó correctamente. + +**Solución:** +```javascript +// Re-inicializar plot +const sessionId = 'plot_13'; // Cambiar por tu session ID +plotManager.controlPlot(sessionId, 'stop'); +setTimeout(() => { + plotManager.controlPlot(sessionId, 'start'); +}, 1000); +``` + +### **Problema: Data Points siempre en 0** + +**Causa:** Los datos no se están agregando al chart o se eliminan inmediatamente. + +**Solución verificar:** +1. **TTL Configuration**: Los datos pueden estar expirando muy rápido +2. **Timestamp Format**: Los timestamps pueden estar en formato incorrecto +3. **Dataset Index**: Los datos se pueden estar agregando al dataset incorrecto + +```javascript +// Agregar punto de prueba manualmente +const sessionData = plotManager.sessions.get('plot_13'); +if (sessionData?.chart) { + window.ChartStreaming.addStreamingData(sessionData.chart, 0, { + x: Date.now(), + y: Math.random() * 100 + }); + console.log('📈 Test point added'); +} +``` + +## 🎯 Test de Resolución Rápida + +**Ejecutar este script completo en consola:** + +```javascript +// Test completo de diagnóstico +console.log('🔧 DIAGNÓSTICO COMPLETO'); +console.log('='.repeat(50)); + +// 1. Verificar componentes básicos +console.log('1️⃣ COMPONENTES:'); +console.log('Chart.js:', typeof Chart !== 'undefined' ? '✅' : '❌'); +console.log('ChartStreaming:', typeof window.ChartStreaming !== 'undefined' ? '✅' : '❌'); +console.log('PlotManager:', typeof plotManager !== 'undefined' ? '✅' : '❌'); + +// 2. Verificar sesiones activas +if (plotManager && plotManager.sessions.size > 0) { + console.log('\n2️⃣ SESIONES ACTIVAS:'); + for (const [sessionId, sessionData] of plotManager.sessions) { + console.log(`📈 ${sessionId}:`, { + hasChart: !!sessionData.chart, + scaleType: sessionData.chart?.scales?.x?.type, + datasets: sessionData.chart?.data?.datasets?.length || 0, + dataPoints: sessionData.chart?.data?.datasets?.reduce((total, d) => total + (d.data?.length || 0), 0) || 0 + }); + } +} + +// 3. Test de backend data +console.log('\n3️⃣ BACKEND DATA TEST:'); +if (plotManager && plotManager.sessions.size > 0) { + const firstSessionId = Array.from(plotManager.sessions.keys())[0]; + fetch(`/api/plots/${firstSessionId}/data`) + .then(r => r.json()) + .then(data => { + console.log('📊 Backend response:', { + success: !!data.datasets, + datasets: data.datasets?.length || 0, + totalPoints: data.data_points_count || 0, + firstDatasetPoints: data.datasets?.[0]?.data?.length || 0 + }); + }) + .catch(err => console.log('❌ Backend error:', err.message)); +} + +console.log('\n4️⃣ NEXT STEPS:'); +console.log('- enablePlotDebug() para logs detallados'); +console.log('- forceStreamingUpdate() para forzar actualización'); +console.log('- Si persiste el problema, revisar configuración del backend'); +``` + +## 📞 Contacto de Soporte + +Si después de estos pasos el problema persiste: + +1. **Compartir resultado completo** del diagnóstico en consola +2. **Verificar logs del backend** en la terminal donde corre `python main.py` +3. **Revisar Network tab** en DevTools para errores de red + +--- + +## 🎉 Resultado Esperado + +Cuando funcione correctamente verás: + +``` +📈 Chart.js Streaming Plugin loaded successfully +📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false} +📈 Plot plot_13: Successfully initialized 1 streaming datasets +📈 Plot plot_13: Fetching data from backend... +📈 Plot plot_13: Adding 3 new points for UR29_Brix +📈 Added point to dataset 0 (UR29_Brix): x=1642598234567, y=54.258 +``` + +Y el gráfico mostrará: +- ✅ Línea de tiempo deslizándose automáticamente +- ✅ Data Points incrementándose +- ✅ Líneas de variables dibujándose en tiempo real +- ✅ Escala Y ajustándose a los datos \ No newline at end of file diff --git a/application_events.json b/application_events.json index 1c796d8..587a353 100644 --- a/application_events.json +++ b/application_events.json @@ -4119,8 +4119,473 @@ "trigger_variable": null, "auto_started": true } + }, + { + "timestamp": "2025-07-21T17:01:18.871713", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T17:01:18.903374", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T17:01:18.912924", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T17:13:36.924970", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Conducibilita Prodotto' removed", + "details": { + "session_id": "plot_9" + } + }, + { + "timestamp": "2025-07-21T17:13:40.385435", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Brix' removed", + "details": { + "session_id": "plot_12" + } + }, + { + "timestamp": "2025-07-21T17:13:59.375435", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Brix' created and started", + "details": { + "session_id": "plot_13", + "variables": [ + "UR29_Brix" + ], + "time_window": 10, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-07-21T17:33:27.303533", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T17:33:27.334173", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T17:33:27.345774", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T17:34:51.459774", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T17:34:51.489625", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T17:34:51.498003", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T17:38:38.361177", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T17:38:38.394439", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T17:38:38.402804", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:22:11.672676", + "level": "info", + "event_type": "csv_recording_stopped", + "message": "CSV recording stopped (dataset threads continue for UDP streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:22:11.682630", + "level": "info", + "event_type": "udp_streaming_stopped", + "message": "UDP streaming to PlotJuggler stopped (CSV recording continues)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:22:11.818203", + "level": "info", + "event_type": "dataset_deactivated", + "message": "Dataset deactivated: DAR", + "details": { + "dataset_id": "dar" + } + }, + { + "timestamp": "2025-07-21T18:22:11.827661", + "level": "info", + "event_type": "plc_disconnection", + "message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:24:05.417266", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Brix' removed", + "details": { + "session_id": "plot_13" + } + }, + { + "timestamp": "2025-07-21T18:25:07.058543", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T18:25:07.066931", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:25:07.076741", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 1 datasets", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2, + "auto_started_recording": true, + "recording_datasets": 1, + "dataset_names": [ + "DAR" + ] + } + }, + { + "timestamp": "2025-07-21T18:25:18.499928", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Brix' created and started", + "details": { + "session_id": "plot_14", + "variables": [ + "UR29_Brix" + ], + "time_window": 60, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-07-21T18:26:03.568841", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T18:26:03.600360", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T18:26:03.609460", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:27:08.559092", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:08.586976", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T18:27:08.593701", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:27:09.538108", + "level": "info", + "event_type": "csv_recording_stopped", + "message": "CSV recording stopped (dataset threads continue for UDP streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:09.545334", + "level": "info", + "event_type": "udp_streaming_stopped", + "message": "UDP streaming to PlotJuggler stopped (CSV recording continues)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:09.596088", + "level": "info", + "event_type": "dataset_deactivated", + "message": "Dataset deactivated: DAR", + "details": { + "dataset_id": "dar" + } + }, + { + "timestamp": "2025-07-21T18:27:09.606944", + "level": "info", + "event_type": "plc_disconnection", + "message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:12.713350", + "level": "info", + "event_type": "csv_recording_stopped", + "message": "CSV recording stopped (dataset threads continue for UDP streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:12.727587", + "level": "info", + "event_type": "udp_streaming_stopped", + "message": "UDP streaming to PlotJuggler stopped (CSV recording continues)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:12.762044", + "level": "info", + "event_type": "plc_disconnection", + "message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T18:27:26.860032", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T18:27:26.872811", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:27:26.884920", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 1 datasets", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2, + "auto_started_recording": true, + "recording_datasets": 1, + "dataset_names": [ + "DAR" + ] + } + }, + { + "timestamp": "2025-07-21T18:34:08.696308", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T18:34:08.730392", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T18:34:08.738627", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T18:34:46.471786", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Brix' removed", + "details": { + "session_id": "plot_14" + } + }, + { + "timestamp": "2025-07-21T18:35:13.057820", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Brix' created and started", + "details": { + "session_id": "plot_15", + "variables": [ + "UR29_Brix" + ], + "time_window": 60, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-07-21T18:36:17.216085", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Brix' removed", + "details": { + "session_id": "plot_15" + } + }, + { + "timestamp": "2025-07-21T18:38:00.731968", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'TEst' created and started", + "details": { + "session_id": "plot_16", + "variables": [ + "UR29_Brix" + ], + "time_window": 60, + "trigger_variable": null, + "auto_started": true + } } ], - "last_updated": "2025-07-21T14:44:25.311128", - "total_entries": 389 + "last_updated": "2025-07-21T18:38:00.731968", + "total_entries": 436 } \ No newline at end of file diff --git a/plc_datasets.json b/plc_datasets.json index f2702ef..239ce3e 100644 --- a/plc_datasets.json +++ b/plc_datasets.json @@ -48,8 +48,8 @@ "streaming_variables": [ "UR29_Brix_Digital", "UR62_Brix", - "UR29_Brix", - "CTS306_PV" + "CTS306_PV", + "UR29_Brix" ], "sampling_interval": 0.2, "enabled": true, @@ -70,5 +70,5 @@ ], "current_dataset_id": "dar", "version": "1.0", - "last_update": "2025-07-21T14:43:41.517456" + "last_update": "2025-07-21T18:34:08.728102" } \ No newline at end of file diff --git a/plot_sessions.json b/plot_sessions.json index 71b8b9c..e6484d9 100644 --- a/plot_sessions.json +++ b/plot_sessions.json @@ -1,34 +1,20 @@ { "plots": { - "plot_9": { - "name": "Conducibilita Prodotto", + "plot_16": { + "name": "TEst", "variables": [ "UR29_Brix" ], - "time_window": 10, - "y_min": null, - "y_max": null, - "trigger_variable": null, - "trigger_enabled": false, - "trigger_on_true": true, - "session_id": "plot_9" - }, - "plot_12": { - "name": "Brix", - "variables": [ - "UR29_Brix", - "UR62_Brix" - ], "time_window": 60, "y_min": null, "y_max": null, "trigger_variable": null, "trigger_enabled": false, "trigger_on_true": true, - "session_id": "plot_12" + "session_id": "plot_16" } }, - "session_counter": 13, - "last_saved": "2025-07-21T14:44:25.311128", + "session_counter": 17, + "last_saved": "2025-07-21T18:38:00.730994", "version": "1.0" } \ No newline at end of file diff --git a/static/js/chartjs-streaming/chartjs-plugin-streaming.js b/static/js/chartjs-streaming/chartjs-plugin-streaming.js new file mode 100644 index 0000000..6cc6efd --- /dev/null +++ b/static/js/chartjs-streaming/chartjs-plugin-streaming.js @@ -0,0 +1,405 @@ +/** + * 📈 Chart.js Plugin Streaming Integration + * Integra chartjs-plugin-streaming para plotting en tiempo real + * + * Combinación de módulos: + * - helpers.streaming.js + * - scale.realtime.js + * - plugin.streaming.js + * - plugin.zoom.js (integración con zoom) + */ + +(function (global, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + factory(exports, require('chart.js')); + } else if (typeof define === 'function' && define.amd) { + define(['exports', 'chart.js'], factory); + } else { + global = global || self; + factory(global.ChartStreaming = {}, global.Chart); + } +})(this, function (exports, Chart) { + 'use strict'; + + // ============= HELPERS.STREAMING.JS ============= + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function resolveOption(scale, key) { + const realtimeOptions = scale.options.realtime || {}; + const scaleOptions = scale.options; + + if (realtimeOptions[key] !== undefined) { + return realtimeOptions[key]; + } + if (scaleOptions[key] !== undefined) { + return scaleOptions[key]; + } + + // Valores por defecto + const defaults = { + duration: 10000, + delay: 0, + refresh: 1000, + frameRate: 30, + pause: false, + ttl: undefined, + onRefresh: null + }; + + return defaults[key]; + } + + function getAxisMap(element, keys, meta) { + const axis = meta.vAxisID || 'y'; + return keys[axis] || []; + } + + // ============= SCALE.REALTIME.JS (Corregido) ============= + class RealTimeScale extends Chart.Scale { + constructor(cfg) { + super(cfg); + this.type = 'realtime'; + } + + init(scaleOptions, scaleContext) { + super.init(scaleOptions, scaleContext); + + const me = this; + const chart = me.chart; + const streaming = chart.$streaming = chart.$streaming || {}; + streaming.enabled = true; // Marcar como streaming activo + + // 🔧 DEBUG: Ver qué opciones estamos recibiendo + console.log('📈 RealTimeScale DEBUG - scaleOptions:', scaleOptions); + console.log('📈 RealTimeScale DEBUG - me.options:', me.options); + console.log('📈 RealTimeScale DEBUG - me.options.realtime:', me.options.realtime); + + // Inicializar opciones de tiempo real + const onRefreshResolved = resolveOption(me, 'onRefresh'); + console.log('📈 RealTimeScale DEBUG - onRefresh resolved:', onRefreshResolved, typeof onRefreshResolved); + + me.realtime = { + duration: resolveOption(me, 'duration'), + delay: resolveOption(me, 'delay'), + refresh: resolveOption(me, 'refresh'), + frameRate: resolveOption(me, 'frameRate'), + pause: resolveOption(me, 'pause'), + ttl: resolveOption(me, 'ttl'), + onRefresh: onRefreshResolved + }; + + console.log('📈 RealTimeScale initialized:', { + duration: me.realtime.duration, + refresh: me.realtime.refresh, + pause: me.realtime.pause, + hasOnRefresh: typeof me.realtime.onRefresh === 'function' + }); + + // Configurar intervalo de actualización + if (!streaming.intervalId && me.realtime.refresh > 0) { + streaming.intervalId = setInterval(() => { + if (!me.realtime.pause && typeof me.realtime.onRefresh === 'function') { + me.realtime.onRefresh(chart); + } + me.updateRealTimeData(); + chart.update('quiet'); + }, me.realtime.refresh); + + console.log('📈 RealTimeScale interval started:', me.realtime.refresh + 'ms'); + } + } + + updateRealTimeData() { + const me = this; + const chart = me.chart; + + if (!chart.data || !chart.data.datasets) { + return; + } + + const now = Date.now(); + const duration = me.realtime.duration; + const delay = me.realtime.delay; + const ttl = me.realtime.ttl || duration * 2; // TTL por defecto + + // Calcular ventana de tiempo + me.max = now - delay; + me.min = me.max - duration; + + // Limpiar datos antiguos automáticamente + const cutoff = now - ttl; + chart.data.datasets.forEach(dataset => { + if (dataset.data) { + const oldLength = dataset.data.length; + dataset.data = dataset.data.filter(point => point.x > cutoff); + if (oldLength !== dataset.data.length) { + console.log(`📈 Cleaned ${oldLength - dataset.data.length} old points from ${dataset.label}`); + } + } + }); + } + + update(args) { + this.updateRealTimeData(); + super.update(args); + } + + destroy() { + const me = this; + const chart = me.chart; + const streaming = chart.$streaming; + + if (streaming && streaming.intervalId) { + clearInterval(streaming.intervalId); + delete streaming.intervalId; + console.log('📈 RealTimeScale interval cleared'); + } + + super.destroy(); + } + + static id = 'realtime'; + static defaults = { + realtime: { + duration: 10000, + delay: 0, + refresh: 1000, + frameRate: 30, + pause: false, + ttl: undefined, + onRefresh: null + }, + time: { + unit: 'second', + displayFormats: { + second: 'HH:mm:ss' + } + } + }; + } + + // ============= PLUGIN.STREAMING.JS (Simplificado) ============= + const streamingPlugin = { + id: 'streaming', + + beforeInit(chart) { + const streaming = chart.$streaming = chart.$streaming || {}; + streaming.enabled = false; + + // Detectar si hay escalas realtime + const scales = chart.options.scales || {}; + Object.keys(scales).forEach(scaleId => { + if (scales[scaleId].type === 'realtime') { + streaming.enabled = true; + } + }); + }, + + afterInit(chart) { + const streaming = chart.$streaming; + if (streaming && streaming.enabled) { + // Configurar actualización automática + const update = chart.update; + chart.update = function (mode) { + if (mode === 'quiet') { + // Actualización silenciosa para streaming + Chart.prototype.update.call(this, mode); + } else { + update.call(this, mode); + } + }; + } + }, + + beforeUpdate(chart) { + const streaming = chart.$streaming; + if (!streaming || !streaming.enabled) return; + + // Permitir que las líneas Bézier se extiendan fuera del área del gráfico + const elements = chart.options.elements || {}; + if (elements.line) { + elements.line.capBezierPoints = false; + } + }, + + destroy(chart) { + const streaming = chart.$streaming; + if (streaming && streaming.intervalId) { + clearInterval(streaming.intervalId); + delete streaming.intervalId; + } + delete chart.$streaming; + } + }; + + // ============= REGISTRO DE COMPONENTES ============= + + // Registrar escala realtime + Chart.register(RealTimeScale); + + // Registrar plugin de streaming + Chart.register(streamingPlugin); + + // ============= UTILIDADES PARA LA APLICACIÓN ============= + + /** + * Crea una configuración de Chart.js optimizada para streaming + */ + function createStreamingChartConfig(options = {}) { + const config = { + type: 'line', + data: { + datasets: [] + }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: false, // Desactivar animaciones para mejor performance + + scales: { + x: { + type: 'realtime', + realtime: { + duration: options.duration || 60000, // 60 segundos por defecto + delay: options.delay || 0, + refresh: options.refresh || 1000, // 1 segundo + frameRate: options.frameRate || 30, + pause: options.pause || false, + ttl: options.ttl || undefined, + onRefresh: options.onRefresh || null + }, + title: { + display: true, + text: 'Tiempo' + } + }, + y: { + title: { + display: true, + text: 'Valor' + }, + min: options.yMin, + max: options.yMax + } + }, + + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + + elements: { + point: { + radius: 0, // Sin puntos para mejor performance + hoverRadius: 3 + }, + line: { + tension: 0.1, + borderWidth: 2 + } + } + }, + plugins: ['streaming'] + }; + + return config; + } + + /** + * Agrega datos a un dataset de streaming + */ + function addStreamingData(chart, datasetIndex, data) { + if (!chart || !chart.data || !chart.data.datasets[datasetIndex]) { + console.warn(`📈 Cannot add streaming data - chart or dataset ${datasetIndex} not found`); + return false; + } + + const dataset = chart.data.datasets[datasetIndex]; + if (!dataset.data) { + dataset.data = []; + } + + // Agregar nuevo punto con timestamp + const timestamp = data.x || Date.now(); + const newPoint = { + x: timestamp, + y: data.y + }; + + dataset.data.push(newPoint); + + console.log(`📈 Added point to dataset ${datasetIndex} (${dataset.label}): x=${timestamp}, y=${data.y}`); + + // Chart.js se encarga automáticamente de eliminar datos antiguos + // basado en la configuración de TTL y duration de la escala realtime + return true; + } + + /** + * Controla la pausa/reanudación del streaming + */ + function setStreamingPause(chart, paused) { + if (!chart || !chart.$streaming) return; + + const scales = chart.scales; + Object.keys(scales).forEach(scaleId => { + const scale = scales[scaleId]; + if (scale instanceof RealTimeScale) { + scale.realtime.pause = paused; + } + }); + } + + /** + * Limpia todos los datos de streaming + */ + function clearStreamingData(chart) { + if (!chart || !chart.data) return; + + chart.data.datasets.forEach(dataset => { + if (dataset.data) { + dataset.data.length = 0; + } + }); + + chart.update('quiet'); + } + + // ============= EXPORTS ============= + + // Exportar para uso en la aplicación + exports.RealTimeScale = RealTimeScale; + exports.streamingPlugin = streamingPlugin; + exports.createStreamingChartConfig = createStreamingChartConfig; + exports.addStreamingData = addStreamingData; + exports.setStreamingPause = setStreamingPause; + exports.clearStreamingData = clearStreamingData; + + // Hacer disponible globalmente + if (typeof window !== 'undefined') { + window.ChartStreaming = { + createStreamingChartConfig, + addStreamingData, + setStreamingPause, + clearStreamingData, + RealTimeScale, + streamingPlugin + }; + } + + console.log('📈 Chart.js Streaming Plugin loaded successfully'); +}); \ No newline at end of file diff --git a/static/js/chartjs-streaming/helpers.streaming.js b/static/js/chartjs-streaming/helpers.streaming.js new file mode 100644 index 0000000..9b4c1c8 --- /dev/null +++ b/static/js/chartjs-streaming/helpers.streaming.js @@ -0,0 +1,85 @@ +import {callback as call, each, noop, requestAnimFrame, valueOrDefault} from 'chart.js/helpers'; + +export function clamp(value, lower, upper) { + return Math.min(Math.max(value, lower), upper); +} + +export function resolveOption(scale, key) { + const realtimeOpts = scale.options.realtime; + const streamingOpts = scale.chart.options.plugins.streaming; + return valueOrDefault(realtimeOpts[key], streamingOpts[key]); +} + +export function getAxisMap(element, {x, y}, {xAxisID, yAxisID}) { + const axisMap = {}; + + each(x, key => { + axisMap[key] = {axisId: xAxisID}; + }); + each(y, key => { + axisMap[key] = {axisId: yAxisID}; + }); + return axisMap; +} + +/** +* Cancel animation polyfill +*/ +const cancelAnimFrame = (function() { + if (typeof window === 'undefined') { + return noop; + } + return window.cancelAnimationFrame; +}()); + +export function startFrameRefreshTimer(context, func) { + if (!context.frameRequestID) { + const refresh = () => { + const nextRefresh = context.nextRefresh || 0; + const now = Date.now(); + + if (nextRefresh <= now) { + const newFrameRate = call(func); + const frameDuration = 1000 / (Math.max(newFrameRate, 0) || 30); + const newNextRefresh = context.nextRefresh + frameDuration || 0; + + context.nextRefresh = newNextRefresh > now ? newNextRefresh : now + frameDuration; + } + context.frameRequestID = requestAnimFrame.call(window, refresh); + }; + context.frameRequestID = requestAnimFrame.call(window, refresh); + } +} + +export function stopFrameRefreshTimer(context) { + const frameRequestID = context.frameRequestID; + + if (frameRequestID) { + cancelAnimFrame.call(window, frameRequestID); + delete context.frameRequestID; + } +} + +export function stopDataRefreshTimer(context) { + const refreshTimerID = context.refreshTimerID; + + if (refreshTimerID) { + clearInterval(refreshTimerID); + delete context.refreshTimerID; + delete context.refreshInterval; + } +} + +export function startDataRefreshTimer(context, func, interval) { + if (!context.refreshTimerID) { + context.refreshTimerID = setInterval(() => { + const newInterval = call(func); + + if (context.refreshInterval !== newInterval && !isNaN(newInterval)) { + stopDataRefreshTimer(context); + startDataRefreshTimer(context, func, newInterval); + } + }, interval || 0); + context.refreshInterval = interval || 0; + } +} diff --git a/static/js/chartjs-streaming/plugin.streaming.js b/static/js/chartjs-streaming/plugin.streaming.js new file mode 100644 index 0000000..adff208 --- /dev/null +++ b/static/js/chartjs-streaming/plugin.streaming.js @@ -0,0 +1,216 @@ +import {Chart, DatasetController, defaults, registry} from 'chart.js'; +import {each, noop, getRelativePosition, clipArea, unclipArea} from 'chart.js/helpers'; +import {getAxisMap} from '../helpers/helpers.streaming'; +import {attachChart as annotationAttachChart, detachChart as annotationDetachChart} from '../plugins/plugin.annotation'; +import {update as tooltipUpdate} from '../plugins/plugin.tooltip'; +import {attachChart as zoomAttachChart, detachChart as zoomDetachChart} from '../plugins/plugin.zoom'; +import RealTimeScale from '../scales/scale.realtime'; +import {version} from '../../package.json'; + +defaults.set('transitions', { + quiet: { + animation: { + duration: 0 + } + } +}); + +const transitionKeys = {x: ['x', 'cp1x', 'cp2x'], y: ['y', 'cp1y', 'cp2y']}; + +function update(mode) { + const me = this; + + if (mode === 'quiet') { + each(me.data.datasets, (dataset, datasetIndex) => { + const controller = me.getDatasetMeta(datasetIndex).controller; + + // Set transition mode to 'quiet' + controller._setStyle = function(element, index, _mode, active) { + DatasetController.prototype._setStyle.call(this, element, index, 'quiet', active); + }; + }); + } + + Chart.prototype.update.call(me, mode); + + if (mode === 'quiet') { + each(me.data.datasets, (dataset, datasetIndex) => { + delete me.getDatasetMeta(datasetIndex).controller._setStyle; + }); + } +} + +function render(chart) { + const streaming = chart.$streaming; + + chart.render(); + + if (streaming.lastMouseEvent) { + setTimeout(() => { + const lastMouseEvent = streaming.lastMouseEvent; + if (lastMouseEvent) { + chart._eventHandler(lastMouseEvent); + } + }, 0); + } +} + +export default { + id: 'streaming', + + version, + + beforeInit(chart) { + const streaming = chart.$streaming = chart.$streaming || {render}; + const canvas = streaming.canvas = chart.canvas; + const mouseEventListener = streaming.mouseEventListener = event => { + const pos = getRelativePosition(event, chart); + streaming.lastMouseEvent = { + type: 'mousemove', + chart: chart, + native: event, + x: pos.x, + y: pos.y + }; + }; + + canvas.addEventListener('mousedown', mouseEventListener); + canvas.addEventListener('mouseup', mouseEventListener); + }, + + afterInit(chart) { + chart.update = update; + }, + + beforeUpdate(chart) { + const {scales, elements} = chart.options; + const tooltip = chart.tooltip; + + each(scales, ({type}) => { + if (type === 'realtime') { + // Allow Bézier control to be outside the chart + elements.line.capBezierPoints = false; + } + }); + + if (tooltip) { + tooltip.update = tooltipUpdate; + } + + try { + const plugin = registry.getPlugin('annotation'); + annotationAttachChart(plugin, chart); + } catch (e) { + annotationDetachChart(chart); + } + + try { + const plugin = registry.getPlugin('zoom'); + zoomAttachChart(plugin, chart); + } catch (e) { + zoomDetachChart(chart); + } + }, + + beforeDatasetUpdate(chart, args) { + const {meta, mode} = args; + + if (mode === 'quiet') { + const {controller, $animations} = meta; + + // Skip updating element options if show/hide transition is active + if ($animations && $animations.visible && $animations.visible._active) { + controller.updateElement = noop; + controller.updateSharedOptions = noop; + } + } + }, + + afterDatasetUpdate(chart, args) { + const {meta, mode} = args; + const {data: elements = [], dataset: element, controller} = meta; + + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + elements[i].$streaming = getAxisMap(elements[i], transitionKeys, meta); + } + if (element) { + element.$streaming = getAxisMap(element, transitionKeys, meta); + } + + if (mode === 'quiet') { + delete controller.updateElement; + delete controller.updateSharedOptions; + } + }, + + beforeDatasetDraw(chart, args) { + const {ctx, chartArea, width, height} = chart; + const {xAxisID, yAxisID, controller} = args.meta; + const area = { + left: 0, + top: 0, + right: width, + bottom: height + }; + + if (xAxisID && controller.getScaleForId(xAxisID) instanceof RealTimeScale) { + area.left = chartArea.left; + area.right = chartArea.right; + } + if (yAxisID && controller.getScaleForId(yAxisID) instanceof RealTimeScale) { + area.top = chartArea.top; + area.bottom = chartArea.bottom; + } + clipArea(ctx, area); + }, + + afterDatasetDraw(chart) { + unclipArea(chart.ctx); + }, + + beforeEvent(chart, args) { + const streaming = chart.$streaming; + const event = args.event; + + if (event.type === 'mousemove') { + // Save mousemove event for reuse + streaming.lastMouseEvent = event; + } else if (event.type === 'mouseout') { + // Remove mousemove event + delete streaming.lastMouseEvent; + } + }, + + destroy(chart) { + const {scales, $streaming: streaming, tooltip} = chart; + const {canvas, mouseEventListener} = streaming; + + delete chart.update; + if (tooltip) { + delete tooltip.update; + } + + canvas.removeEventListener('mousedown', mouseEventListener); + canvas.removeEventListener('mouseup', mouseEventListener); + + each(scales, scale => { + if (scale instanceof RealTimeScale) { + scale.destroy(); + } + }); + }, + + defaults: { + duration: 10000, + delay: 0, + frameRate: 30, + refresh: 1000, + onRefresh: null, + pause: false, + ttl: undefined + }, + + descriptors: { + _scriptable: name => name !== 'onRefresh' + } +}; diff --git a/static/js/chartjs-streaming/plugin.zoom.js b/static/js/chartjs-streaming/plugin.zoom.js new file mode 100644 index 0000000..8e57580 --- /dev/null +++ b/static/js/chartjs-streaming/plugin.zoom.js @@ -0,0 +1,125 @@ +import {each} from 'chart.js/helpers'; +import {clamp, resolveOption} from '../helpers/helpers.streaming'; + +const chartStates = new WeakMap(); + +function getState(chart) { + let state = chartStates.get(chart); + + if (!state) { + state = {originalScaleOptions: {}}; + chartStates.set(chart, state); + } + return state; +} + +function removeState(chart) { + chartStates.delete(chart); +} + +function storeOriginalScaleOptions(chart) { + const {originalScaleOptions} = getState(chart); + const scales = chart.scales; + + each(scales, scale => { + const id = scale.id; + + if (!originalScaleOptions[id]) { + originalScaleOptions[id] = { + duration: resolveOption(scale, 'duration'), + delay: resolveOption(scale, 'delay') + }; + } + }); + each(originalScaleOptions, (opt, key) => { + if (!scales[key]) { + delete originalScaleOptions[key]; + } + }); + return originalScaleOptions; +} + +function zoomRealTimeScale(scale, zoom, center, limits) { + const {chart, axis} = scale; + const {minDuration = 0, maxDuration = Infinity, minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const realtimeOpts = scale.options.realtime; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const newDuration = clamp(duration * (2 - zoom), minDuration, maxDuration); + let maxPercent, newDelay; + + storeOriginalScaleOptions(chart); + + if (scale.isHorizontal()) { + maxPercent = (scale.right - center.x) / (scale.right - scale.left); + } else { + maxPercent = (scale.bottom - center.y) / (scale.bottom - scale.top); + } + newDelay = delay + maxPercent * (duration - newDuration); + realtimeOpts.duration = newDuration; + realtimeOpts.delay = clamp(newDelay, minDelay, maxDelay); + return newDuration !== scale.max - scale.min; +} + +function panRealTimeScale(scale, delta, limits) { + const {chart, axis} = scale; + const {minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {}; + const delay = resolveOption(scale, 'delay'); + const newDelay = delay + (scale.getValueForPixel(delta) - scale.getValueForPixel(0)); + + storeOriginalScaleOptions(chart); + + scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay); + return true; +} + +function resetRealTimeScaleOptions(chart) { + const originalScaleOptions = storeOriginalScaleOptions(chart); + + each(chart.scales, scale => { + const realtimeOptions = scale.options.realtime; + + if (realtimeOptions) { + const original = originalScaleOptions[scale.id]; + + if (original) { + realtimeOptions.duration = original.duration; + realtimeOptions.delay = original.delay; + } else { + delete realtimeOptions.duration; + delete realtimeOptions.delay; + } + } + }); +} + +function initZoomPlugin(plugin) { + plugin.zoomFunctions.realtime = zoomRealTimeScale; + plugin.panFunctions.realtime = panRealTimeScale; +} + +export function attachChart(plugin, chart) { + const streaming = chart.$streaming; + + if (streaming.zoomPlugin !== plugin) { + const resetZoom = streaming.resetZoom = chart.resetZoom; + + initZoomPlugin(plugin); + chart.resetZoom = transition => { + resetRealTimeScaleOptions(chart); + resetZoom(transition); + }; + streaming.zoomPlugin = plugin; + } +} + +export function detachChart(chart) { + const streaming = chart.$streaming; + + if (streaming.zoomPlugin) { + chart.resetZoom = streaming.resetZoom; + removeState(chart); + delete streaming.resetZoom; + delete streaming.zoomPlugin; + } +} diff --git a/static/js/chartjs-streaming/scale.realtime.js b/static/js/chartjs-streaming/scale.realtime.js new file mode 100644 index 0000000..203be3c --- /dev/null +++ b/static/js/chartjs-streaming/scale.realtime.js @@ -0,0 +1,507 @@ +import {defaults, TimeScale} from 'chart.js'; +import {_lookup, callback as call, each, isArray, isFinite, isNumber, noop, clipArea, unclipArea} from 'chart.js/helpers'; +import {resolveOption, startFrameRefreshTimer, stopFrameRefreshTimer, startDataRefreshTimer, stopDataRefreshTimer} from '../helpers/helpers.streaming'; +import {getElements} from '../plugins/plugin.annotation'; + +// Ported from Chart.js 2.8.0 35273ee. +const INTERVALS = { + millisecond: { + common: true, + size: 1, + steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] + }, + second: { + common: true, + size: 1000, + steps: [1, 2, 5, 10, 15, 30] + }, + minute: { + common: true, + size: 60000, + steps: [1, 2, 5, 10, 15, 30] + }, + hour: { + common: true, + size: 3600000, + steps: [1, 2, 3, 6, 12] + }, + day: { + common: true, + size: 86400000, + steps: [1, 2, 5] + }, + week: { + common: false, + size: 604800000, + steps: [1, 2, 3, 4] + }, + month: { + common: true, + size: 2.628e9, + steps: [1, 2, 3] + }, + quarter: { + common: false, + size: 7.884e9, + steps: [1, 2, 3, 4] + }, + year: { + common: true, + size: 3.154e10 + } +}; + +// Ported from Chart.js 2.8.0 35273ee. +const UNITS = Object.keys(INTERVALS); + +// Ported from Chart.js 2.8.0 35273ee. +function determineStepSize(min, max, unit, capacity) { + const range = max - min; + const {size: milliseconds, steps} = INTERVALS[unit]; + let factor; + + if (!steps) { + return Math.ceil(range / (capacity * milliseconds)); + } + + for (let i = 0, ilen = steps.length; i < ilen; ++i) { + factor = steps[i]; + if (Math.ceil(range / (milliseconds * factor)) <= capacity) { + break; + } + } + + return factor; +} + +// Ported from Chart.js 2.8.0 35273ee. +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const range = max - min; + const ilen = UNITS.length; + + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const {common, size, steps} = INTERVALS[UNITS[i]]; + const factor = steps ? steps[steps.length - 1] : Number.MAX_SAFE_INTEGER; + + if (common && Math.ceil(range / (factor * size)) <= capacity) { + return UNITS[i]; + } + } + + return UNITS[ilen - 1]; +} + +// Ported from Chart.js 2.8.0 35273ee. +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} + +// Ported from Chart.js 3.2.0 e1404ac. +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} + +const datasetPropertyKeys = [ + 'pointBackgroundColor', + 'pointBorderColor', + 'pointBorderWidth', + 'pointRadius', + 'pointRotation', + 'pointStyle', + 'pointHitRadius', + 'pointHoverBackgroundColor', + 'pointHoverBorderColor', + 'pointHoverBorderWidth', + 'pointHoverRadius', + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'radius', + 'rotation' +]; + +function clean(scale) { + const {chart, id, max} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const ttl = resolveOption(scale, 'ttl'); + const pause = resolveOption(scale, 'pause'); + const min = Date.now() - (isNaN(ttl) ? duration + delay : ttl); + let i, start, count, removalRange; + + // Remove old data + each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const axis = id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y'; + + if (axis) { + const controller = meta.controller; + const data = dataset.data; + const length = data.length; + + if (pause) { + // If the scale is paused, preserve the visible data points + for (i = 0; i < length; ++i) { + const point = controller.getParsed(i); + if (point && !(point[axis] < max)) { + break; + } + } + start = i + 2; + } else { + start = 0; + } + + for (i = start; i < length; ++i) { + const point = controller.getParsed(i); + if (!point || !(point[axis] <= min)) { + break; + } + } + count = i - start; + if (isNaN(ttl)) { + // Keep the last two data points outside the range not to affect the existing bezier curve + count = Math.max(count - 2, 0); + } + + data.splice(start, count); + each(datasetPropertyKeys, key => { + if (isArray(dataset[key])) { + dataset[key].splice(start, count); + } + }); + each(dataset.datalabels, value => { + if (isArray(value)) { + value.splice(start, count); + } + }); + if (typeof data[0] !== 'object') { + removalRange = { + start: start, + count: count + }; + } + + each(chart._active, (item, index) => { + if (item.datasetIndex === datasetIndex && item.index >= start) { + if (item.index >= start + count) { + item.index -= count; + } else { + chart._active.splice(index, 1); + } + } + }, null, true); + } + }); + if (removalRange) { + chart.data.labels.splice(removalRange.start, removalRange.count); + } +} + +function transition(element, id, translate) { + const animations = element.$animations || {}; + + each(element.$streaming, (item, key) => { + if (item.axisId === id) { + const delta = item.reverse ? -translate : translate; + const animation = animations[key]; + + if (isFinite(element[key])) { + element[key] -= delta; + } + if (animation) { + animation._from -= delta; + animation._to -= delta; + } + } + }); +} + +function scroll(scale) { + const {chart, id, $realtime: realtime} = scale; + const duration = resolveOption(scale, 'duration'); + const delay = resolveOption(scale, 'delay'); + const isHorizontal = scale.isHorizontal(); + const length = isHorizontal ? scale.width : scale.height; + const now = Date.now(); + const tooltip = chart.tooltip; + const annotations = getElements(chart); + let offset = length * (now - realtime.head) / duration; + + if (isHorizontal === !!scale.options.reverse) { + offset = -offset; + } + + // Shift all the elements leftward or downward + each(chart.data.datasets, (dataset, datasetIndex) => { + const meta = chart.getDatasetMeta(datasetIndex); + const {data: elements = [], dataset: element} = meta; + + for (let i = 0, ilen = elements.length; i < ilen; ++i) { + transition(elements[i], id, offset); + } + if (element) { + transition(element, id, offset); + delete element._path; + } + }); + + // Shift all the annotation elements leftward or downward + for (let i = 0, ilen = annotations.length; i < ilen; ++i) { + transition(annotations[i], id, offset); + } + + // Shift tooltip leftward or downward + if (tooltip) { + transition(tooltip, id, offset); + } + + scale.max = now - delay; + scale.min = scale.max - duration; + + realtime.head = now; +} + +export default class RealTimeScale extends TimeScale { + + constructor(props) { + super(props); + this.$realtime = this.$realtime || {}; + } + + init(scaleOpts, opts) { + const me = this; + + super.init(scaleOpts, opts); + startDataRefreshTimer(me.$realtime, () => { + const chart = me.chart; + const onRefresh = resolveOption(me, 'onRefresh'); + + call(onRefresh, [chart], me); + clean(me); + chart.update('quiet'); + return resolveOption(me, 'refresh'); + }); + } + + update(maxWidth, maxHeight, margins) { + const me = this; + const {$realtime: realtime, options} = me; + const {bounds, offset, ticks: ticksOpts} = options; + const {autoSkip, source, major: majorTicksOpts} = ticksOpts; + const majorEnabled = majorTicksOpts.enabled; + + if (resolveOption(me, 'pause')) { + stopFrameRefreshTimer(realtime); + } else { + if (!realtime.frameRequestID) { + realtime.head = Date.now(); + } + startFrameRefreshTimer(realtime, () => { + const chart = me.chart; + const streaming = chart.$streaming; + + scroll(me); + if (streaming) { + call(streaming.render, [chart]); + } + return resolveOption(me, 'frameRate'); + }); + } + + options.bounds = undefined; + options.offset = false; + ticksOpts.autoSkip = false; + ticksOpts.source = source === 'auto' ? '' : source; + majorTicksOpts.enabled = true; + + super.update(maxWidth, maxHeight, margins); + + options.bounds = bounds; + options.offset = offset; + ticksOpts.autoSkip = autoSkip; + ticksOpts.source = source; + majorTicksOpts.enabled = majorEnabled; + } + + buildTicks() { + const me = this; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const max = me.$realtime.head - delay; + const min = max - duration; + const maxArray = [1e15, max]; + const minArray = [-1e15, min]; + + Object.defineProperty(me, 'min', { + get: () => minArray.shift(), + set: noop + }); + Object.defineProperty(me, 'max', { + get: () => maxArray.shift(), + set: noop + }); + + const ticks = super.buildTicks(); + + delete me.min; + delete me.max; + me.min = min; + me.max = max; + + return ticks; + } + + calculateLabelRotation() { + const ticksOpts = this.options.ticks; + const maxRotation = ticksOpts.maxRotation; + + ticksOpts.maxRotation = ticksOpts.minRotation || 0; + super.calculateLabelRotation(); + ticksOpts.maxRotation = maxRotation; + } + + fit() { + const me = this; + const options = me.options; + + super.fit(); + + if (options.ticks.display && options.display && me.isHorizontal()) { + me.paddingLeft = 3; + me.paddingRight = 3; + me._handleMargins(); + } + } + + draw(chartArea) { + const me = this; + const {chart, ctx} = me; + const area = me.isHorizontal() ? + { + left: chartArea.left, + top: 0, + right: chartArea.right, + bottom: chart.height + } : { + left: 0, + top: chartArea.top, + right: chart.width, + bottom: chartArea.bottom + }; + + me._gridLineItems = null; + me._labelItems = null; + + // Clip and draw the scale + clipArea(ctx, area); + super.draw(chartArea); + unclipArea(ctx); + } + + destroy() { + const realtime = this.$realtime; + + stopFrameRefreshTimer(realtime); + stopDataRefreshTimer(realtime); + } + + _generate() { + const me = this; + const adapter = me._adapter; + const duration = resolveOption(me, 'duration'); + const delay = resolveOption(me, 'delay'); + const refresh = resolveOption(me, 'refresh'); + const max = me.$realtime.head - delay; + const min = max - duration; + const capacity = me._getLabelCapacity(min); + const {time: timeOpts, ticks: ticksOpts} = me.options; + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + const major = determineMajorUnit(minor); + const stepSize = timeOpts.stepSize || determineStepSize(min, max, minor, capacity); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const majorTicksEnabled = ticksOpts.major.enabled; + const hasWeekday = isNumber(weekday) || weekday === true; + const interval = INTERVALS[minor]; + const ticks = {}; + let first = min; + let time, count; + + // For 'week' unit, handle the first day of week option + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + + // Align first ticks on unit + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + + // Prevent browser from freezing in case user options request millions of milliseconds + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + + time = first; + + if (majorTicksEnabled && major && !hasWeekday && !timeOpts.round) { + // Align the first tick on the previous `minor` unit aligned on the `major` unit: + // we first aligned time on the previous `major` unit then add the number of full + // stepSize there is between first and the previous major time. + time = +adapter.startOf(time, major); + time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + } + + const timestamps = ticksOpts.source === 'data' && me.getDataTimestamps(); + for (count = 0; time < max + refresh; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + + if (time === max + refresh || count === 1) { + addTick(ticks, time, timestamps); + } + + return Object.keys(ticks).sort((a, b) => a - b).map(x => +x); + } +} + +RealTimeScale.id = 'realtime'; + +RealTimeScale.defaults = { + bounds: 'data', + adapters: {}, + time: { + parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + isoWeekday: false, // override week start day - see http://momentjs.com/docs/#/get-set/iso-weekday/ + minUnit: 'millisecond', + displayFormats: {} + }, + realtime: {}, + ticks: { + autoSkip: false, + source: 'auto', + major: { + enabled: true + } + } +}; + +defaults.describe('scale.realtime', { + _scriptable: name => name !== 'onRefresh' +}); diff --git a/static/js/debug-streaming.js b/static/js/debug-streaming.js new file mode 100644 index 0000000..e44e20a --- /dev/null +++ b/static/js/debug-streaming.js @@ -0,0 +1,387 @@ +/** + * 🔧 Script de Diagnóstico Detallado para Chart.js Streaming + * Ejecutar en consola del navegador para identificar el problema exacto + */ + +// Función principal de diagnóstico +window.diagnoseStreamingIssue = async function () { + console.log('🔧 DIAGNÓSTICO DETALLADO DE STREAMING'); + console.log('='.repeat(60)); + + const results = { + librariesLoaded: false, + plotManagerReady: false, + sessionExists: false, + backendData: null, + chartConfig: null, + streamingWorking: false, + errors: [] + }; + + try { + // 1. Verificar librerías básicas + console.log('\n1️⃣ VERIFICANDO LIBRERÍAS...'); + results.librariesLoaded = { + chartjs: typeof Chart !== 'undefined', + chartStreaming: typeof window.ChartStreaming !== 'undefined', + plotManager: typeof plotManager !== 'undefined' + }; + + console.log('Chart.js:', results.librariesLoaded.chartjs ? '✅' : '❌'); + console.log('ChartStreaming:', results.librariesLoaded.chartStreaming ? '✅' : '❌'); + console.log('PlotManager:', results.librariesLoaded.plotManager ? '✅' : '❌'); + + if (!results.librariesLoaded.chartStreaming) { + results.errors.push('ChartStreaming plugin no está cargado'); + console.error('❌ ChartStreaming plugin no está disponible'); + return results; + } + + // 2. Verificar PlotManager + console.log('\n2️⃣ VERIFICANDO PLOT MANAGER...'); + const pm = window.plotManager || plotManager; + if (pm && pm.sessions) { + results.plotManagerReady = true; + console.log('✅ PlotManager inicializado'); + console.log('📊 Sesiones activas:', pm.sessions.size); + + if (pm.sessions.size === 0) { + console.log('⚠️ No hay sesiones de plot activas'); + console.log('💡 Crea un nuevo plot para continuar el diagnóstico'); + return results; + } + } else { + results.errors.push('PlotManager no está inicializado'); + console.error('❌ PlotManager no está disponible'); + console.log('💡 Tip: Espera unos segundos e intenta de nuevo, plotManager podría estar inicializándose'); + return results; + } + + // 3. Verificar sesión específica + console.log('\n3️⃣ ANALIZANDO SESIÓN DE PLOT...'); + const sessionId = Array.from(pm.sessions.keys())[0]; + const sessionData = pm.sessions.get(sessionId); + + if (sessionData) { + results.sessionExists = true; + console.log(`📈 Sesión encontrada: ${sessionId}`); + + // Verificar configuración del chart + results.chartConfig = { + hasChart: !!sessionData.chart, + chartType: sessionData.chart?.config?.type, + scaleType: sessionData.chart?.scales?.x?.type, + scaleConstructor: sessionData.chart?.scales?.x?.constructor?.name, + streamingEnabled: !!sessionData.chart?.$streaming?.enabled, + datasets: sessionData.chart?.data?.datasets?.length || 0, + dataPoints: sessionData.chart?.data?.datasets?.reduce((total, d) => total + (d.data?.length || 0), 0) || 0 + }; + + console.log('Chart Config:', results.chartConfig); + + // Verificar si la escala es realtime + if (results.chartConfig.scaleType !== 'realtime') { + results.errors.push('La escala X no es de tipo realtime'); + console.error('❌ Escala X no es realtime:', results.chartConfig.scaleType); + } else { + console.log('✅ Escala realtime configurada correctamente'); + } + + // Verificar streaming + if (!results.chartConfig.streamingEnabled) { + results.errors.push('Streaming no está habilitado en el chart'); + console.error('❌ Streaming no está habilitado'); + } else { + console.log('✅ Streaming habilitado en el chart'); + } + } + + // 4. Verificar datos del backend + console.log('\n4️⃣ VERIFICANDO DATOS DEL BACKEND...'); + try { + const response = await fetch(`/api/plots/${sessionId}/data`); + results.backendData = await response.json(); + + console.log('📊 Respuesta del backend:', { + success: !!results.backendData.datasets, + datasets: results.backendData.datasets?.length || 0, + totalPoints: results.backendData.data_points_count || 0, + isActive: results.backendData.is_active, + isPaused: results.backendData.is_paused + }); + + if (results.backendData.datasets && results.backendData.datasets.length > 0) { + console.log('✅ Backend devuelve datos'); + + // Verificar estructura de datos + const firstDataset = results.backendData.datasets[0]; + console.log('📈 Primer dataset:', { + label: firstDataset.label, + dataPoints: firstDataset.data?.length || 0, + samplePoint: firstDataset.data?.[0] + }); + + if (firstDataset.data && firstDataset.data.length > 0) { + console.log('✅ Dataset tiene puntos de datos'); + + // Verificar formato de timestamps + const firstPoint = firstDataset.data[0]; + const currentTime = Date.now(); + const timeDiff = Math.abs(currentTime - firstPoint.x); + + console.log('⏰ Análisis de timestamps:', { + firstPointTime: firstPoint.x, + currentTime: currentTime, + differenceMs: timeDiff, + differenceSec: Math.round(timeDiff / 1000), + isRecent: timeDiff < 300000 // 5 minutos + }); + + if (timeDiff > 300000) { + results.errors.push('Los timestamps de los datos son muy antiguos'); + console.warn('⚠️ Los datos parecen ser muy antiguos'); + } + } else { + results.errors.push('El dataset no tiene puntos de datos'); + console.error('❌ El dataset está vacío'); + } + } else { + results.errors.push('Backend no devuelve datasets'); + console.error('❌ Backend no devuelve datos válidos'); + } + } catch (error) { + results.errors.push(`Error al obtener datos del backend: ${error.message}`); + console.error('❌ Error del backend:', error); + } + + // 5. Test de funcionalidad streaming + console.log('\n5️⃣ PROBANDO FUNCIONALIDAD STREAMING...'); + if (sessionData && sessionData.chart && results.backendData) { + try { + // Intentar agregar un punto de prueba + const testResult = window.ChartStreaming.addStreamingData(sessionData.chart, 0, { + x: Date.now(), + y: Math.random() * 100 + }); + + console.log('🧪 Test de addStreamingData:', testResult ? '✅' : '❌'); + + if (testResult) { + // Verificar si el punto se agregó + const currentDataPoints = sessionData.chart.data.datasets[0]?.data?.length || 0; + console.log('📊 Puntos después del test:', currentDataPoints); + results.streamingWorking = currentDataPoints > 0; + } else { + results.errors.push('addStreamingData falló'); + } + } catch (error) { + results.errors.push(`Error en test de streaming: ${error.message}`); + console.error('❌ Error en test de streaming:', error); + } + } + + // 6. Resumen final + console.log('\n6️⃣ RESUMEN DEL DIAGNÓSTICO'); + console.log('='.repeat(40)); + + if (results.errors.length === 0) { + console.log('🎉 No se encontraron errores graves'); + if (!results.streamingWorking) { + console.log('⚠️ Sin embargo, el streaming no parece estar funcionando'); + console.log('💡 Intenta: forceStreamingUpdate()'); + } + } else { + console.log('❌ Errores encontrados:'); + results.errors.forEach((error, index) => { + console.log(` ${index + 1}. ${error}`); + }); + } + + // 7. Recomendaciones + console.log('\n7️⃣ PRÓXIMOS PASOS RECOMENDADOS:'); + + if (!results.librariesLoaded.chartStreaming) { + console.log('🔧 1. Recargar la página para asegurar que el plugin se carga'); + } + + if (results.errors.some(e => e.includes('timestamp'))) { + console.log('🔧 2. Ejecutar clearStreamingData() y reiniciar el plot'); + } + + if (results.backendData && !results.streamingWorking) { + console.log('🔧 3. Ejecutar enablePlotDebug() y forceStreamingUpdate()'); + } + + console.log('🔧 4. Si persiste: plotManager.controlPlot("' + sessionId + '", "stop") y luego "start"'); + + return results; + + } catch (error) { + console.error('💥 Error durante el diagnóstico:', error); + results.errors.push(`Error fatal: ${error.message}`); + return results; + } +}; + +// Función para limpiar y reiniciar streaming +window.resetStreaming = function (sessionId = null) { + console.log('🔄 REINICIANDO STREAMING...'); + + const pm = window.plotManager || plotManager; + if (!pm || !pm.sessions) { + console.error('❌ PlotManager no disponible'); + return false; + } + + const targetSessionId = sessionId || Array.from(pm.sessions.keys())[0]; + + if (!targetSessionId) { + console.error('❌ No hay sesiones disponibles'); + return false; + } + + console.log(`🔄 Reiniciando sesión: ${targetSessionId}`); + + try { + // 1. Limpiar datos existentes + if (pm.clearStreamingData) { + pm.clearStreamingData(targetSessionId); + console.log('✅ Datos de streaming limpiados'); + } + + // 2. Pausar y reiniciar + pm.controlPlot(targetSessionId, 'stop'); + console.log('⏹️ Plot detenido'); + + setTimeout(() => { + pm.controlPlot(targetSessionId, 'start'); + console.log('▶️ Plot reiniciado'); + + // 3. Forzar actualización después de un momento + setTimeout(() => { + if (pm.refreshStreamingData) { + const sessionData = pm.sessions.get(targetSessionId); + if (sessionData?.chart) { + pm.refreshStreamingData(targetSessionId, sessionData.chart); + console.log('🔄 Actualización forzada'); + } + } + }, 1000); + }, 500); + + return true; + } catch (error) { + console.error('❌ Error al reiniciar streaming:', error); + return false; + } +}; + +// Función de test rápido +window.quickStreamingTest = function () { + console.log('⚡ TEST RÁPIDO DE STREAMING'); + console.log('-'.repeat(30)); + + const pm = window.plotManager || plotManager; + const checks = { + plotManager: !!pm, + sessions: pm?.sessions?.size || 0, + chartStreaming: !!window.ChartStreaming + }; + + Object.entries(checks).forEach(([key, value]) => { + console.log(`${key}: ${value ? '✅' : '❌'} ${value}`); + }); + + if (checks.sessions > 0) { + const sessionId = Array.from(pm.sessions.keys())[0]; + console.log(`\n🧪 Probando sesión: ${sessionId}`); + + // Ejecutar diagnóstico completo + return diagnoseStreamingIssue(); + } else { + console.log('\n💡 Crea un plot primero, luego ejecuta: quickStreamingTest()'); + return false; + } +}; + +// Función específica para diagnosticar el onRefresh callback +window.diagnoseOnRefreshIssue = function () { + console.log('🔧 DIAGNÓSTICO ESPECÍFICO DEL ONREFRESH CALLBACK'); + console.log('='.repeat(50)); + + const pm = window.plotManager || plotManager; + if (!pm || !pm.sessions) { + console.error('❌ PlotManager no disponible'); + console.log('💡 Tip: Espera unos segundos e intenta de nuevo'); + return; + } + + if (pm.sessions.size === 0) { + console.log('⚠️ No hay sesiones activas. Crea un plot primero.'); + return; + } + + const sessionId = Array.from(pm.sessions.keys())[0]; + const sessionData = pm.sessions.get(sessionId); + + if (!sessionData || !sessionData.chart) { + console.error('❌ No se encuentra el chart para la sesión'); + return; + } + + const chart = sessionData.chart; + const realTimeScale = chart.scales.x; + + console.log('📊 Información del Chart:'); + console.log('- Chart existe:', !!chart); + console.log('- Chart type:', chart.config.type); + console.log('- Chart scales:', Object.keys(chart.scales)); + + console.log('\n📈 Información de la Escala RealTime:'); + console.log('- Scale type:', realTimeScale.type); + console.log('- Scale constructor:', realTimeScale.constructor.name); + console.log('- Scale options:', realTimeScale.options); + console.log('- Scale realtime options:', realTimeScale.options.realtime); + + if (realTimeScale.realtime) { + console.log('\n⚙️ Configuración RealTime:'); + console.log('- Duration:', realTimeScale.realtime.duration); + console.log('- Refresh:', realTimeScale.realtime.refresh); + console.log('- Pause:', realTimeScale.realtime.pause); + console.log('- onRefresh type:', typeof realTimeScale.realtime.onRefresh); + console.log('- onRefresh value:', realTimeScale.realtime.onRefresh); + + if (typeof realTimeScale.realtime.onRefresh === 'function') { + console.log('✅ onRefresh callback está configurado correctamente'); + + // Test manual del callback + console.log('\n🧪 Probando callback onRefresh manualmente...'); + try { + realTimeScale.realtime.onRefresh(chart); + console.log('✅ Callback onRefresh ejecutado sin errores'); + } catch (error) { + console.error('❌ Error al ejecutar callback onRefresh:', error); + } + } else { + console.error('❌ onRefresh callback NO está configurado como función'); + } + } else { + console.error('❌ No se encontró configuración realtime en la escala'); + } + + // Verificar streaming en chart + if (chart.$streaming) { + console.log('\n🔄 Estado del Streaming:'); + console.log('- Streaming enabled:', chart.$streaming.enabled); + console.log('- Interval ID:', chart.$streaming.intervalId); + console.log('- Interval activo:', !!chart.$streaming.intervalId); + } else { + console.error('❌ No se encontró objeto $streaming en el chart'); + } +}; + +console.log('🔧 Scripts de diagnóstico cargados:'); +console.log('- diagnoseStreamingIssue() - Diagnóstico completo'); +console.log('- resetStreaming() - Reiniciar streaming'); +console.log('- quickStreamingTest() - Test rápido'); +console.log('- diagnoseOnRefreshIssue() - Diagnóstico específico del onRefresh'); \ No newline at end of file diff --git a/static/js/events.js b/static/js/events.js index 2b2a410..0f1e078 100644 --- a/static/js/events.js +++ b/static/js/events.js @@ -4,20 +4,29 @@ // Refrescar log de eventos function refreshEventLog() { - const limit = document.getElementById('log-limit').value; + const limitElement = document.getElementById('log-limit'); + const limit = limitElement ? limitElement.value : 100; fetch(`/api/events?limit=${limit}`) .then(response => response.json()) .then(data => { if (data.success) { - const logContainer = document.getElementById('events-log'); - const logStats = document.getElementById('log-stats'); + const logContainer = document.getElementById('events-container'); + const logStats = document.getElementById('events-count'); + + // Verificar que los elementos existan + if (!logContainer) { + console.warn('Events container not found'); + return; + } // Limpiar entradas existentes logContainer.innerHTML = ''; // Actualizar estadísticas - logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`; + if (logStats) { + logStats.textContent = `${data.showing} of ${data.total_events}`; + } // Añadir eventos (orden inverso para mostrar primero los más nuevos) const events = data.events.reverse(); @@ -103,7 +112,7 @@ function initEventListeners() { } // Función para cargar eventos en el tab de events -window.loadEvents = function() { +window.loadEvents = function () { fetch('/api/events?limit=50') .then(response => response.json()) .then(data => { @@ -111,11 +120,19 @@ window.loadEvents = function() { const eventsContainer = document.getElementById('events-container'); const eventsCount = document.getElementById('events-count'); + // Verificar que los elementos existan + if (!eventsContainer) { + console.warn('Events container not found in loadEvents'); + return; + } + // Limpiar contenedor eventsContainer.innerHTML = ''; // Actualizar contador - eventsCount.textContent = data.showing || 0; + if (eventsCount) { + eventsCount.textContent = data.showing || 0; + } // Añadir eventos (orden inverso para mostrar primero los más nuevos) const events = data.events.reverse(); diff --git a/static/js/plotting.js b/static/js/plotting.js index c3b344b..b10c7e8 100644 --- a/static/js/plotting.js +++ b/static/js/plotting.js @@ -175,13 +175,20 @@ class PlotManager { async updateSessionData(sessionId) { try { + // 🚀 NUEVO: Para streaming, el refreshStreamingData maneja la actualización automática + // Esta función ahora es principalmente para compatibilidad y casos especiales + const sessionData = this.sessions.get(sessionId); + if (!sessionData || !sessionData.chart) { + return; + } + const response = await fetch(`/api/plots/${sessionId}/data`); const plotData = await response.json(); if (plotData.datasets) { // 🔧 DEBUG: Log para troubleshooting if (plotData.datasets.length > 1) { - plotDebugLog(`📈 Plot ${sessionId}: Updating ${plotData.datasets.length} variables, ${plotData.data_points_count} total points`); + plotDebugLog(`📈 Plot ${sessionId}: Manual update ${plotData.datasets.length} variables, ${plotData.data_points_count} total points`); } this.updateChart(sessionId, plotData); } else { @@ -233,83 +240,57 @@ class PlotManager { document.getElementById('plot-sessions-container').appendChild(container); - // Crear Chart.js + // 🚀 NUEVO: Usar chartjs-plugin-streaming para crear chart const ctx = document.getElementById(`chart-${sessionId}`).getContext('2d'); - const chart = new Chart(ctx, { - type: 'line', - data: { - datasets: [] - }, - options: { - responsive: true, - maintainAspectRatio: false, - animation: { - duration: 0 // Sin animación para mejor performance - }, - scales: { - x: { - type: 'time', - time: { - unit: 'second', - displayFormats: { - second: 'HH:mm:ss' - } - }, - title: { - display: true, - text: 'Time' - } - }, - y: { - title: { - display: true, - text: 'Value' - } - } - }, - plugins: { - legend: { - display: true, - position: 'top' - }, - tooltip: { - mode: 'index', - intersect: false - } - }, - interaction: { - mode: 'nearest', - axis: 'x', - intersect: false - } + + // Configurar streaming con la nueva librería + const streamingConfig = window.ChartStreaming.createStreamingChartConfig({ + duration: (config.time_window || 60) * 1000, // Convertir a milisegundos + refresh: 500, // Actualizar cada 500ms + frameRate: 30, + pause: false, + yMin: config.y_min, + yMax: config.y_max, + onRefresh: (chart) => { + // Esta función se llama automáticamente para obtener nuevos datos + this.refreshStreamingData(sessionId, chart); } }); - this.sessions.set(sessionId, chart); + const chart = new Chart(ctx, streamingConfig); - // Actualizar datos iniciales - this.updateSessionData(sessionId); + this.sessions.set(sessionId, { + chart: chart, + config: config, + lastDataFetch: 0, + datasets: new Map() // Para tracking de datasets por variable + }); - console.log(`📈 Created plot session: ${sessionId}`); + // Inicializar datasets para las variables del plot + this.initializeStreamingDatasets(sessionId, config); + + console.log(`📈 Created streaming plot session: ${sessionId}`); } updateChart(sessionId, plotData) { - const chart = this.sessions.get(sessionId); - if (!chart) { + const sessionData = this.sessions.get(sessionId); + if (!sessionData || !sessionData.chart) { console.warn(`📈 Plot ${sessionId}: Chart not found in sessions`); return; } + const chart = sessionData.chart; + // 🔧 DEBUG: Verificar datos antes de actualizar if (plotData.datasets && plotData.datasets.length > 0) { - plotDebugLog(`📈 Plot ${sessionId}: Updating chart with ${plotData.datasets.length} datasets`); + plotDebugLog(`📈 Plot ${sessionId}: Updating streaming chart with ${plotData.datasets.length} datasets`); plotData.datasets.forEach((dataset, idx) => { plotDebugLog(` - Variable ${idx + 1}: ${dataset.label} (${dataset.data.length} points)`); }); } - // Actualizar datasets - chart.data.datasets = plotData.datasets || []; + // 🚀 NUEVO: Para streaming, agregamos nuevos datos en lugar de reemplazar todo + this.updateStreamingData(sessionId, plotData); // Actualizar escalas Y - mejore el auto-scaling if (plotData.y_min !== undefined && plotData.y_max !== undefined) { @@ -330,7 +311,7 @@ class PlotManager { // 🔧 DEBUG: Log si el contador no coincide const calculatedPoints = (plotData.datasets || []).reduce((sum, dataset) => sum + (dataset.data?.length || 0), 0); if (totalPoints !== calculatedPoints) { - console.warn(`📈 Plot ${sessionId}: Points mismatch - reported: ${totalPoints}, calculated: ${calculatedPoints}`); + plotDebugLog(`📈 Plot ${sessionId}: Points mismatch - reported: ${totalPoints}, calculated: ${calculatedPoints}`); } } @@ -346,12 +327,20 @@ class PlotManager { }); } - // Actualizar chart - chart.update('none'); + // El chart de streaming se actualiza automáticamente, no necesitamos llamar update manualmente } async controlPlot(sessionId, action) { try { + // 🚀 NUEVO: Controlar streaming localmente para algunas acciones + if (action === 'pause') { + this.setStreamingPause(sessionId, true); + } else if (action === 'start') { + this.setStreamingPause(sessionId, false); + } else if (action === 'clear') { + this.clearStreamingData(sessionId); + } + const response = await fetch(`/api/plots/${sessionId}/control`, { method: 'POST', headers: { @@ -366,8 +355,15 @@ class PlotManager { // 🔑 NUEVO: Actualizar status inmediatamente después de la acción await this.updateSessionStatus(sessionId); - // Actualizar datos inmediatamente - await this.updateSessionData(sessionId); + // Para stop, también pausar el streaming + if (action === 'stop') { + this.setStreamingPause(sessionId, true); + } + + // Para start, asegurar que el streaming esté activo + if (action === 'start') { + this.setStreamingPause(sessionId, false); + } showNotification(result.message, 'success'); } else { @@ -387,6 +383,17 @@ class PlotManager { if (data.success && data.config) { this.updatePlotStats(sessionId, data.config); + + // 🚀 NUEVO: Actualizar estado de streaming basado en el status del backend + const sessionData = this.sessions.get(sessionId); + if (sessionData) { + // Actualizar configuración local + sessionData.config = data.config; + + // Controlar pausa del streaming basado en el estado del plot + const shouldPause = !data.config.is_active || data.config.is_paused; + this.setStreamingPause(sessionId, shouldPause); + } } } catch (error) { console.error(`Error updating session status ${sessionId}:`, error); @@ -415,9 +422,9 @@ class PlotManager { removeChart(sessionId) { // Destruir Chart.js - const chart = this.sessions.get(sessionId); - if (chart) { - chart.destroy(); + const sessionData = this.sessions.get(sessionId); + if (sessionData && sessionData.chart) { + sessionData.chart.destroy(); this.sessions.delete(sessionId); } @@ -428,70 +435,266 @@ class PlotManager { } } + // 🚀 NUEVAS FUNCIONES PARA STREAMING + + /** + * Inicializa los datasets de streaming para las variables del plot + */ + initializeStreamingDatasets(sessionId, config) { + const sessionData = this.sessions.get(sessionId); + if (!sessionData || !sessionData.chart || !config.variables) { + plotDebugLog(`📈 Plot ${sessionId}: Cannot initialize datasets - missing data`); + return; + } + + const chart = sessionData.chart; + const datasets = []; + + plotDebugLog(`📈 Plot ${sessionId}: Initializing ${config.variables.length} streaming datasets`); + + config.variables.forEach((variable, index) => { + const color = this.getColor(variable, index); + const dataset = { + label: variable, + data: [], + borderColor: color, + backgroundColor: color + '20', // Color con transparencia + borderWidth: 2, + fill: false, + pointRadius: 0, + pointHoverRadius: 3, + tension: 0.1 + }; + + datasets.push(dataset); + sessionData.datasets.set(variable, index); + + plotDebugLog(`📈 Plot ${sessionId}: Dataset ${index}: ${variable} (color: ${color})`); + }); + + // Inicializar timestamps tracking + sessionData.lastTimestamps = {}; + + chart.data.datasets = datasets; + chart.update('quiet'); + + plotDebugLog(`📈 Plot ${sessionId}: Successfully initialized ${datasets.length} streaming datasets`); + + // 🔧 DEBUG: Verificar configuración del chart + plotDebugLog(`📈 Plot ${sessionId}: Chart config:`, { + type: chart.config.type, + scaleType: chart.scales.x?.type, + hasRealTimeScale: chart.scales.x?.constructor?.name, + streamingEnabled: !!chart.$streaming?.enabled + }); + } + + /** + * Función llamada automáticamente por chartjs-plugin-streaming para obtener nuevos datos + */ + async refreshStreamingData(sessionId, chart) { + try { + // Evitar llamadas muy frecuentes + const sessionData = this.sessions.get(sessionId); + if (!sessionData) return; + + const now = Date.now(); + if (now - sessionData.lastDataFetch < 800) { // Mínimo 800ms entre llamadas + return; + } + sessionData.lastDataFetch = now; + + // 🔧 DEBUG: Log de llamada + plotDebugLog(`📈 Plot ${sessionId}: Fetching data from backend...`); + + // Obtener datos del backend + const response = await fetch(`/api/plots/${sessionId}/data`); + const plotData = await response.json(); + + plotDebugLog(`📈 Plot ${sessionId}: Received data:`, plotData); + + if (plotData && plotData.datasets && plotData.datasets.length > 0) { + // Agregar los nuevos puntos de datos al chart de streaming + this.processBackendDataForStreaming(sessionId, plotData); + + // Actualizar contador de puntos + const pointsElement = document.getElementById(`points-${sessionId}`); + if (pointsElement) { + pointsElement.textContent = plotData.data_points_count || 0; + } + } else { + plotDebugLog(`📈 Plot ${sessionId}: No datasets received or empty data`); + } + + } catch (error) { + console.error(`Error refreshing streaming data for ${sessionId}:`, error); + } + } + + /** + * Actualiza los datos de streaming del chart + */ + updateStreamingData(sessionId, plotData) { + const sessionData = this.sessions.get(sessionId); + if (!sessionData || !sessionData.chart || !plotData.datasets) { + return; + } + + const chart = sessionData.chart; + + // Para cada dataset del backend, agregar los nuevos puntos + plotData.datasets.forEach((backendDataset, index) => { + if (chart.data.datasets[index] && backendDataset.data) { + const chartDataset = chart.data.datasets[index]; + + // Obtener los últimos puntos que no tengamos aún + const existingPoints = chartDataset.data.length; + const newPoints = backendDataset.data.slice(existingPoints); + + newPoints.forEach(point => { + window.ChartStreaming.addStreamingData(chart, index, { + x: point.x, + y: point.y + }); + }); + + plotDebugLog(`📈 Plot ${sessionId}: Added ${newPoints.length} new points to dataset ${index}`); + } + }); + } + + /** + * Procesa datos del backend para el chart de streaming + */ + processBackendDataForStreaming(sessionId, plotData) { + const sessionData = this.sessions.get(sessionId); + if (!sessionData || !sessionData.chart || !plotData.datasets) { + return; + } + + const chart = sessionData.chart; + + // Asegurar que tenemos tracking de los últimos timestamps por dataset + if (!sessionData.lastTimestamps) { + sessionData.lastTimestamps = {}; + } + + plotDebugLog(`📈 Plot ${sessionId}: Processing ${plotData.datasets.length} datasets for streaming`); + + // Para cada dataset del backend + plotData.datasets.forEach((backendDataset, index) => { + const variableName = backendDataset.label; + + if (!backendDataset.data || backendDataset.data.length === 0) { + plotDebugLog(`📈 Plot ${sessionId}: Dataset ${variableName} has no data`); + return; + } + + // Obtener el último timestamp que agregamos para esta variable + const lastTimestamp = sessionData.lastTimestamps[variableName] || 0; + + // Filtrar solo los puntos nuevos (timestamp mayor al último que agregamos) + const newPoints = backendDataset.data.filter(point => point.x > lastTimestamp); + + if (newPoints.length > 0) { + plotDebugLog(`📈 Plot ${sessionId}: Adding ${newPoints.length} new points for ${variableName}`); + + // Agregar cada nuevo punto al chart de streaming + newPoints.forEach(point => { + if (point.y !== null && point.y !== undefined) { + window.ChartStreaming.addStreamingData(chart, index, { + x: point.x, // Usar timestamp del backend + y: point.y + }); + } + }); + + // Actualizar el último timestamp procesado para esta variable + const latestPoint = newPoints[newPoints.length - 1]; + sessionData.lastTimestamps[variableName] = latestPoint.x; + + plotDebugLog(`📈 Plot ${sessionId}: Updated last timestamp for ${variableName} to ${latestPoint.x}`); + } else { + plotDebugLog(`📈 Plot ${sessionId}: No new points for ${variableName} (last: ${lastTimestamp})`); + } + }); + } + + /** + * Controla la pausa/reanudación del streaming + */ + setStreamingPause(sessionId, paused) { + const sessionData = this.sessions.get(sessionId); + if (sessionData && sessionData.chart) { + window.ChartStreaming.setStreamingPause(sessionData.chart, paused); + plotDebugLog(`📈 Plot ${sessionId}: Streaming ${paused ? 'paused' : 'resumed'}`); + } + } + + /** + * Limpia todos los datos de streaming + */ + clearStreamingData(sessionId) { + const sessionData = this.sessions.get(sessionId); + if (sessionData && sessionData.chart) { + window.ChartStreaming.clearStreamingData(sessionData.chart); + plotDebugLog(`📈 Plot ${sessionId}: Streaming data cleared`); + } + } + async createPlotSessionTab(sessionId, sessionInfo) { // Crear tab dinámico para el plot if (typeof tabManager !== 'undefined') { tabManager.createPlotTab(sessionId, sessionInfo.name || `Plot ${sessionId}`); } - // Crear el Chart.js en el canvas del tab + // 🚀 NUEVO: Obtener configuración completa del plot para el streaming + let plotConfig = sessionInfo; + try { + const configResponse = await fetch(`/api/plots/${sessionId}/config`); + const configData = await configResponse.json(); + if (configData.success && configData.config) { + plotConfig = configData.config; + } + } catch (error) { + console.warn(`Could not load plot config for ${sessionId}, using basic info`); + } + + // Crear el Chart.js con streaming en el canvas del tab const ctx = document.getElementById(`chart-${sessionId}`).getContext('2d'); - const chart = new Chart(ctx, { - type: 'line', - data: { - datasets: [] - }, - options: { - responsive: true, - maintainAspectRatio: false, - animation: { - duration: 0 // Sin animación para mejor performance - }, - scales: { - x: { - type: 'time', - time: { - unit: 'second', - displayFormats: { - second: 'HH:mm:ss' - } - }, - title: { - display: true, - text: 'Time' - } - }, - y: { - title: { - display: true, - text: 'Value' - } - } - }, - plugins: { - legend: { - display: true, - position: 'top' - }, - tooltip: { - mode: 'index', - intersect: false - } - }, - interaction: { - mode: 'nearest', - axis: 'x', - intersect: false - } + + // Configurar streaming con la nueva librería + const streamingConfig = window.ChartStreaming.createStreamingChartConfig({ + duration: (plotConfig.time_window || 60) * 1000, // Convertir a milisegundos + refresh: 500, // Actualizar cada 500ms + frameRate: 30, + pause: !plotConfig.is_active, // Pausar si no está activo + yMin: plotConfig.y_min, + yMax: plotConfig.y_max, + onRefresh: (chart) => { + // Esta función se llama automáticamente para obtener nuevos datos + this.refreshStreamingData(sessionId, chart); } }); - this.sessions.set(sessionId, chart); + const chart = new Chart(ctx, streamingConfig); + + this.sessions.set(sessionId, { + chart: chart, + config: plotConfig, + lastDataFetch: 0, + datasets: new Map() // Para tracking de datasets por variable + }); + + // Inicializar datasets para las variables del plot + if (plotConfig.variables) { + this.initializeStreamingDatasets(sessionId, plotConfig); + } // Actualizar estadísticas del plot this.updatePlotStats(sessionId, sessionInfo); - console.log(`📈 Created plot tab and chart for session: ${sessionId}`); + console.log(`📈 Created streaming plot tab and chart for session: ${sessionId}`); } updatePlotStats(sessionId, sessionInfo) { @@ -1205,8 +1408,10 @@ class PlotManager { this.stopAutoUpdate(); // Destruir todos los charts - for (const [sessionId, chart] of this.sessions) { - chart.destroy(); + for (const [sessionId, sessionData] of this.sessions) { + if (sessionData && sessionData.chart) { + sessionData.chart.destroy(); + } } this.sessions.clear(); @@ -1214,6 +1419,81 @@ class PlotManager { } } +// 🔧 NUEVAS FUNCIONES DE DEBUG Y VERIFICACIÓN + +/** + * Verifica que chartjs-plugin-streaming esté cargado correctamente + */ +window.verifyStreamingIntegration = function () { + console.log('🧪 Verificando integración de Chart.js Streaming...'); + + const checks = { + chartjsLoaded: typeof Chart !== 'undefined', + streamingLoaded: typeof window.ChartStreaming !== 'undefined', + plotManagerLoaded: typeof plotManager !== 'undefined', + activeStreamingSessions: 0 + }; + + console.log('✅ Chart.js cargado:', checks.chartjsLoaded); + console.log('✅ ChartStreaming cargado:', checks.streamingLoaded); + console.log('✅ PlotManager cargado:', checks.plotManagerLoaded); + + if (checks.plotManagerLoaded && plotManager.sessions) { + checks.activeStreamingSessions = plotManager.sessions.size; + console.log('✅ Sesiones de streaming activas:', checks.activeStreamingSessions); + + // Verificar cada sesión + for (const [sessionId, sessionData] of plotManager.sessions) { + console.log(`📈 Sesión ${sessionId}:`, { + hasChart: !!sessionData.chart, + chartType: sessionData.chart?.config?.type, + scaleType: sessionData.chart?.scales?.x?.type, + datasets: sessionData.chart?.data?.datasets?.length || 0, + lastTimestamps: sessionData.lastTimestamps, + streaming: !!sessionData.chart?.$streaming?.enabled + }); + } + } + + if (checks.streamingLoaded) { + console.log('✅ ChartStreaming functions:', Object.keys(window.ChartStreaming)); + + // Test de configuración + try { + const testConfig = window.ChartStreaming.createStreamingChartConfig({ + duration: 10000, + refresh: 1000 + }); + console.log('✅ Test configuración streaming:', !!testConfig); + } catch (e) { + console.log('❌ Error en test configuración:', e.message); + } + } + + return checks; +}; + +/** + * Fuerza una actualización de datos para todas las sesiones + */ +window.forceStreamingUpdate = function () { + if (!plotManager || !plotManager.sessions) { + console.log('❌ PlotManager no disponible'); + return; + } + + console.log('🔄 Forzando actualización de streaming...'); + + for (const [sessionId, sessionData] of plotManager.sessions) { + console.log(`🔄 Actualizando sesión ${sessionId}...`); + + if (sessionData.chart) { + // Forzar refresh de datos + plotManager.refreshStreamingData(sessionId, sessionData.chart); + } + } +}; + // Función global para toggle de trigger config window.togglePlotFormTriggerConfig = function () { const triggerEnabled = document.getElementById('plot-form-trigger-enabled'); @@ -1298,8 +1578,38 @@ window.showNotification = function (message, type = 'info') { let plotManager = null; document.addEventListener('DOMContentLoaded', function () { + console.log('📈 Initializing Plot Manager with streaming support...'); + + // Verificar que chartjs-plugin-streaming esté cargado + if (typeof window.ChartStreaming === 'undefined') { + console.error('❌ ChartStreaming plugin not loaded! The streaming functionality will not work.'); + console.log('💡 Make sure chartjs-plugin-streaming.js is loaded before plotting.js'); + } else { + console.log('✅ ChartStreaming plugin loaded successfully'); + console.log('Available functions:', Object.keys(window.ChartStreaming)); + } + + // Exportar clase PlotManager globalmente + window.PlotManager = PlotManager; + // Inicializar Plot Manager - plotManager = new PlotManager(); + window.plotManager = new PlotManager(); + + // Para compatibilidad + plotManager = window.plotManager; + + // Auto-verificar integración después de un breve delay + setTimeout(() => { + window.verifyStreamingIntegration(); + + // Si hay plots activos, mostrar información de debugging + if (plotManager.sessions.size > 0) { + console.log('📈 Active plot sessions detected. To debug:'); + console.log('- enablePlotDebug() - Enable detailed logging'); + console.log('- forceStreamingUpdate() - Force data update'); + console.log('- verifyStreamingIntegration() - Check system status'); + } + }, 2000); // Cerrar modales con Escape (pero prevenir cierre accidental) document.addEventListener('keydown', function (e) { diff --git a/static/js/quick-fix-test.js b/static/js/quick-fix-test.js new file mode 100644 index 0000000..3ad26de --- /dev/null +++ b/static/js/quick-fix-test.js @@ -0,0 +1,133 @@ +/** + * 🧪 Test Rápido de Verificación Post-Fix + * Ejecutar después de cargar la página para verificar que todo funciona + */ + +window.quickFixTest = function () { + console.log('🧪 TEST RÁPIDO POST-FIX'); + console.log('='.repeat(40)); + + const results = { + duplicatedElements: false, + plotManagerAvailable: false, + eventsWorking: false, + chartStreamingLoaded: false, + debugFunctionsLoaded: false + }; + + // 1. Verificar elementos duplicados + console.log('\n1️⃣ VERIFICANDO ELEMENTOS DUPLICADOS...'); + const eventsContainers = document.querySelectorAll('#events-container'); + const refreshBtns = document.querySelectorAll('#refresh-events-btn'); + + console.log(`- events-container: ${eventsContainers.length} (esperado: 1)`); + console.log(`- refresh-events-btn: ${refreshBtns.length} (esperado: 1)`); + + results.duplicatedElements = eventsContainers.length === 1 && refreshBtns.length === 1; + console.log(results.duplicatedElements ? '✅ Sin duplicados' : '❌ Hay elementos duplicados'); + + // 2. Verificar PlotManager + console.log('\n2️⃣ VERIFICANDO PLOTMANAGER...'); + const pm = window.plotManager || plotManager; + results.plotManagerAvailable = !!pm; + + console.log(`- window.plotManager: ${!!window.plotManager}`); + console.log(`- plotManager global: ${!!plotManager}`); + console.log(`- PlotManager class: ${!!window.PlotManager}`); + console.log(results.plotManagerAvailable ? '✅ PlotManager disponible' : '❌ PlotManager no disponible'); + + // 3. Verificar Events + console.log('\n3️⃣ VERIFICANDO EVENTS...'); + const eventsContainer = document.getElementById('events-container'); + const eventsCount = document.getElementById('events-count'); + + console.log(`- events-container existe: ${!!eventsContainer}`); + console.log(`- events-count existe: ${!!eventsCount}`); + + results.eventsWorking = !!eventsContainer && !!eventsCount; + console.log(results.eventsWorking ? '✅ Elements de events OK' : '❌ Faltan elementos de events'); + + // 4. Verificar Chart.js Streaming + console.log('\n4️⃣ VERIFICANDO CHART.JS STREAMING...'); + results.chartStreamingLoaded = !!window.ChartStreaming; + + if (results.chartStreamingLoaded) { + const functions = Object.keys(window.ChartStreaming); + console.log(`✅ ChartStreaming cargado con ${functions.length} funciones`); + console.log(`- Funciones: ${functions.join(', ')}`); + } else { + console.log('❌ ChartStreaming no cargado'); + } + + // 5. Verificar funciones de debug + console.log('\n5️⃣ VERIFICANDO FUNCIONES DE DEBUG...'); + const debugFunctions = [ + 'diagnoseStreamingIssue', + 'resetStreaming', + 'quickStreamingTest', + 'diagnoseOnRefreshIssue' + ]; + + const availableFunctions = debugFunctions.filter(fn => typeof window[fn] === 'function'); + results.debugFunctionsLoaded = availableFunctions.length === debugFunctions.length; + + console.log(`✅ Funciones cargadas: ${availableFunctions.join(', ')}`); + if (availableFunctions.length < debugFunctions.length) { + const missing = debugFunctions.filter(fn => typeof window[fn] !== 'function'); + console.log(`❌ Funciones faltantes: ${missing.join(', ')}`); + } + + // 6. Resumen + console.log('\n6️⃣ RESUMEN FINAL'); + console.log('='.repeat(30)); + + const allGood = Object.values(results).every(Boolean); + + if (allGood) { + console.log('🎉 ¡TODOS LOS PROBLEMAS ARREGLADOS!'); + console.log('✅ Ya puedes crear plots y usar el diagnóstico'); + console.log(''); + console.log('📋 Próximos pasos:'); + console.log('1. Ve a la pestaña "Real-Time Plotting"'); + console.log('2. Crea un nuevo plot'); + console.log('3. Ejecuta diagnoseOnRefreshIssue() para verificar streaming'); + } else { + console.log('⚠️ Algunos problemas persisten:'); + Object.entries(results).forEach(([key, value]) => { + if (!value) { + console.log(` ❌ ${key}`); + } + }); + console.log(''); + console.log('💡 Intenta recargar la página (F5) y ejecutar quickFixTest() de nuevo'); + } + + return results; +}; + +// Test básico de events +window.testEventsIntegration = function () { + console.log('🧪 PROBANDO INTEGRACIÓN DE EVENTS...'); + + try { + refreshEventLog(); + console.log('✅ refreshEventLog() ejecutado sin errores'); + + // Test loadEvents + if (typeof window.loadEvents === 'function') { + window.loadEvents(); + console.log('✅ loadEvents() ejecutado sin errores'); + } else { + console.log('❌ loadEvents() no disponible'); + } + + return true; + } catch (error) { + console.error('❌ Error en events:', error); + return false; + } +}; + +console.log('🧪 Funciones de test cargadas:'); +console.log('- quickFixTest() - Verificación completa post-fix'); +console.log('- testEventsIntegration() - Test específico de events'); \ No newline at end of file diff --git a/static/js/test-streaming.js b/static/js/test-streaming.js new file mode 100644 index 0000000..c44a12c --- /dev/null +++ b/static/js/test-streaming.js @@ -0,0 +1,216 @@ +/** + * 🧪 Script de prueba para Chart.js Plugin Streaming + * Ejecutar en consola del navegador para verificar integración + */ + +// Test básico de disponibilidad +function testStreamingAvailability() { + console.log('🧪 Testing Chart.js Streaming Plugin availability...'); + + let results = { + chartjsLoaded: typeof Chart !== 'undefined', + streamingLoaded: typeof window.ChartStreaming !== 'undefined', + realTimeScaleRegistered: false, + streamingPluginRegistered: false + }; + + // Verificar que Chart.js esté cargado + if (results.chartjsLoaded) { + console.log('✅ Chart.js is loaded'); + + // Verificar escala realtime + try { + Chart.register(window.ChartStreaming.RealTimeScale); + results.realTimeScaleRegistered = true; + console.log('✅ RealTime scale is available'); + } catch (e) { + console.log('❌ RealTime scale registration failed:', e.message); + } + + // Verificar plugin de streaming + try { + Chart.register(window.ChartStreaming.streamingPlugin); + results.streamingPluginRegistered = true; + console.log('✅ Streaming plugin is available'); + } catch (e) { + console.log('❌ Streaming plugin registration failed:', e.message); + } + } else { + console.log('❌ Chart.js is not loaded'); + } + + // Verificar ChartStreaming global + if (results.streamingLoaded) { + console.log('✅ ChartStreaming global object is available'); + console.log('Available functions:', Object.keys(window.ChartStreaming)); + } else { + console.log('❌ ChartStreaming global object is not available'); + } + + return results; +} + +// Test de creación de configuración +function testStreamingConfig() { + console.log('🧪 Testing streaming configuration creation...'); + + if (typeof window.ChartStreaming === 'undefined') { + console.log('❌ ChartStreaming not available'); + return false; + } + + try { + const config = window.ChartStreaming.createStreamingChartConfig({ + duration: 30000, + refresh: 1000, + yMin: 0, + yMax: 100 + }); + + console.log('✅ Streaming configuration created successfully'); + console.log('Config structure:', { + type: config.type, + hasRealTimeScale: config.options.scales.x.type === 'realtime', + duration: config.options.scales.x.realtime.duration, + refresh: config.options.scales.x.realtime.refresh + }); + + return true; + } catch (e) { + console.log('❌ Failed to create streaming configuration:', e.message); + return false; + } +} + +// Test de creación de chart (requiere canvas) +function testStreamingChart() { + console.log('🧪 Testing streaming chart creation...'); + + // Buscar un canvas existente o crear uno temporal + let canvas = document.querySelector('canvas'); + let isTemporary = false; + + if (!canvas) { + console.log('Creating temporary canvas for testing...'); + canvas = document.createElement('canvas'); + canvas.id = 'test-streaming-canvas'; + canvas.style.display = 'none'; + document.body.appendChild(canvas); + isTemporary = true; + } + + try { + const ctx = canvas.getContext('2d'); + const config = window.ChartStreaming.createStreamingChartConfig({ + duration: 10000, + refresh: 2000 + }); + + const chart = new Chart(ctx, config); + + console.log('✅ Streaming chart created successfully'); + console.log('Chart info:', { + id: chart.id, + type: chart.config.type, + hasStreamingPlugin: chart.config.plugins.includes('streaming'), + scaleType: chart.scales.x.type + }); + + // Cleanup + chart.destroy(); + if (isTemporary) { + canvas.remove(); + } + + return true; + } catch (e) { + console.log('❌ Failed to create streaming chart:', e.message); + + // Cleanup en caso de error + if (isTemporary && canvas.parentNode) { + canvas.remove(); + } + + return false; + } +} + +// Test completo +function runStreamingTests() { + console.log('🚀 Starting Chart.js Streaming Plugin Integration Tests'); + console.log('='.repeat(60)); + + const results = { + availability: testStreamingAvailability(), + config: testStreamingConfig(), + chart: testStreamingChart() + }; + + console.log('='.repeat(60)); + console.log('📊 Test Results Summary:'); + console.log('- Availability:', results.availability.chartjsLoaded && results.availability.streamingLoaded ? '✅' : '❌'); + console.log('- Configuration:', results.config ? '✅' : '❌'); + console.log('- Chart Creation:', results.chart ? '✅' : '❌'); + + const allPassed = results.availability.chartjsLoaded && + results.availability.streamingLoaded && + results.config && + results.chart; + + if (allPassed) { + console.log('🎉 All tests passed! Chart.js Streaming Plugin is ready to use.'); + } else { + console.log('⚠️ Some tests failed. Check the logs above for details.'); + } + + return results; +} + +// Test específico para la aplicación PLC +function testPlotManagerIntegration() { + console.log('🧪 Testing PlotManager integration...'); + + if (typeof plotManager === 'undefined') { + console.log('❌ PlotManager not available (might not be on plotting tab)'); + return false; + } + + console.log('✅ PlotManager is available'); + console.log('PlotManager info:', { + isInitialized: plotManager.isInitialized, + sessionsCount: plotManager.sessions.size, + colorsAvailable: plotManager.colors.length + }); + + // Verificar métodos de streaming + const streamingMethods = [ + 'setStreamingPause', + 'clearStreamingData', + 'refreshStreamingData', + 'initializeStreamingDatasets' + ]; + + const availableMethods = streamingMethods.filter(method => + typeof plotManager[method] === 'function' + ); + + console.log('Available streaming methods:', availableMethods); + + return availableMethods.length === streamingMethods.length; +} + +// Hacer funciones disponibles globalmente para testing manual +if (typeof window !== 'undefined') { + window.testStreamingAvailability = testStreamingAvailability; + window.testStreamingConfig = testStreamingConfig; + window.testStreamingChart = testStreamingChart; + window.runStreamingTests = runStreamingTests; + window.testPlotManagerIntegration = testPlotManagerIntegration; + + console.log('🧪 Streaming test functions loaded. Available commands:'); + console.log('- testStreamingAvailability()'); + console.log('- testStreamingConfig()'); + console.log('- testStreamingChart()'); + console.log('- runStreamingTests()'); + console.log('- testPlotManagerIntegration()'); +} \ No newline at end of file diff --git a/system_state.json b/system_state.json index 23d7449..38b4a7d 100644 --- a/system_state.json +++ b/system_state.json @@ -7,5 +7,5 @@ ] }, "auto_recovery_enabled": true, - "last_update": "2025-07-21T14:43:41.538809" + "last_update": "2025-07-21T18:34:08.746917" } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4505320..500c835 100644 --- a/templates/index.html +++ b/templates/index.html @@ -669,32 +669,6 @@ - - - -
📋 Event Logging: Monitor system events, errors, and operational status
-🔍 Real-time Updates: Events are automatically updated as they occur
-📊 Filtering: Filter events by type and time range
-Loading events...
-