diff --git a/application_events.json b/application_events.json
index 8e4fca0..427fe0c 100644
--- a/application_events.json
+++ b/application_events.json
@@ -8170,8 +8170,15 @@
"trigger_variable": null,
"auto_started": true
}
+ },
+ {
+ "timestamp": "2025-08-16T00:53:19.221218",
+ "level": "info",
+ "event_type": "application_started",
+ "message": "Application initialization completed successfully",
+ "details": {}
}
],
- "last_updated": "2025-08-15T23:05:23.049195",
- "total_entries": 678
+ "last_updated": "2025-08-16T00:53:19.221218",
+ "total_entries": 679
}
\ No newline at end of file
diff --git a/frontend/src/components/ChartjsHistoricalPlot.jsx b/frontend/src/components/ChartjsHistoricalPlot.jsx
new file mode 100644
index 0000000..1081c22
--- /dev/null
+++ b/frontend/src/components/ChartjsHistoricalPlot.jsx
@@ -0,0 +1,430 @@
+import React, { useRef, useEffect, useState, useCallback } from 'react';
+import { Box, Text, Switch, FormLabel, HStack, useColorModeValue } from '@chakra-ui/react';
+
+// Historical Chart.js Plot Component with Zoom and Pan
+const ChartjsHistoricalPlot = ({ session, height = '400px' }) => {
+ const canvasRef = useRef(null);
+ const chartRef = useRef(null);
+ const sessionDataRef = useRef({
+ sessionId: null,
+ currentTimeRange: { start: null, end: null },
+ pendingDataRequest: false
+ });
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [dataPointsCount, setDataPointsCount] = useState(0);
+ const [isZoomEnabled, setIsZoomEnabled] = useState(true);
+ const [currentRange, setCurrentRange] = useState('');
+
+ const bgColor = useColorModeValue('white', 'gray.800');
+ const textColor = useColorModeValue('gray.600', 'gray.300');
+
+ // Check if Chart.js is available
+ const checkChartDependencies = useCallback(() => {
+ try {
+ if (typeof window === 'undefined' || !window.Chart) {
+ console.error('❌ Chart.js not loaded');
+ return false;
+ }
+ return true;
+ } catch (error) {
+ console.error('❌ Chart.js dependency check failed:', error);
+ return false;
+ }
+ }, []);
+
+ // Destroy chart helper
+ const destroyChart = useCallback(() => {
+ if (chartRef.current) {
+ try {
+ chartRef.current.destroy();
+ } catch (error) {
+ console.warn('Chart destroy error (non-critical):', error);
+ }
+ chartRef.current = null;
+ }
+ }, []);
+
+ // Create historical chart
+ const createChart = useCallback(() => {
+ if (!canvasRef.current || !checkChartDependencies()) {
+ setError('Chart.js not available');
+ return;
+ }
+
+ destroyChart();
+
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) {
+ setError('Cannot get canvas context');
+ return;
+ }
+
+ try {
+ // Initialize empty datasets
+ const datasets = (session.config.variables || []).map((variable, index) => ({
+ label: variable.name || variable,
+ data: [],
+ borderColor: `hsl(${(index * 137.5) % 360}, 70%, 50%)`,
+ backgroundColor: `hsla(${(index * 137.5) % 360}, 70%, 50%, 0.1)`,
+ borderWidth: 2,
+ fill: session.config.stacked || false,
+ tension: session.config.line_tension || 0.4,
+ pointRadius: session.config.point_radius || 1,
+ pointHoverRadius: session.config.point_hover_radius || 4,
+ stepped: session.config.stepped || false
+ }));
+
+ const config = {
+ type: 'line',
+ data: { datasets },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false, // Disable animations for better performance
+ interaction: {
+ mode: 'index',
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top',
+ labels: {
+ usePointStyle: true,
+ padding: 10
+ }
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false,
+ callbacks: {
+ title: function(context) {
+ if (context[0] && context[0].parsed && context[0].parsed.x) {
+ return new Date(context[0].parsed.x).toLocaleString();
+ }
+ return '';
+ }
+ }
+ },
+ zoom: isZoomEnabled && window.Chart?.registry?.plugins?.get?.('zoom') ? {
+ pan: {
+ enabled: true,
+ mode: 'x',
+ onPanComplete: handlePanComplete
+ },
+ zoom: {
+ wheel: {
+ enabled: true,
+ },
+ pinch: {
+ enabled: true,
+ },
+ mode: 'x',
+ onZoomComplete: handleZoomComplete
+ }
+ } : {}
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ displayFormats: {
+ millisecond: 'HH:mm:ss.SSS',
+ second: 'HH:mm:ss',
+ minute: 'HH:mm',
+ hour: 'MM/DD HH:mm',
+ day: 'MM/DD',
+ week: 'MM/DD',
+ month: 'MM/YY',
+ quarter: 'MM/YY',
+ year: 'YYYY'
+ }
+ },
+ title: {
+ display: true,
+ text: 'Time'
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Value'
+ },
+ min: session.config.y_min,
+ max: session.config.y_max
+ }
+ }
+ }
+ };
+
+ chartRef.current = new window.Chart(ctx, config);
+
+ // Register chart controls
+ if (session.onChartReady) {
+ session.onChartReady({
+ loadHistoricalRange: loadHistoricalRange,
+ refreshData: refreshCurrentRange,
+ getTimeRange: getCurrentTimeRange,
+ resetZoom: resetZoom
+ });
+ }
+
+ console.log(`📈 Historical chart created for session ${session.session_id}`);
+ setError(null);
+
+ } catch (error) {
+ console.error('Error creating historical chart:', error);
+ setError(`Chart creation failed: ${error.message}`);
+ }
+ }, [session, isZoomEnabled]);
+
+ // Handle zoom completion
+ const handleZoomComplete = useCallback(({ chart }) => {
+ const xScale = chart.scales.x;
+ const startTime = xScale.min;
+ const endTime = xScale.max;
+
+ updateCurrentRange(startTime, endTime);
+ requestAdditionalData(startTime, endTime);
+ }, []);
+
+ // Handle pan completion
+ const handlePanComplete = useCallback(({ chart }) => {
+ const xScale = chart.scales.x;
+ const startTime = xScale.min;
+ const endTime = xScale.max;
+
+ updateCurrentRange(startTime, endTime);
+ requestAdditionalData(startTime, endTime);
+ }, []);
+
+ // Update current range display
+ const updateCurrentRange = (startTime, endTime) => {
+ const start = new Date(startTime);
+ const end = new Date(endTime);
+ const range = `${start.toLocaleString()} - ${end.toLocaleString()}`;
+ setCurrentRange(range);
+
+ // Update session time range if callback available
+ if (session.onTimeRangeChange) {
+ session.onTimeRangeChange(startTime, endTime);
+ }
+ };
+
+ // Request additional data for the visible range
+ const requestAdditionalData = useCallback(async (startTime, endTime) => {
+ if (!session.onDataRequest || sessionDataRef.current.pendingDataRequest) {
+ return;
+ }
+
+ try {
+ sessionDataRef.current.pendingDataRequest = true;
+ setIsLoading(true);
+
+ const data = await session.onDataRequest(startTime, endTime);
+
+ if (data && chartRef.current) {
+ updateChartData(data);
+ }
+ } catch (error) {
+ console.error('Error requesting historical data:', error);
+ } finally {
+ sessionDataRef.current.pendingDataRequest = false;
+ setIsLoading(false);
+ }
+ }, [session]);
+
+ // Load specific time range
+ const loadHistoricalRange = useCallback(async (startTime, endTime) => {
+ if (!session.onDataRequest || !chartRef.current) {
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const data = await session.onDataRequest(startTime, endTime);
+
+ if (data) {
+ // Clear existing data
+ chartRef.current.data.datasets.forEach(dataset => {
+ dataset.data = [];
+ });
+
+ updateChartData(data);
+
+ // Set chart view to the requested range
+ if (chartRef.current.scales?.x) {
+ chartRef.current.scales.x.options.min = startTime;
+ chartRef.current.scales.x.options.max = endTime;
+ }
+ chartRef.current.update('none');
+
+ updateCurrentRange(startTime, endTime);
+ }
+ } catch (error) {
+ console.error('Error loading historical range:', error);
+ setError(`Failed to load data: ${error.message}`);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [session]);
+
+ // Update chart with new data from the optimized API response
+ const updateChartData = useCallback((response) => {
+ if (!chartRef.current || !response) return;
+
+ let totalPoints = 0;
+
+ try {
+ // Use the optimized chart_data format from the API
+ const chartData = response.chart_data || {};
+
+ chartRef.current.data.datasets.forEach((dataset, index) => {
+ const variableName = session.config.variables[index]?.name || session.config.variables[index];
+ const variableData = chartData[variableName];
+
+ if (variableData && Array.isArray(variableData)) {
+ // Data is already in {x, y} format for Chart.js
+ const newPoints = variableData.map(point => ({
+ x: new Date(point.x).getTime(),
+ y: point.y
+ }));
+
+ // Merge with existing data and remove duplicates
+ const allPoints = [...dataset.data, ...newPoints];
+ const uniquePoints = allPoints.filter((point, index, arr) =>
+ arr.findIndex(p => p.x === point.x) === index
+ );
+
+ // Sort by timestamp
+ dataset.data = uniquePoints.sort((a, b) => a.x - b.x);
+ totalPoints += dataset.data.length;
+ }
+ });
+
+ chartRef.current.update('none');
+ setDataPointsCount(totalPoints);
+
+ } catch (error) {
+ console.error('Error updating chart data:', error);
+ }
+ }, [session]);
+
+ // Refresh current visible range
+ const refreshCurrentRange = useCallback(() => {
+ if (!chartRef.current?.scales?.x) return;
+
+ const xScale = chartRef.current.scales.x;
+ const startTime = xScale.min;
+ const endTime = xScale.max;
+
+ if (startTime && endTime) {
+ loadHistoricalRange(startTime, endTime);
+ }
+ }, [loadHistoricalRange]);
+
+ // Get current time range
+ const getCurrentTimeRange = useCallback(() => {
+ if (!chartRef.current?.scales?.x) return null;
+
+ const xScale = chartRef.current.scales.x;
+ return {
+ start: xScale.min,
+ end: xScale.max
+ };
+ }, []);
+
+ // Reset zoom to show all data
+ const resetZoom = useCallback(() => {
+ if (chartRef.current && chartRef.current.resetZoom) {
+ chartRef.current.resetZoom();
+ }
+ }, []);
+
+ // Initialize chart
+ useEffect(() => {
+ createChart();
+ return destroyChart;
+ }, [createChart, destroyChart]);
+
+ // Handle session changes
+ useEffect(() => {
+ sessionDataRef.current.sessionId = session.session_id;
+ }, [session.session_id]);
+
+ // Load initial data when component mounts
+ useEffect(() => {
+ if (session.timeRange && session.timeRange.startDate && session.timeRange.endDate) {
+ const startDateTime = new Date(`${session.timeRange.startDate}T${session.timeRange.startTime || '00:00'}:00`);
+ const endDateTime = new Date(`${session.timeRange.endDate}T${session.timeRange.endTime || '23:59'}:00`);
+
+ loadHistoricalRange(startDateTime.getTime(), endDateTime.getTime());
+ }
+ }, [session.timeRange, loadHistoricalRange]);
+
+ if (error) {
+ return (
+