e306db6816
- SettingsTab: chamber ID, master CSV mapping (families A-Z), wafer behavior toggle, light/dark mode toggle, Edit CSV Metadata dialog - StatusTab: connection status card, wafer info, activity log (blank until Detect Wafer fires from the side rail) - DataTab: empty state, ready for parsed data - HomePage: side rail buttons dispatch device actions and jump to Status tab; selectedSideActionIndex defaults to -1 on startup - DeviceController registered as QML context property in main.py - LocalSettings: added status persistence fields - Theme: added subheadingColor and disabledText - Added serialcomm/ package and backend data/graph modules - gitignore: restore clean version
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
"""pyqtgraph PlotWidget wrapper for embedding in QML.
|
|
|
|
Exposes a QWidget with a pyqtgraph PlotWidget that can be displayed
|
|
via QML's `import QtWidgets` or `QtWidgets.QWidget` integration.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
from PySide6.QtCore import QObject, Property, Signal, Slot
|
|
from PySide6.QtWidgets import QWidget
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Import pyqtgraph after Qt is initialized
|
|
import pyqtgraph as pg
|
|
from pyqtgraph import PlotWidget
|
|
|
|
|
|
class GraphView(QObject):
|
|
"""QML-exposed controller for a pyqtgraph line chart.
|
|
|
|
Accepts sensor temperature data (list of lists) and renders
|
|
each sensor as a separate line series.
|
|
"""
|
|
|
|
# ---- signals ----
|
|
dataReady = Signal(object) # {"success": bool, "sensory_names": list, "series": list}
|
|
|
|
def __init__(self, parent: Optional[QObject] = None) -> None:
|
|
super().__init__(parent)
|
|
self._plot_widget: Optional[PlotWidget] = None
|
|
self._plot_window: Optional[QWidget] = None
|
|
self._series: list[Any] = []
|
|
self._sensor_names: list[str] = []
|
|
|
|
@Property(object, notify=dataReady)
|
|
def plotWidget(self) -> Any:
|
|
"""Return the QWidget hosting the pyqtgraph PlotWidget for QML embedding."""
|
|
return self._plot_window
|
|
|
|
@Slot()
|
|
def createPlotWidget(self, parent_widget: Optional[QWidget] = None) -> None:
|
|
"""Create and return a QWidget containing a pyqtgraph PlotWidget.
|
|
|
|
Args:
|
|
parent_widget: Optional parent widget.
|
|
"""
|
|
pg.setConfigOption("background", "default")
|
|
pg.setConfigOption("foreground", "default")
|
|
|
|
self._plot_window = QWidget(parent=parent_widget)
|
|
self._plot_widget = PlotWidget()
|
|
self._plot_widget.setBackground("default")
|
|
|
|
from PySide6.QtWidgets import QVBoxLayout
|
|
layout = QVBoxLayout(self._plot_window)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
layout.addWidget(self._plot_widget)
|
|
|
|
# Set axis labels
|
|
self._plot_widget.setLabel("left", "Temperature", units="°C")
|
|
self._plot_widget.setLabel("bottom", "Measurement Interval")
|
|
self._plot_widget.setTitle("Sensor Temperature Over Time")
|
|
|
|
@Slot(str, str)
|
|
def updateChart(self, sensor_names_str: str, series_data_str: str) -> None:
|
|
"""Update the chart with sensor data.
|
|
|
|
Args:
|
|
sensor_names_str: Comma-separated sensor names (e.g. "Sensor1,Sensor2").
|
|
series_data_str: JSON-like string of nested lists for each sensor's values.
|
|
"""
|
|
import json
|
|
|
|
if not self._plot_widget:
|
|
log.warning("PlotWidget not created yet")
|
|
return
|
|
|
|
try:
|
|
sensor_names = [s.strip() for s in sensor_names_str.split(",") if s.strip()]
|
|
series_data = json.loads(series_data_str)
|
|
except (json.JSONDecodeError, AttributeError) as exc:
|
|
log.error("Failed to parse chart data: %s", exc)
|
|
return
|
|
|
|
# Clear existing series
|
|
self._plot_widget.clear()
|
|
self._series = []
|
|
|
|
if not series_data:
|
|
return
|
|
|
|
# Determine Y-axis range from all data
|
|
all_values = []
|
|
for sensor_values in series_data:
|
|
for v in sensor_values:
|
|
try:
|
|
all_values.append(float(v))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if all_values:
|
|
y_min = min(all_values)
|
|
y_max = max(all_values)
|
|
y_range = y_max - y_min
|
|
buffer = max(y_range * 0.1, 1.0) # At least 1 degree buffer
|
|
self._plot_widget.setYRange(y_min - buffer, y_max + buffer)
|
|
else:
|
|
self._plot_widget.setYRange(-50, 150)
|
|
|
|
# X-axis: measurement intervals (0-based index)
|
|
num_points = len(series_data[0]) if series_data else 0
|
|
x_axis = list(range(num_points))
|
|
|
|
# Define a set of distinct colors for series
|
|
colors = [
|
|
(255, 87, 87), # Red
|
|
(66, 165, 245), # Blue
|
|
(102, 187, 106), # Green
|
|
(255, 167, 38), # Orange
|
|
(171, 71, 188), # Purple
|
|
(0, 188, 212), # Cyan
|
|
(255, 112, 67), # Deep Orange
|
|
(121, 85, 72), # Brown
|
|
(92, 107, 192), # Indigo
|
|
(48, 125, 117), # Teal
|
|
]
|
|
|
|
# Add each sensor as a line series
|
|
for i, sensor_name in enumerate(sensor_names):
|
|
if i >= len(series_data):
|
|
break
|
|
|
|
sensor_values = series_data[i]
|
|
y_values = []
|
|
for v in sensor_values:
|
|
try:
|
|
y_values.append(float(v))
|
|
except (ValueError, TypeError):
|
|
y_values.append(0.0)
|
|
|
|
color = colors[i % len(colors)]
|
|
pen = pg.mkPen(color=color, width=1)
|
|
|
|
curve = self._plot_widget.plot(x_axis, y_values, name=sensor_name, pen=pen)
|
|
self._series.append(curve)
|
|
|
|
self._sensor_names = sensor_names
|
|
|
|
@Slot()
|
|
def resetChart(self) -> None:
|
|
"""Clear the chart."""
|
|
if self._plot_widget:
|
|
self._plot_widget.clear()
|
|
self._series = []
|
|
self._sensor_names = []
|
|
|
|
@Slot()
|
|
def destroyPlotWidget(self) -> None:
|
|
"""Destroy the plot widget."""
|
|
if self._plot_window:
|
|
self._plot_window.deleteLater()
|
|
self._plot_window = None
|
|
self._plot_widget = None
|
|
self._series = []
|