from __future__ import annotations
#-----------------------------------------------------------------------------
# Company : SLAC National Accelerator Laboratory
#-----------------------------------------------------------------------------
# Description:
# PyRogue PyDM Plot Window
#-----------------------------------------------------------------------------
# This file is part of the rogue software platform. It is subject to
# the license terms in the LICENSE.txt file found in the top-level directory
# of this distribution and at:
# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html.
# No part of the rogue software platform, including this file, may be
# copied, modified, propagated, or distributed except according to the terms
# contained in the LICENSE.txt file.
#-----------------------------------------------------------------------------
from pydm.widgets.frame import PyDMFrame
from qtpy.QtWidgets import QVBoxLayout, QWidget
from qtpy.QtCore import QCoreApplication, QEvent
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
[docs]
class Plotter(PyDMFrame):
"""PyDM widget that displays an incoming matplotlib figure published by a pyrogue Variable.
Parameters
----------
parent : QWidget | None, optional
Parent Qt widget.
init_channel : str | None, optional
Initial Rogue channel address.
show_toolbar : bool, optional
If ``True``, display the matplotlib navigation toolbar above the plot.
"""
def __init__(
self,
parent: QWidget | None = None,
init_channel: str | None = None,
show_toolbar: bool = True,
) -> None:
super().__init__(parent, init_channel)
self._systemLog = None
self._vb: QVBoxLayout | None = None
self._canvas: FigureCanvas | None = None
self._nav: NavigationToolbar | None = None
self._fig: Figure | None = None
self._show_toolbar = show_toolbar
def _ensure_layout(self) -> None:
"""Build the widget layout before embedding the plot widgets."""
if self._vb is not None:
return
self._vb = QVBoxLayout()
self.setLayout(self._vb)
def _clear_plot(self) -> None:
"""Release the current toolbar, canvas, and figure."""
if self._vb is None:
self._canvas = None
self._nav = None
self._fig = None
return
old_nav = self._nav
old_canvas = self._canvas
old_fig = self._fig
# Drop our own references first.
self._nav = None
self._canvas = None
self._fig = None
# Remove toolbar first because it may hold references to the canvas.
if old_nav is not None:
self._vb.removeWidget(old_nav)
old_nav.hide()
old_nav.setParent(None)
old_nav.deleteLater()
# Then remove canvas.
if old_canvas is not None:
self._vb.removeWidget(old_canvas)
# Break the canvas -> figure reference if possible.
try:
old_canvas.figure = None
except Exception:
pass
old_canvas.hide()
old_canvas.setParent(None)
old_canvas.deleteLater()
# Finally release figure-side resources.
if old_fig is not None:
try:
old_fig.clear()
except Exception:
pass
try:
plt.close(old_fig)
except Exception:
pass
# Flush deferred deletes for the widgets we just scheduled, so old Qt
# objects do not pile up without impacting unrelated Qt objects.
if old_nav is not None:
QCoreApplication.sendPostedEvents(old_nav, QEvent.DeferredDelete)
if old_canvas is not None:
QCoreApplication.sendPostedEvents(old_canvas, QEvent.DeferredDelete)
[docs]
def connection_changed(self, connected: bool) -> None:
"""Build the layout after the first successful channel connection."""
build = (self._vb is None) and (self._connected != connected and connected is True)
super().connection_changed(connected)
if not build:
return
self._ensure_layout()
[docs]
def value_changed(self, new_val: object) -> None:
"""Replace the displayed plot with the latest incoming figure."""
self._ensure_layout()
self._clear_plot()
fig = new_val
canvas = FigureCanvas(fig)
nav = NavigationToolbar(canvas, self) if self._show_toolbar else None
self._fig = fig
self._canvas = canvas
self._nav = nav
if nav is not None:
self._vb.addWidget(nav)
self._vb.addWidget(canvas)
[docs]
def closeEvent(self, event) -> None:
"""Release embedded plot resources when the widget closes."""
self._clear_plot()
super().closeEvent(event)