from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QPushButton, QScrollArea, QSplitter ) from PySide6.QtCore import Qt, Slot from PySide6.QtCharts import QChart, QChartView, QLineSeries, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QScatterSeries, QValueAxis from PySide6.QtGui import QPainter, QColor, QPen, QFont, QBrush, QLinearGradient, QGradient class ChartWidget(QWidget): """Base widget for analytics charts""" def __init__(self, title): super().__init__() self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) # Chart title self.title_label = QLabel(title) self.title_label.setAlignment(Qt.AlignCenter) self.title_label.setStyleSheet("font-weight: bold; font-size: 14px;") self.layout.addWidget(self.title_label) # Create chart self.chart = QChart() self.chart.setAnimationOptions(QChart.SeriesAnimations) self.chart.setBackgroundBrush(QBrush(QColor(240, 240, 240))) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignBottom) # Chart view self.chartview = QChartView(self.chart) self.chartview.setRenderHint(QPainter.RenderHint.Antialiasing) self.layout.addWidget(self.chartview) self.setMinimumSize(400, 300) class TimeSeriesChart(ChartWidget): """Time series chart for traffic data""" def __init__(self, title="Traffic Over Time"): super().__init__(title) # Create series self.vehicle_series = QLineSeries() self.vehicle_series.setName("Vehicles") self.vehicle_series.setPen(QPen(QColor(0, 162, 232), 2)) self.pedestrian_series = QLineSeries() self.pedestrian_series.setName("Pedestrians") self.pedestrian_series.setPen(QPen(QColor(255, 140, 0), 2)) self.violation_series = QLineSeries() self.violation_series.setName("Violations") self.violation_series.setPen(QPen(QColor(232, 0, 0), 2)) self.traffic_light_color_series = QLineSeries() self.traffic_light_color_series.setName("Traffic Light Color") self.traffic_light_color_series.setPen(QPen(QColor(128, 0, 128), 2, Qt.DashLine)) # Add series to chart self.chart.addSeries(self.vehicle_series) self.chart.addSeries(self.pedestrian_series) self.chart.addSeries(self.violation_series) self.chart.addSeries(self.traffic_light_color_series) # Create and configure axes self.chart.createDefaultAxes() x_axis = self.chart.axes(Qt.Horizontal)[0] x_axis.setTitleText("Time") x_axis.setGridLineVisible(True) x_axis.setLabelsAngle(45) y_axis = self.chart.axes(Qt.Vertical)[0] y_axis.setTitleText("Count") y_axis.setGridLineVisible(True) def update_data(self, time_series): """Update chart with new time series data""" try: if not time_series or 'timestamps' not in time_series: return # Check if chart and series are still valid if not hasattr(self, 'chart') or self.chart is None: return if not hasattr(self, 'vehicle_series') or self.vehicle_series is None: return timestamps = time_series.get('timestamps', []) vehicle_counts = time_series.get('vehicle_counts', []) pedestrian_counts = time_series.get('pedestrian_counts', []) violation_counts = time_series.get('violation_counts', []) traffic_light_colors = time_series.get('traffic_light_colors', []) # Clear existing series safely try: self.vehicle_series.clear() self.pedestrian_series.clear() self.violation_series.clear() self.traffic_light_color_series.clear() except RuntimeError: # C++ object was already deleted, skip update return # Add data points for i in range(len(timestamps)): try: # Add x as index, y as count self.vehicle_series.append(i, vehicle_counts[i] if i < len(vehicle_counts) else 0) self.pedestrian_series.append(i, pedestrian_counts[i] if i < len(pedestrian_counts) else 0) self.violation_series.append(i, violation_counts[i] if i < len(violation_counts) else 0) # Add traffic light color as mapped int for charting (0=unknown, 1=red, 2=yellow, 3=green) if i < len(traffic_light_colors): color_map = {'unknown': 0, 'red': 1, 'yellow': 2, 'green': 3} color_val = color_map.get(traffic_light_colors[i], 0) self.traffic_light_color_series.append(i, color_val) except RuntimeError: # C++ object was deleted during update return # Update axes safely try: axes = self.chart.axes(Qt.Horizontal) if axes: axes[0].setRange(0, max(len(timestamps)-1, 10)) max_count = max( max(vehicle_counts) if vehicle_counts else 0, max(pedestrian_counts) if pedestrian_counts else 0, max(violation_counts) if violation_counts else 0 ) axes = self.chart.axes(Qt.Vertical) if axes: axes[0].setRange(0, max(max_count+1, 5)) except (RuntimeError, IndexError): # Chart axes were deleted or not available pass # Optionally, set y-axis label for traffic light color axes = self.chart.axes(Qt.Vertical) if axes: axes[0].setTitleText("Count / TL Color (0=U,1=R,2=Y,3=G)") except Exception as e: print(f"[WARNING] Chart update failed: {e}") class DetectionPieChart(ChartWidget): """Pie chart for detected object classes""" def __init__(self, title="Detection Classes"): super().__init__(title) self.pie_series = QPieSeries() self.chart.addSeries(self.pie_series) def update_data(self, detection_counts): """Update chart with detection counts""" try: if not detection_counts: return # Check if chart and series are still valid if not hasattr(self, 'chart') or self.chart is None: return if not hasattr(self, 'pie_series') or self.pie_series is None: return # Clear existing slices safely try: self.pie_series.clear() except RuntimeError: # C++ object was already deleted, skip update return # Add new slices for class_name, count in detection_counts.items(): # Only add if count > 0 if count > 0: try: slice = self.pie_series.append(class_name, count) # Set colors based on class if class_name.lower() == 'car': slice.setBrush(QColor(0, 200, 0)) elif class_name.lower() == 'person': slice.setBrush(QColor(255, 165, 0)) elif class_name.lower() == 'truck': slice.setBrush(QColor(0, 100, 200)) elif class_name.lower() == 'bus': slice.setBrush(QColor(200, 0, 100)) # Highlight important slices if count > 10: slice.setExploded(True) slice.setLabelVisible(True) except RuntimeError: # C++ object was deleted during update return except Exception as e: print(f"[WARNING] Pie chart update failed: {e}") class ViolationBarChart(ChartWidget): """Bar chart for violation types""" def __init__(self, title="Violations by Type"): super().__init__(title) # Create series self.bar_series = QBarSeries() self.chart.addSeries(self.bar_series) # Create axes self.axis_x = QBarCategoryAxis() self.chart.addAxis(self.axis_x, Qt.AlignBottom) self.bar_series.attachAxis(self.axis_x) self.chart.createDefaultAxes() self.chart.axes(Qt.Vertical)[0].setTitleText("Count") def update_data(self, violation_counts): """Update chart with violation counts""" try: if not violation_counts: return # Check if chart and series are still valid if not hasattr(self, 'chart') or self.chart is None: return if not hasattr(self, 'bar_series') or self.bar_series is None: return if not hasattr(self, 'axis_x') or self.axis_x is None: return # Clear existing data safely try: self.bar_series.clear() except RuntimeError: # C++ object was already deleted, skip update return # Create bar set bar_set = QBarSet("Violations") # Set colors try: bar_set.setColor(QColor(232, 0, 0)) except RuntimeError: return # Add values values = [] categories = [] for violation_type, count in violation_counts.items(): if count > 0: values.append(count) # Format violation type for display display_name = violation_type.replace('_', ' ').title() categories.append(display_name) if values: try: bar_set.append(values) self.bar_series.append(bar_set) # Update x-axis categories self.axis_x.setCategories(categories) # Update y-axis range y_axes = self.chart.axes(Qt.Vertical) if y_axes: y_axes[0].setRange(0, max(values) * 1.2) except RuntimeError: # C++ object was deleted during update return except Exception as e: print(f"[WARNING] Bar chart update failed: {e}") class LatencyChartWidget(ChartWidget): """Custom latency chart with spikes, device/res changes, and live stats legend.""" def __init__(self, title="Inference Latency Over Time"): super().__init__(title) self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32))) self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;") self.chart.legend().setVisible(False) # Main latency line self.latency_series = QLineSeries() self.latency_series.setName("Latency (ms)") self.latency_series.setPen(QPen(QColor(0, 255, 255), 2)) self.chart.addSeries(self.latency_series) # Spikes as red dots self.spike_series = QScatterSeries() self.spike_series.setName("Spikes") self.spike_series.setMarkerSize(8) self.spike_series.setColor(QColor(255, 64, 64)) self.chart.addSeries(self.spike_series) # Device/resolution change lines (vertical) self.event_lines = [] # Axes self.chart.createDefaultAxes() self.x_axis = self.chart.axes(Qt.Horizontal)[0] self.x_axis.setTitleText("") self.x_axis.setLabelsColor(QColor("#fff")) self.x_axis.setGridLineColor(QColor("#444")) self.y_axis = self.chart.axes(Qt.Vertical)[0] self.y_axis.setTitleText("ms") self.y_axis.setLabelsColor(QColor("#fff")) self.y_axis.setGridLineColor(QColor("#444")) # Stats label self.stats_label = QLabel() self.stats_label.setStyleSheet("color: #00e6ff; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;") self.layout.addWidget(self.stats_label) def update_data(self, latency_data): """ latency_data: dict with keys: 'latencies': list of float, 'spike_indices': list of int, 'device_switches': list of int, 'resolution_changes': list of int """ if not latency_data or 'latencies' not in latency_data: return latencies = latency_data.get('latencies', []) spikes = set(latency_data.get('spike_indices', [])) device_switches = set(latency_data.get('device_switches', [])) res_changes = set(latency_data.get('resolution_changes', [])) # Clear series self.latency_series.clear() self.spike_series.clear() # Remove old event lines for line in self.event_lines: self.chart.removeAxis(line) self.event_lines = [] # Plot latency and spikes for i, val in enumerate(latencies): self.latency_series.append(i, val) if i in spikes: self.spike_series.append(i, val) # Add device/resolution change lines for idx in device_switches: line = QLineSeries() line.setPen(QPen(QColor(33, 150, 243), 3)) # Blue line.append(idx, min(latencies) if latencies else 0) line.append(idx, max(latencies) if latencies else 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) for idx in res_changes: line = QLineSeries() line.setPen(QPen(QColor(255, 167, 38), 3)) # Orange line.append(idx, min(latencies) if latencies else 0) line.append(idx, max(latencies) if latencies else 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) # Update axes self.x_axis.setRange(0, max(len(latencies)-1, 10)) self.y_axis.setRange(0, max(max(latencies) if latencies else 1, 10)) # Stats if latencies: avg = sum(latencies)/len(latencies) mx = max(latencies) self.stats_label.setText(f"Avg: {avg:.1f}ms | Max: {mx:.1f}ms | Spikes: {len(spikes)}") else: self.stats_label.setText("") class FPSChartWidget(ChartWidget): """FPS & Resolution Impact chart with device/resolution change lines and live stats.""" def __init__(self, title="FPS & Resolution Impact"): super().__init__(title) self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32))) self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;") self.chart.legend().setVisible(False) self.fps_series = QLineSeries() self.fps_series.setName("FPS") self.fps_series.setPen(QPen(QColor(0, 255, 255), 2)) self.chart.addSeries(self.fps_series) self.event_lines = [] self.chart.createDefaultAxes() self.x_axis = self.chart.axes(Qt.Horizontal)[0] self.x_axis.setLabelsColor(QColor("#fff")) self.x_axis.setGridLineColor(QColor("#444")) self.y_axis = self.chart.axes(Qt.Vertical)[0] self.y_axis.setTitleText("FPS") self.y_axis.setLabelsColor(QColor("#fff")) self.y_axis.setGridLineColor(QColor("#444")) self.stats_label = QLabel() self.stats_label.setStyleSheet("color: #00ff82; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;") self.layout.addWidget(self.stats_label) def update_data(self, fps_data): if not fps_data or 'fps' not in fps_data: return fps = fps_data.get('fps', []) device_switches = set(fps_data.get('device_switches', [])) res_changes = set(fps_data.get('resolution_changes', [])) device_labels = fps_data.get('device_labels', {}) res_labels = fps_data.get('resolution_labels', {}) self.fps_series.clear() for line in self.event_lines: self.chart.removeAxis(line) self.event_lines = [] for i, val in enumerate(fps): self.fps_series.append(i, val) for idx in device_switches: line = QLineSeries() line.setPen(QPen(QColor(33, 150, 243), 3)) line.append(idx, min(fps) if fps else 0) line.append(idx, max(fps) if fps else 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) for idx in res_changes: line = QLineSeries() line.setPen(QPen(QColor(255, 167, 38), 3)) line.append(idx, min(fps) if fps else 0) line.append(idx, max(fps) if fps else 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) self.x_axis.setRange(0, max(len(fps)-1, 10)) self.y_axis.setRange(0, max(max(fps) if fps else 1, 10)) # Live stats (current FPS, resolution, device) cur_fps = fps[-1] if fps else 0 cur_res = res_labels.get(len(fps)-1, "-") cur_dev = device_labels.get(len(fps)-1, "-") self.stats_label.setText(f"Current FPS: {cur_fps:.1f} | Resolution: {cur_res} | Device: {cur_dev}") class DeviceSwitchChartWidget(ChartWidget): """Device Switching & Resolution Changes chart with colored vertical lines and legend.""" def __init__(self, title="Device Switching & Resolution Changes"): super().__init__(title) self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32))) self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;") self.chart.legend().setVisible(False) self.event_lines = [] self.chart.createDefaultAxes() self.x_axis = self.chart.axes(Qt.Horizontal)[0] self.x_axis.setLabelsColor(QColor("#fff")) self.x_axis.setGridLineColor(QColor("#444")) self.y_axis = self.chart.axes(Qt.Vertical)[0] self.y_axis.setTitleText("-") self.y_axis.setLabelsColor(QColor("#fff")) self.y_axis.setGridLineColor(QColor("#444")) self.legend_label = QLabel() self.legend_label.setStyleSheet("color: #ffb300; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;") self.layout.addWidget(self.legend_label) def update_data(self, event_data): if not event_data: return cpu_spikes = set(event_data.get('cpu_spikes', [])) gpu_spikes = set(event_data.get('gpu_spikes', [])) switches = set(event_data.get('switches', [])) res_changes = set(event_data.get('res_changes', [])) n = event_data.get('n', 100) for line in self.event_lines: self.chart.removeAxis(line) self.event_lines = [] for idx in cpu_spikes: line = QLineSeries() line.setPen(QPen(QColor(255, 64, 64), 2)) line.append(idx, 0) line.append(idx, 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) for idx in gpu_spikes: line = QLineSeries() line.setPen(QPen(QColor(255, 87, 34), 2)) line.append(idx, 0) line.append(idx, 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) for idx in switches: line = QLineSeries() line.setPen(QPen(QColor(33, 150, 243), 2)) line.append(idx, 0) line.append(idx, 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) for idx in res_changes: line = QLineSeries() line.setPen(QPen(QColor(255, 167, 38), 2)) line.append(idx, 0) line.append(idx, 1) self.chart.addSeries(line) line.attachAxis(self.x_axis) line.attachAxis(self.y_axis) self.event_lines.append(line) self.x_axis.setRange(0, n) self.y_axis.setRange(0, 1) self.legend_label.setText("CPU Spikes: {} | GPU Spikes: {} | Switches: {} | Res Changes: {}".format(len(cpu_spikes), len(gpu_spikes), len(switches), len(res_changes))) class AnalyticsTab(QWidget): """Analytics tab with charts and statistics""" def __init__(self): super().__init__() self.initUI() def initUI(self): """Initialize UI components""" main_layout = QVBoxLayout(self) # Add notice that violations are disabled notice_label = QLabel("⚠️ Violation detection is currently disabled. Only object detection statistics will be shown.") notice_label.setStyleSheet("font-size: 14px; color: #FFA500; font-weight: bold; padding: 10px;") notice_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(notice_label) # Charts section charts_splitter = QSplitter(Qt.Horizontal) # Latency chart (top, full width) self.latency_chart = LatencyChartWidget("Inference Latency Over Time") main_layout.addWidget(self.latency_chart) # Left side - Time series chart self.time_series_chart = TimeSeriesChart("Traffic Over Time") charts_splitter.addWidget(self.time_series_chart) # Right side - Detection and violation charts right_charts = QWidget() right_layout = QVBoxLayout(right_charts) self.detection_chart = DetectionPieChart("Detection Classes") self.violation_chart = ViolationBarChart("Violations by Type") right_layout.addWidget(self.detection_chart) right_layout.addWidget(self.violation_chart) charts_splitter.addWidget(right_charts) charts_splitter.setSizes([500, 500]) # Equal initial sizes main_layout.addWidget(charts_splitter) # Key metrics section metrics_box = QGroupBox("Key Metrics") metrics_layout = QHBoxLayout(metrics_box) # Vehicle metrics vehicle_metrics = QGroupBox("Traffic") vehicle_layout = QVBoxLayout(vehicle_metrics) self.total_vehicles_label = QLabel("Total Vehicles: 0") self.total_pedestrians_label = QLabel("Total Pedestrians: 0") vehicle_layout.addWidget(self.total_vehicles_label) vehicle_layout.addWidget(self.total_pedestrians_label) metrics_layout.addWidget(vehicle_metrics) # Violation metrics violation_metrics = QGroupBox("Violations") violation_layout = QVBoxLayout(violation_metrics) self.total_violations_label = QLabel("Total Violations: 0") self.peak_violation_label = QLabel("Peak Violation Hour: --") violation_layout.addWidget(self.total_violations_label) violation_layout.addWidget(self.peak_violation_label) metrics_layout.addWidget(violation_metrics) # Performance metrics performance_metrics = QGroupBox("Performance") performance_layout = QVBoxLayout(performance_metrics) self.avg_fps_label = QLabel("Avg FPS: 0") self.avg_processing_label = QLabel("Avg Processing Time: 0 ms") performance_layout.addWidget(self.avg_fps_label) performance_layout.addWidget(self.avg_processing_label) metrics_layout.addWidget(performance_metrics) main_layout.addWidget(metrics_box) # Controls controls = QHBoxLayout() self.reset_btn = QPushButton("Reset Statistics") controls.addWidget(self.reset_btn) controls.addStretch(1) # Push button to left main_layout.addLayout(controls) @Slot(dict) def update_analytics(self, analytics): """ Update analytics display with new data. Args: analytics: Dictionary of analytics data """ try: if not analytics: return # Update latency chart try: if hasattr(self, 'latency_chart') and self.latency_chart is not None: self.latency_chart.update_data(analytics.get('latency', {})) except Exception as e: print(f"[WARNING] Latency chart update failed: {e}") # Update charts with error handling try: if hasattr(self, 'time_series_chart') and self.time_series_chart is not None: self.time_series_chart.update_data(analytics.get('time_series', {})) except Exception as e: print(f"[WARNING] Time series chart update failed: {e}") try: if hasattr(self, 'detection_chart') and self.detection_chart is not None: self.detection_chart.update_data(analytics.get('detection_counts', {})) except Exception as e: print(f"[WARNING] Detection chart update failed: {e}") try: if hasattr(self, 'violation_chart') and self.violation_chart is not None: self.violation_chart.update_data(analytics.get('violation_counts', {})) except Exception as e: print(f"[WARNING] Violation chart update failed: {e}") # Update metrics try: metrics = analytics.get('metrics', {}) if hasattr(self, 'total_vehicles_label'): self.total_vehicles_label.setText(f"Total Vehicles: {metrics.get('total_vehicles', 0)}") if hasattr(self, 'total_pedestrians_label'): self.total_pedestrians_label.setText(f"Total Pedestrians: {metrics.get('total_pedestrians', 0)}") if hasattr(self, 'total_violations_label'): self.total_violations_label.setText(f"Total Violations: {metrics.get('total_violations', 0)}") peak_hour = metrics.get('peak_violation_hour') if peak_hour: peak_text = f"Peak Violation Hour: {peak_hour.get('time', '--')} ({peak_hour.get('violations', 0)})" else: peak_text = "Peak Violation Hour: --" if hasattr(self, 'peak_violation_label'): self.peak_violation_label.setText(peak_text) if hasattr(self, 'avg_fps_label'): self.avg_fps_label.setText(f"Avg FPS: {metrics.get('avg_fps', 0):.1f}") if hasattr(self, 'avg_processing_label'): self.avg_processing_label.setText( f"Avg Processing Time: {metrics.get('avg_processing_time', 0):.1f} ms" ) except Exception as e: print(f"[WARNING] Metrics update failed: {e}") # Update traffic light label with latest color try: tl_series = analytics.get('traffic_light_color_series', []) if tl_series: latest = tl_series[-1][1] self.traffic_light_label.setText(f"Traffic Light: {latest.title()}") else: self.traffic_light_label.setText("Traffic Light: Unknown") except Exception as e: print(f"[WARNING] Traffic light label update failed: {e}") except Exception as e: print(f"[ERROR] Analytics update failed: {e}")