Files
pyGUI/src/pygui/backend/visualization/graph_view.py
T
jack 5514653b94 refactor: prune orphaned attic modules and standardize project configurations
- Move orphaned modules (data_segment.py, contour_models.py, marching_squares.py) to attic/.
- Standardize local settings logic, serial port parameters, and graph view plots.
- Update pyproject.toml pyside6-project dependencies and configure gitignore.
2026-06-18 17:09:24 -07:00

167 lines
5.4 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
import pyqtgraph as pg
from pyqtgraph import PlotWidget
from PySide6.QtCore import Property, QObject, Signal, Slot
from PySide6.QtWidgets import QWidget
log = logging.getLogger(__name__)
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 = []