There are a few examples out there of how to do this, but after trying all of them I don't understand how to correctly implement it. I have a program that a button creates a widget with contents inside and assigns it to a grid layout, and it also creates a figure on a canvas. Clicking on the button again creates another widget filled with the same contents and another figure and assigns it to the layout.
One of the contents is a spin box which controls the rotation of the figure. I want each spin box to be able to control the figure that was created with it individually. Here is where I am stuck.
How do I have a general button that can create several widgets, but then on every value change of each spin box, be able to tell which widget it came from so it will rotate the correct figure? I want the widget id or name or however I can access it. Here is what I have so far - Thanks in advance!:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import numpy as np
import math
from scipy.optimize import fsolve
from mplwindow5 import Ui_mplMainWindow
from cU_widget import cU_Widget
class Viewer(QMainWindow, Ui_mplMainWindow):
def __init__(self, parent = None):
super(Viewer, self).__init__(parent)
self.setupUi(self)
self.count = 0
self.cU_widget = []
self.cU_rotate = []
self.lbl_cU_rotate = []
self.lbl_cU = []
self.btn_cU.clicked.connect(self.add_cU)
def add_cU(self):
self.cU_widget.append(int(self.count))
self.cU_widget[self.count] = QWidget(self.scrollAreaWidgetContents)
self.cU_widget[self.count].setMinimumSize(QSize(101, 81))
self.cU_widget[self.count].setMaximumSize(QSize(101, 81))
self.cU_widget[self.count].setObjectName("cU_widget" + str(self.count+1))
self.lbl_cU.append(int(self.count))
self.lbl_cU[self.count] = QLabel("cU " + str(self.count+1), self.cU_widget[self.count])
self.lbl_cU[self.count].setGeometry(QRect(0, 0, 101, 27))
self.lbl_cU[self.count].setObjectName("lbl_cU_" + str(self.count+1))
self.lbl_cU_rotate.append(int(self.count))
self.lbl_cU_rotate[self.count] = QLabel("R", self.cU_widget[self.count])
self.lbl_cU_rotate[self.count].setGeometry(QRect(6, 50, 20, 20))
self.lbl_cU_rotate[self.count].setObjectName("lbl_cU_rotate" + str(self.count+1))
self.cU_rotate.append(int(self.count))
self.cU_rotate[self.count] = QDoubleSpinBox(self.cU_widget[self.count])
self.cU_rotate[self.count].setGeometry(QRect(20, 40, 71, 27))
self.cU_rotate[self.count].setObjectName("cU_rotate" + str(self.count+1))
self.cU_rotate[self.count].valueChanged.connect(self.cU) # ??? What to use here
self.gridLayout.addWidget(self.cU_widget[self.count], self.count, 0)
self.cU()
def cU(self):
self.cU_rotate[self.count] = self.cU_rotate[self.count].value() # ?? What to use here
rotate = 1
tt = np.arange(0,1, 0.001)
lco_x0 = 0
lco_x1 = 4
lco_y0 = 1
lco_y1 = 3
cU_L_x0 = (lco_x0 * math.cos(math.radians(self.cU_rotate[self.count] + rotate))) - (lco_y0 * math.sin(math.radians(self.cU_rotate[self.count] + rotate)))
cU_L_x1 = (lco_x1 * math.cos(math.radians(self.cU_rotate[self.count] + rotate))) - (lco_y1 * math.sin(math.radians(self.cU_rotate[self.count] + rotate)))
#...
cU_L_y0 = (lco_x0 * math.sin(math.radians(self.cU_rotate[self.count] + rotate))) + (lco_y0 * math.cos(math.radians(self.cU_rotate[self.count] + rotate)))
cU_L_y1 = (lco_x1 * math.sin(math.radians(self.cU_rotate[self.count] + rotate))) + (lco_y1 * math.cos(math.radians(self.cU_rotate[self.count] + rotate)))
#...
cU_L_ax = ( 1 * cU_L_x0)
cU_L_bx = ((-6 * cU_L_x0) +(30 * cU_L_x1))
# ...
cU_L_ay = ( 1 * cU_L_y0)
cU_L_by = ((-6 * cU_L_y0) +(30 * cU_L_y1))
#...
cU_L_xtt = (cU_L_ax * tt**2) + (cU_L_bx * tt) + 1
cU_L_ytt = (cU_L_ay * tt**2) + (cU_L_by * tt) + 1
self.mplContainer.canvas.ax.plot(cU_L_xtt, cU_L_ytt, 'r')
self.mplContainer.canvas.ax.set_ylim([-5, 5])
self.mplContainer.canvas.ax.set_xlim([0, 10])
self.mplContainer.canvas.ax.set_aspect(1)
self.mplContainer.canvas.draw()
self.count += 1
app = QApplication(sys.argv)
viewer = Viewer()
viewer.show()
sys.exit(app.exec_())
here is mplwindow5:
from PyQt4 import QtCore, QtGui
class Ui_mplMainWindow(object):
def setupUi(self, mplMainWindow):
mplMainWindow.setObjectName("mplMainWindow")
mplMainWindow.resize(1171, 826)
self.centralwidget = QtGui.QWidget(mplMainWindow)
self.centralwidget.setObjectName("centralwidget")
self.mplContainer = MplWidget(self.centralwidget)
self.mplContainer.setGeometry(QtCore.QRect(259, 20, 861, 741))
self.mplContainer.setObjectName("mplContainer")
self.inputContainer = QtGui.QWidget(self.centralwidget)
self.inputContainer.setGeometry(QtCore.QRect(10, 20, 251, 741))
self.inputContainer.setObjectName("inputContainer")
self.scrollArea = QtGui.QScrollArea(self.inputContainer)
self.scrollArea.setGeometry(QtCore.QRect(0, 160, 241, 581))
self.scrollArea.setFrameShape(QtGui.QFrame.WinPanel)
self.scrollArea.setLineWidth(1)
self.scrollArea.setMidLineWidth(10)
self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setObjectName("scrollArea")
self.scrollAreaWidgetContents = QtGui.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 226, 577))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.scrollLayout = QtGui.QVBoxLayout(self.scrollAreaWidgetContents)
self.gridLayout = QtGui.QGridLayout()
self.gridLayout.setObjectName("formLayout")
self.gridLayout.setColumnStretch(0, 0)
self.gridLayout.setColumnStretch(2, 4)
self.scrollLayout.addLayout(self.gridLayout)
self.scrollLayout.addStretch()
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
mplMainWindow.setCentralWidget(self.centralwidget)
self.btn_cU = QtGui.QPushButton("cU", self.inputContainer)
self.btn_cU.setGeometry(QtCore.QRect(0, 0, 31, 27))
self.btn_cU.setObjectName("btn_cU")
QtCore.QMetaObject.connectSlotsByName(mplMainWindow)
from mplwidget import MplWidget
mplwidget:
from PyQt4.QtGui import *
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
class MplCanvas(FigureCanvas):
def __init__(self):
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
FigureCanvas.__init__(self, self.fig)
FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Preferred)
FigureCanvas.updateGeometry(self)
class MplWidget(QWidget):
def __init__(self, parent = None):
QWidget.__init__(self, parent)
self.main_widget = QWidget(self)
self.canvas = MplCanvas()
self.ntb = NavigationToolbar(self.canvas, self.main_widget)
self.vbl = QGridLayout()
self.vbl.addWidget(self.canvas)
self.vbl.addWidget(self.ntb)
self.setLayout(self.vbl)
As it's not necessary to make extensive mathematical manipulations to solve this issue, I ignored that part. So let's assume that we want to change the slope of several lines using SpinBoxes.
An option would be to make the lines be part of a class that controls the SpinBoxes as well as the matplotlib lines. I called it LineWidget in the code below. When the button is pressed a new LineWidget instance is created and added to a scroll area, where you can manipulate the parameter. Once the parameter changes the line is updated.
Here is a full example where I also simplified the rest of the code.
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import numpy as np
from PyQt4 import QtGui , QtCore
class Viewer(QtGui.QMainWindow):
def __init__(self, parent = None):
super(Viewer, self).__init__(parent)
self.setupUI()
self.ax = self.fig.add_subplot(111)
self.count = 0
self.container = []
self.button.clicked.connect(self.addLine)
def setupUI(self):
self.centralwidget = QtGui.QWidget(self)
self.setCentralWidget(self.centralwidget)
self.centralwidget.setLayout(QtGui.QHBoxLayout())
self.leftWidget = QtGui.QWidget(self)
self.leftWidget.setMinimumWidth(200)
self.leftWidget.setLayout(QtGui.QVBoxLayout())
self.mplWidget = QtGui.QWidget(self)
self.mplWidget.setLayout(QtGui.QVBoxLayout())
self.fig = Figure()
self.canvas = FigureCanvas(self.fig)
self.ntb = NavigationToolbar(self.canvas, self.mplWidget)
self.mplWidget.layout().addWidget(self.canvas)
self.mplWidget.layout().addWidget(self.ntb)
self.button = QtGui.QPushButton("Push")
self.scrollWidget = QtGui.QWidget()
self.scrollLayout = QtGui.QVBoxLayout()
self.scrollWidget.setLayout(self.scrollLayout)
self.scrollLayout.addStretch()
self.scrollArea = QtGui.QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setWidget(self.scrollWidget)
self.leftWidget.layout().addWidget(self.button)
self.leftWidget.layout().addWidget(self.scrollArea)
self.centralwidget.layout().addWidget(self.leftWidget)
self.centralwidget.layout().addWidget(self.mplWidget)
def addLine(self):
b = LineWidget(self.count, self.ax)
self.container.append(b)
self.scrollLayout.insertWidget(self.scrollLayout.count() - 1, b)
self.count += 1
class LineWidget(QtGui.QWidget):
def __init__( self, number, ax, R=0, parent=None, **kwargs):
super(LineWidget, self).__init__(parent)
self.number = number
label = QtGui.QLabel("cU " + str(self.number))
self.spin = QtGui.QDoubleSpinBox()
self.spin.setSingleStep(0.2)
self.spin.setRange(-100,100)
self.setLayout(QtGui.QHBoxLayout())
self.layout().addWidget(label)
self.layout().addWidget(self.spin)
self.R = R
self.t = np.linspace(0,1)
self.f = lambda t, R: R*t
self.ax = ax
self.line, = self.ax.plot([],[], **kwargs)
self.update()
self.spin.valueChanged.connect(self.changed)
def changed(self):
self.R = self.spin.value()
self.update()
def update(self):
self.line.set_data(self.t, self.f(self.t, self.R))
self.ax.relim()
self.ax.autoscale_view()
self.ax.figure.canvas.draw_idle()
app = QtGui.QApplication(sys.argv)
viewer = Viewer()
viewer.show()
sys.exit(app.exec_())
Related
I have a window which holds two plots, one is a 2D plot and the other is a selection of a cut along the y-axis of that plot. I would like to be able to select what cut I want by moving a horizontal bar up to a position and having the 1D plot update. I am having trouble relating the position from the scene where the line item is, to the axes on the plot. The part where I would be figuring this out is in the main window in the function defined plot_position. I am also open to other ideas of how to go about this. Here is a screen-shot for reference:
import sys
from PyQt5.Qt import Qt, QObject, QPen, QPointF
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsLineItem, QGraphicsView, \
QGraphicsScene, QWidget, QHBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import xarray as xr
import numpy as np
class Signals(QObject):
bttnReleased = pyqtSignal(float, float)
class HLineItem(QGraphicsLineItem):
def __init__(self, signals):
super(HLineItem, self).__init__()
self.signals = signals
self.setPen(QPen(Qt.red, 3))
self.setFlag(QGraphicsLineItem.ItemIsMovable)
self.setCursor(Qt.OpenHandCursor)
self.setAcceptHoverEvents(True)
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_y = updated_cursor_position.y() - \
orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(orig_position.x(), updated_cursor_y))
def mouseReleaseEvent(self, event):
x_pos = event.scenePos().x()
y_pos = event.scenePos().y()
self.signals.bttnReleased.emit(x_pos, y_pos)
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
super(PlotCanvas, self).__init__(self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.data = xr.DataArray()
self.axes = None
def plot(self, data):
self.data = data
self.axes = self.fig.add_subplot(111)
self.data.plot(ax=self.axes)
self.axes.set_xlim(-.5, .5)
self.draw()
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.signals = Signals()
x = np.linspace(-1, 1, 51)
y = np.linspace(-1, 1, 51)
z = np.linspace(-1, 1, 51)
xyz = np.meshgrid(x, y, z, indexing='ij')
d = np.sin(np.pi * np.exp(-1 * (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))) * np.cos(np.pi / 2 * xyz[1])
self.xar = xr.DataArray(d, coords={"slit": x, 'perp': y, "energy": z}, dims=["slit", "perp", "energy"])
self.cut = self.xar.sel({"perp": 0}, method='nearest')
self.edc = self.cut.sel({'slit': 0}, method='nearest')
self.canvas = PlotCanvas()
self.canvas_edc = PlotCanvas()
self.canvas.plot(self.cut)
self.canvas_edc.plot(self.edc)
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.line = HLineItem(self.signals)
self.line_pos = [0, 0]
self.layout1 = QHBoxLayout()
self.layout2 = QHBoxLayout()
self.connect_scene()
self.layout1.addWidget(self.view)
self.layout2.addWidget(self.canvas_edc)
self.central = QWidget()
self.main_layout = QHBoxLayout()
self.main_layout.addLayout(self.layout1)
self.main_layout.addLayout(self.layout2)
self.central.setLayout(self.main_layout)
self.setCentralWidget(self.central)
self.signals.bttnReleased.connect(self.plot_position)
def connect_scene(self):
s = self.canvas.figure.get_size_inches() * self.canvas.figure.dpi
self.view.setScene(self.scene)
self.scene.addWidget(self.canvas)
# self.scene.setSceneRect(0, 0, s[0], s[1])
# self.capture_scene_change()
self.line.setLine(0, 0, self.scene.sceneRect().width(), 0)
self.line.setPos(self.line_pos[0], self.line_pos[1])
self.scene.addItem(self.line)
def handle_plotting(self):
self.clearLayout(self.layout2)
self.refresh_edc()
def refresh_edc(self):
self.canvas_edc = PlotCanvas()
self.canvas_edc.plot(self.edc)
self.layout2.addWidget(self.canvas_edc)
def clearLayout(self, layout):
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
def plot_position(self, x, y):
self.line_pos = [x, y]
plot_bbox = self.canvas.axes.get_position()
# something here to relate the hline position in the scene to the axes positions/plot axes
sel_val = min(self.cut.slit, key=lambda f: abs(f - y)) # this should not be y, but rather
# the corresponding value on the y-axis
self.edc = self.cut.sel({"slit": 0}, method='nearest') # this should not be 0, but rather
# the sel_val
self.handle_plotting()
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.mainWindow = MainWindow()
self.mainWindow.setWindowTitle("arpys")
self.mainWindow.show()
def main():
app = App(sys.argv)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
This took me quite a while to figure out. The frustrating part was dealing with Matplotlib's positions of their objects and understanding what classes include what. For a while I was trying to use axes.get_position but this was returning a value for the height that was way too small. I am still not sure for this function what they define as y1 and y0 to give the height. Next I looked at axes.get_window_extent (had a similar problem) and axes.get_tightbbox. the tightbbox function returns the bounding box of the axes including their decorators (xlabel, title, etc) which was better but was actually giving me a height that was beyond the extent I wanted since it included these decorators. Finally, I found that what I really wanted with the length of the spine! In the end I used the spine.get_window_extent() function and this was exactly what I needed. I have attached the updated code.
It was helpful to look at this diagram of the inheritances to see what I was dealing with/looking for.
import sys
from PyQt5.Qt import Qt, QObject, QPen, QPointF
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsLineItem, QGraphicsView, \
QGraphicsScene, QWidget, QHBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import xarray as xr
import numpy as np
class Signals(QObject):
bttnReleased = pyqtSignal(float)
class HLineItem(QGraphicsLineItem):
def __init__(self, signals):
super(HLineItem, self).__init__()
self.signals = signals
self.setPen(QPen(Qt.red, 3))
self.setFlag(QGraphicsLineItem.ItemIsMovable)
self.setCursor(Qt.OpenHandCursor)
self.setAcceptHoverEvents(True)
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_y = updated_cursor_position.y() - \
orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(orig_position.x(), updated_cursor_y))
def mouseReleaseEvent(self, event):
y_pos = event.scenePos().y()
self.signals.bttnReleased.emit(y_pos)
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
super(PlotCanvas, self).__init__(self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.data = xr.DataArray()
self.axes = None
def plot(self, data):
self.data = data
self.axes = self.fig.add_subplot(111)
self.data.plot(ax=self.axes)
self.fig.subplots_adjust(left=0.2)
self.fig.subplots_adjust(bottom=0.2)
self.axes.set_xlim(-.5, .5)
self.draw()
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.signals = Signals()
x = np.linspace(-1, 1, 51)
y = np.linspace(-1, 1, 51)
z = np.linspace(-1, 1, 51)
xyz = np.meshgrid(x, y, z, indexing='ij')
d = np.sin(np.pi * np.exp(-1 * (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))) * np.cos(np.pi / 2 * xyz[1])
self.xar = xr.DataArray(d, coords={"slit": x, 'perp': y, "energy": z}, dims=["slit", "perp", "energy"])
self.cut = self.xar.sel({"perp": 0}, method='nearest')
self.edc = self.cut.sel({'slit': 0}, method='nearest')
self.canvas = PlotCanvas()
self.canvas_edc = PlotCanvas()
self.canvas.plot(self.cut)
self.canvas_edc.plot(self.edc)
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.line = HLineItem(self.signals)
self.line_pos = [0, 0]
self.layout1 = QHBoxLayout()
self.layout2 = QHBoxLayout()
self.connect_scene()
self.layout1.addWidget(self.view)
self.layout2.addWidget(self.canvas_edc)
self.central = QWidget()
self.main_layout = QHBoxLayout()
self.main_layout.addLayout(self.layout1)
self.main_layout.addLayout(self.layout2)
self.central.setLayout(self.main_layout)
self.setCentralWidget(self.central)
self.signals.bttnReleased.connect(self.plot_position)
def connect_scene(self):
s = self.canvas.figure.get_size_inches() * self.canvas.figure.dpi
self.view.setScene(self.scene)
self.scene.addWidget(self.canvas)
self.scene.setSceneRect(0, 0, s[0], s[1])
# self.capture_scene_change()
self.line.setLine(0, 0, self.scene.sceneRect().width(), 0)
self.line.setPos(self.line_pos[0], self.line_pos[1])
self.scene.addItem(self.line)
def handle_plotting(self):
self.clearLayout(self.layout2)
self.refresh_edc()
def refresh_edc(self):
self.canvas_edc = PlotCanvas()
self.canvas_edc.plot(self.edc)
self.layout2.addWidget(self.canvas_edc)
def clearLayout(self, layout):
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
def plot_position(self, y):
rel_pos = lambda x: abs(self.scene.sceneRect().height() - x)
bbox = self.canvas.axes.spines['left'].get_window_extent()
plot_bbox = [bbox.y0, bbox.y1]
if rel_pos(y) < plot_bbox[0]:
self.line.setPos(0, rel_pos(plot_bbox[0]))
elif rel_pos(y) > plot_bbox[1]:
self.line.setPos(0, rel_pos(plot_bbox[1]))
self.line_pos = self.line.pos().y()
size_range = len(self.cut.slit)
r = np.linspace(plot_bbox[0], plot_bbox[1], size_range).tolist()
corr = list(zip(r, self.cut.slit.values))
sel_val = min(r, key=lambda f: abs(f - rel_pos(self.line_pos)))
what_index = r.index(sel_val)
self.edc = self.cut.sel({"slit": corr[what_index][1]}, method='nearest')
self.handle_plotting()
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.mainWindow = MainWindow()
self.mainWindow.setWindowTitle("arpys")
self.mainWindow.show()
def main():
app = App(sys.argv)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I have a PyQt5 GUI application. This application shows a graph (with my default xlim and ylim) that gets updated every second, a real-time graph basically. This functionality I have, but I want to add a NavigationToolbar so one can zoom in/out the graph.
I added the toolbar to my layout and it gets displayed. So far so good. Now I zoom in, the graph gets zoomed in, but once the graph gets periodically updated the xlim and ylim are defaulted again and the zoom is gone. What properties I need to call from the toolbar so I can save them and pass them to my _update_canvas function? I looked at https://matplotlib.org/3.1.0/api/axes_api.html, and noticed the function get_ylim. So i tried as followed:
self._dynamic_ax.set_ylim(self._dynamic_ax.get_ylim()) and self._dynamic_ax.set_ylim(self._dynamic_ax2.get_ylim())
As well as:
self._dynamic_ax.set_navigate(True)
However these didn't work. How can I persist the settings set by NavigationToolbar? Not only the zoom but also the pan.
A minimal runnable code sample:
import sys
from matplotlib.backends.qt_compat import QtCore, QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
buffer_size = 120
t = [t for t in range(buffer_size)]
bitthrough = [t for t in range(buffer_size)]
errors = bitthrough[::-1]
class App(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PCANbus sniffer")
self.table_widget = MyTableWidget(self)
self.setCentralWidget(self.table_widget)
self.setMinimumSize(QtCore.QSize(640, 400))
self.show()
class MyTableWidget(QtWidgets.QWidget):
def __init__(self, parent):
super(QtWidgets.QWidget, self).__init__(parent)
self.layout = QtWidgets.QVBoxLayout(self)
self.tabs = QtWidgets.QTabWidget()
self.tab_graph = QtWidgets.QWidget()
self.tab_info = QtWidgets.QWidget()
self.tabs.addTab(self.tab_graph, "PCANbus occupation")
self.tabs.addTab(self.tab_info, "PCANbus information")
self.tab_graph.layout = QtWidgets.QVBoxLayout(self)
self.dynamic_canvas = FigureCanvas(Figure(figsize=(6, 4)))
self.tab_graph.layout.addWidget(self.dynamic_canvas)
self.toolbar = NavigationToolbar(self.dynamic_canvas, self)
self.tab_graph.layout.addWidget(self.toolbar)
self._dynamic_ax = self.dynamic_canvas.figure.subplots()
self._dynamic_ax.set_xlabel("time (s)")
self._dynamic_ax.set_xlim(-5, 125)
self._dynamic_ax.set_ylabel("Throughput (%)", color="black")
self._dynamic_ax.set_ylim(-5, 120)
self._dynamic_ax.tick_params(axis="y", labelcolor="black")
# self._dynamic_ax.set_navigate(True)
self._dynamic_ax2 = self._dynamic_ax.twinx()
self._dynamic_ax2.set_ylabel("Errors (%)", color="blue")
self._dynamic_ax2.set_ylim(-4, 100)
self._dynamic_ax2.tick_params(axis="y", labelcolor="blue")
#self._dynamic_ax2.set_navigate(True)
self.tab_graph.setLayout(self.tab_graph.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self._update_canvas)
self.timer.start(1000)
def _update_canvas(self):
self._dynamic_ax.clear()
self._dynamic_ax.plot(t, bitthrough, color="black")
self._dynamic_ax.set_xlabel("time (s)")
self._dynamic_ax.set_ylabel("Throughput (%)", color="black")
self._dynamic_ax.set_ylim(-5, 120) #self._dynamic_ax.get_ylim())
self._dynamic_ax.tick_params(axis="y", labelcolor="black")
self._dynamic_ax2.clear()
self._dynamic_ax2.plot(t, errors, color="blue")
self._dynamic_ax2.set_ylabel("Errors", color="blue")
self._dynamic_ax2.set_ylim(-4, 100) #self._dynamic_ax2.get_ylim())
self._dynamic_ax2.tick_params(axis="y", labelcolor="blue")
self._dynamic_ax.figure.canvas.draw_idle()
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = App()
app.show()
qapp.exec_()
My real _update_canvas function:
def _update_canvas(self):
wh_green = [a <= b for a, b in zip(bitthrough, llvl)]
wh_orange = [a > b and a <= c
for a, b, c in zip(bitthrough, llvl, lvl)]
wh_red = [a > b for a, b, in zip(bitthrough, lvl)]
# self._dynamic_ax.clear()
# self._dynamic_ax2.clear()
self._dynamic_ax.fill_between(
t, 0, bitthrough, where=wh_red, color="red", interpolate=True
)
self._dynamic_ax.fill_between(
t, 0, bitthrough, where=wh_orange, color="orange", interpolate=True
)
self._dynamic_ax.fill_between(
t, 0, bitthrough, where=wh_green, color="green", interpolate=True
)
# self._dynamic_ax.plot(t, bitthrough, color="black")
# self._dynamic_ax.set_xlabel("time (s)")
# self._dynamic_ax.set_ylabel("Throughput (%)", color="black")
# #self._dynamic_ax.set_ylim(self._dynamic_ax.get_ylim())
# self._dynamic_ax.tick_params(axis="y", labelcolor="black")
# self._dynamic_ax2.plot(t, errors, color="blue")
# self._dynamic_ax2.set_ylabel("Errors", color="blue")
# #self._dynamic_ax2.set_ylim(self._dynamic_ax2.get_ylim())
# self._dynamic_ax2.tick_params(axis="y", labelcolor="blue")
self._plot1.set_ydata(bitthrough)
self._plot2.set_ydata(errors)
# logging.debug("redrawing graph!!")
self._dynamic_ax.figure.canvas.draw_idle()
The solution of #DizietAsahi doesn't work while using fill_between. The area gets overwritten and not cleared. So they are displayed on top of eachother.
My advice would be to not clear the figure at each update. Instead, store a reference to the Line2D artists created by plot() and update the {x|y}data (using set_data() or set_ydata()) in your update function.
import sys
from matplotlib.backends.qt_compat import QtCore, QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import numpy as np
buffer_size = 120
t = np.linspace(0, 100, buffer_size)
bitthrough = 120*np.random.random(size=(buffer_size,))
errors = bitthrough[::-1]
class App(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PCANbus sniffer")
self.table_widget = MyTableWidget(self)
self.setCentralWidget(self.table_widget)
self.setMinimumSize(QtCore.QSize(640, 400))
self.show()
class MyTableWidget(QtWidgets.QWidget):
def __init__(self, parent):
super(QtWidgets.QWidget, self).__init__(parent)
self.layout = QtWidgets.QVBoxLayout(self)
self.tabs = QtWidgets.QTabWidget()
self.tab_graph = QtWidgets.QWidget()
self.tab_info = QtWidgets.QWidget()
self.tabs.addTab(self.tab_graph, "PCANbus occupation")
self.tabs.addTab(self.tab_info, "PCANbus information")
self.tab_graph.layout = QtWidgets.QVBoxLayout(self)
self.dynamic_canvas = FigureCanvas(Figure(figsize=(6, 4)))
self.tab_graph.layout.addWidget(self.dynamic_canvas)
self.toolbar = NavigationToolbar(self.dynamic_canvas, self)
self.tab_graph.layout.addWidget(self.toolbar)
self._dynamic_ax = self.dynamic_canvas.figure.subplots()
self._dynamic_ax.set_xlabel("time (s)")
self._dynamic_ax.set_xlim(-5, 125)
self._dynamic_ax.set_ylabel("Throughput (%)", color="black")
self._dynamic_ax.set_ylim(-5, 120)
self._dynamic_ax.tick_params(axis="y", labelcolor="black")
# self._dynamic_ax.set_navigate(True)
self._dynamic_ax2 = self._dynamic_ax.twinx()
self._dynamic_ax2.set_ylabel("Errors (%)", color="blue")
self._dynamic_ax2.set_ylim(-4, 100)
self._dynamic_ax2.tick_params(axis="y", labelcolor="blue")
#self._dynamic_ax2.set_navigate(True)
##
## Create plots here (initially empty)
##
self._plot1, = self._dynamic_ax.plot(t, np.empty(shape=(buffer_size,)), color="black")
self._plot2, = self._dynamic_ax2.plot(t, np.empty(shape=(buffer_size,)), color="blue")
self._fill1 = self._dynamic_ax.fill_between(t, 0, bitthrough, color="orange")
self.tab_graph.setLayout(self.tab_graph.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self._update_canvas)
self.timer.start(1000)
def _update_canvas(self):
bitthrough = 120*np.random.random(size=(buffer_size, ))
errors = bitthrough[::-1]
##
## update the content of the plots here, without clearing the figure
##
self._plot1.set_ydata(bitthrough)
self._plot2.set_ydata(errors)
self._fill1.remove()
self._fill1 = self._dynamic_ax.fill_between(t, 0, bitthrough, color="orange")
self._dynamic_ax.figure.canvas.draw_idle()
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = App()
app.show()
qapp.exec_()
EDIT
I've added some code for handling fill_between().
fill_between() returns a PolyCollection which is a pain to update, so the best option there is to remove the PolyCollection and re-create it at each update (but not clear the whole figure).
I have an issue with a matplotlib figure that I've embedded in a pyqt5 QScrollArea.
My issue is when I'm scrolling down, it is like only the top of the matplotlib has been updated.
For instance, in the image below, the curves below the curve number 35 are not updated properly.
This only happens with mac OS but it run well on Windows 10.
I don't find the issue in the code.
Update
To me, it looks like the matplotlib figure doesn't match with the QSrollBar. I try to explain more. When I scroll down with the Qsrollbar, the Matplotlib figure is going down but not as fast as the scrollbar. I see that with the number of curve that is increasing when I scroll down. It is like the Qscrollbar go down too fast with respect to the matplotlib figure update.
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
import time
class Viewer(QMainWindow):
def __init__(self, parent=None):
super(Viewer, self).__init__()
self.parent = parent
#######################################
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.mainVBOX_param_scene = QVBoxLayout()
self.mascene = plot(self)
self.paramPlotV = QVBoxLayout()
self.horizontalSliders = QScrollBar(Qt.Horizontal)
self.horizontalSliders.setFocusPolicy(Qt.StrongFocus)
self.horizontalSliders.valueChanged.connect(self.update_plot)
self.horizontalSliders.setMinimum(0)
self.horizontalSliders.setMaximum(1)
self.paramPlot = QHBoxLayout()
l_gain = QLabel('Gain')
self.e_gain = QLineEdit('5')
l_win = QLabel('Window')
self.e_win = QLineEdit('10')
l_spacing = QLabel('vertical spacing')
self.e_spacing = QLineEdit('10')
l_linewidth = QLabel('linewidth')
self.e_linewidth = QLineEdit('1')
self.e_gain.returnPressed.connect(self.update_plot)
self.e_win.returnPressed.connect(self.udpate_plot_plus_slider)
self.e_spacing.returnPressed.connect(self.update_plot)
self.e_linewidth.returnPressed.connect(self.update_plot)
self.paramPlot.addWidget(l_gain)
self.paramPlot.addWidget(self.e_gain)
self.paramPlot.addWidget(l_win)
self.paramPlot.addWidget(self.e_win)
self.paramPlot.addWidget(l_spacing)
self.paramPlot.addWidget(self.e_spacing)
self.paramPlot.addWidget(l_linewidth)
self.paramPlot.addWidget(self.e_linewidth)
self.paramPlotV.addWidget(self.horizontalSliders)
self.paramPlotV.addLayout(self.paramPlot)
self.mainVBOX_param_scene.addWidget(self.mascene)
self.mainVBOX_param_scene.addLayout(self.paramPlotV)
self.centralWidget.setLayout(self.mainVBOX_param_scene)
self.Fs = 1024
self.Sigs_dict = np.random.rand(50,20*self.Fs)
self.t = np.arange(self.Sigs_dict.shape[1])/self.Fs
self.parent.processEvents()
self.update()
def updateslider(self):
self.horizontalSliders.setMinimum(0)
self.horizontalSliders.setMaximum(np.ceil(self.t[-1]/int(self.e_win.text()))-1)
self.horizontalSliders.setPageStep(1)
self.horizontalSliders.update()
def udpate_plot_plus_slider(self):
self.updateslider()
self.mascene.update()
def update_plot(self):
t0 = time.time()
self.mascene.update()
print('time old:', time.time()-t0)
def update(self):
self.updateslider()
self.mascene.modify_sigs()
self.mascene.update()
class plot(QGraphicsView):
def __init__(self, parent=None):
super(plot, self).__init__(parent)
self.parent = parent
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
self.figure = plt.figure(facecolor='white')#Figure()
self.canvas = FigureCanvas(self.figure)
self.widget = QWidget()
self.widget.setLayout(QVBoxLayout())
self.widget.layout().setContentsMargins(0, 0, 0, 0)
self.widget.layout().setSpacing(0)
self.scroll = QScrollArea(self.widget)
self.scroll.setWidget(self.canvas)
layout = QVBoxLayout()
layout.addWidget(self.scroll)
self.setLayout(layout)
def modify_sigs(self):
self.Sigs_dict = self.parent.Sigs_dict
self.t = self.parent.t
self.Fs= self.parent.Fs
def update(self):
win_num = self.parent.horizontalSliders.value()
self.figure.clear()
plt.figure(self.figure.number)
plt.subplots_adjust(left=0.1, bottom=0.01, right=1, top=1, wspace=0.0 , hspace=0.0 )
self.axes = plt.subplot(1, 1, 1)
gain = float(self.parent.e_gain.text())
win= float(self.parent.e_win.text())
self.spacing = float(self.parent.e_spacing.text())
linewidth = float(self.parent.e_linewidth.text())
ts = int(win*(win_num) * self.Fs)
te = ts + int(win * self.Fs)
if te > len(self.t):
te=len(self.t)
for i in range(self.Sigs_dict.shape[0]):
line, = plt.plot(self.t[ts:te], gain*(self.Sigs_dict[i,ts:te]-np.mean(self.Sigs_dict[i,ts:te]))+i*self.spacing, linewidth=linewidth )
self.axes.autoscale(enable=True, axis='both', tight=True)
self.axes.set_ylim((-self.spacing,(self.Sigs_dict.shape[0]+1)*self.spacing))
self.axes.set_xlim((ts/ self.Fs, ts / self.Fs + win ))
self.axes.set_yticks(np.arange(self.Sigs_dict.shape[0]) * self.spacing)
self.axes.set_yticklabels([str(n) for n in np.arange(self.Sigs_dict.shape[0])])
self.canvas.setGeometry(0, 0, self.parent.width()-100, (self.parent.height()-100)*self.spacing)
self.canvas.draw()
def main():
app = QApplication(sys.argv)
app.setStyle('Windows')
ex = Viewer(app)
ex.showMaximized()
sys.exit(app.exec())
if __name__ == '__main__':
main()
I update the version of my matplotlib module from 3.1.1 to 3.2.0rc1 and it solves the issue.
I'm very new to PyQt and I'm finding ordering the widgets in the window pretty hard.
I have a plot that updates in real-time, a table that updates every time that the user clicks on the plot, and two buttons (start/stop).
The current layout is behaving this way:
I want it to look something like this:
Where "other widgets" correspond to future Widgets I'll add when this is working (such as a slider or a dropdown list). The table should also be as thin as possible, to maximize the size of the plot and don't have the table occupying unnecessary space. My current code is the following one (it's self-contained):
import sys
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
def onclick(event):
global clicks
clicks.append(event.xdata)
return
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super(ApplicationWindow, self).__init__()
self._title = 'Prueba real-time'
self.setWindowTitle(self._title)
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QHBoxLayout(self._main)
dynamic_canvas = FigureCanvas(Figure(figsize=(10, 10)))
layout.addWidget(dynamic_canvas)
self._dynamic_ax = dynamic_canvas.figure.subplots()
dynamic_canvas.figure.canvas.mpl_connect('button_press_event', onclick)
self._dynamic_ax.grid()
self._timer = dynamic_canvas.new_timer(
100, [(self._update_window, (), {})])
self._timer.start()
button_stop = QtWidgets.QPushButton('Stop', self)
layout.addWidget(button_stop)
button_stop.clicked.connect(self.button_pressed)
button_start = QtWidgets.QPushButton('Start', self)
layout.addWidget(button_start)
button_start.clicked.connect(self.button_pressed)
self.table_clicks = QtWidgets.QTableWidget()
self.table_clicks.setRowCount(0)
self.table_clicks.setColumnCount(2)
layout.addWidget(self.table_clicks)
def button_pressed(self):
if self.sender().text() == 'Stop':
self._timer.stop()
if self.sender().text() == 'Start':
self._timer.start()
def _update_window(self):
self._dynamic_ax.clear()
global x, y1, y2, y3, N, count_iter, last_number_clicks
x.append(x[count_iter] + 0.01)
y1.append(np.random.random())
idx_inf = max([count_iter-N, 0])
if last_number_clicks < len(clicks):
for new_click in clicks[last_number_clicks:(len(clicks))]:
rowPosition = self.table_clicks.rowCount()
self.table_clicks.insertRow(rowPosition)
self.table_clicks.setItem(rowPosition,0, QtWidgets.QTableWidgetItem(str(new_click)))
self.table_clicks.setItem(rowPosition,1, QtWidgets.QTableWidgetItem("Descripcion"))
last_number_clicks = len(clicks)
self._dynamic_ax.plot(x[idx_inf:count_iter], y1[idx_inf:count_iter],'-o', color='b')
count_iter += 1
self._dynamic_ax.figure.canvas.draw()
#%%
if __name__ == "__main__":
pressed_key = {}
clicks = []
last_number_clicks = len(clicks)
N = 25
y1 = [np.random.random()]
x = [0]
count_iter = 0
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
The structure that you want is a grid so you should use a QGridLayout.
QGridLayout has the addWidget() method where you can indicate the row and column where the widget will be placed, the same with addLayout()
void QGridLayout::addWidget(QWidget *widget, int row, int column, Qt::Alignment alignment = ...)
void QGridLayout::addLayout(QLayout *layout, int row, int column, Qt::Alignment alignment = ...)
In your case the structure should be the following:
QGridLayout
├── Plot (0, 0)
├── Table (0, 1)
├── Other Widgets (1, 0)
└── QVBoxLayout (1, 1)
├── button_start
└── button_stop
Then you can indicate the weight of each column, that is, say that the column width will have a proportional width with respect to column 2 using setColumnStretch():
layout.setColumnStretch(0, 2)
layout.setColumnStretch(1, 1)
Considering the above we have the following:
import sys
import numpy as np
from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
def onclick(event):
global clicks
clicks.append(event.xdata)
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super(ApplicationWindow, self).__init__()
self._title = 'Prueba real-time'
self.setWindowTitle(self._title)
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
dynamic_canvas = FigureCanvas(Figure(figsize=(10, 10)))
self._dynamic_ax = dynamic_canvas.figure.subplots()
dynamic_canvas.figure.canvas.mpl_connect('button_press_event', onclick)
self._dynamic_ax.grid()
self._timer = dynamic_canvas.new_timer(
100, [(self._update_window, (), {})])
self._timer.start()
button_stop = QtWidgets.QPushButton('Stop', self)
button_stop.clicked.connect(self._timer.stop)
button_start = QtWidgets.QPushButton('Start', self)
button_start.clicked.connect(self._timer.start)
self.table_clicks = QtWidgets.QTableWidget(0, 2)
self.table_clicks.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
other_widget = QtWidgets.QLabel("Other widgets",
font=QtGui.QFont("Times", 60, QtGui.QFont.Bold),
alignment=QtCore.Qt.AlignCenter)
# layouts
layout = QtWidgets.QGridLayout(self._main)
layout.addWidget(dynamic_canvas, 0, 0)
layout.addWidget(self.table_clicks, 0, 1)
layout.addWidget(other_widget, 1, 0)
button_layout = QtWidgets.QVBoxLayout()
button_layout.addWidget(button_stop)
button_layout.addWidget(button_start)
layout.addLayout(button_layout, 1, 1)
layout.setColumnStretch(0, 2)
layout.setColumnStretch(1, 1)
def _update_window(self):
self._dynamic_ax.clear()
global x, y1, y2, y3, N, count_iter, last_number_clicks
x.append(x[count_iter] + 0.01)
y1.append(np.random.random())
idx_inf = max([count_iter-N, 0])
if last_number_clicks < len(clicks):
for new_click in clicks[last_number_clicks:(len(clicks))]:
rowPosition = self.table_clicks.rowCount()
self.table_clicks.insertRow(rowPosition)
self.table_clicks.setItem(rowPosition,0, QtWidgets.QTableWidgetItem(str(new_click)))
self.table_clicks.setItem(rowPosition,1, QtWidgets.QTableWidgetItem("Descripcion"))
last_number_clicks = len(clicks)
self._dynamic_ax.plot(x[idx_inf:count_iter], y1[idx_inf:count_iter],'-o', color='b')
count_iter += 1
self._dynamic_ax.figure.canvas.draw()
#%%
if __name__ == "__main__":
pressed_key = {}
clicks = []
last_number_clicks = len(clicks)
N = 25
y1 = [np.random.random()]
x = [0]
count_iter = 0
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
sys.exit(qapp.exec_())
I would personally separate the different parts of the program in their own class and use several vertical and horizontal layouts to position the widgets. This especially helps if you want to add features. Especially for large applications this helps to keep the MainWindow, which always tends to become a mess, to be more simple and easier to understand
import sys
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
def onclick(event):
global clicks
clicks.append(event.xdata)
return
class TableWidget(QtWidgets.QWidget):
def __init__(self,parent,*args,**kwargs):
super().__init__(*args,**kwargs)
self.parent = parent
layout = QtWidgets.QVBoxLayout(self)
self.table_clicks = QtWidgets.QTableWidget()
self.table_clicks.setRowCount(0)
self.table_clicks.setColumnCount(2)
layout.addWidget(self.table_clicks)
button_widget = QtWidgets.QWidget(self)
layout.addWidget(button_widget)
button_layout = QtWidgets.QHBoxLayout(button_widget)
button_stop = QtWidgets.QPushButton('Stop', self)
button_layout.addWidget(button_stop)
button_stop.clicked.connect(self.parent.plot_widget.button_pressed)
button_start = QtWidgets.QPushButton('Start', self)
button_layout.addWidget(button_start)
button_start.clicked.connect(self.parent.plot_widget.button_pressed)
class PlotWidget(QtWidgets.QWidget):
def __init__(self,parent,*args,**kwargs):
super().__init__(parent,*args,**kwargs)
self.parent = parent
layout = QtWidgets.QVBoxLayout(self)
dynamic_canvas = FigureCanvas(Figure(figsize=(10, 10)))
layout.addWidget(dynamic_canvas)
self._dynamic_ax = dynamic_canvas.figure.subplots()
dynamic_canvas.figure.canvas.mpl_connect('button_press_event', onclick)
self._dynamic_ax.grid()
self._timer = dynamic_canvas.new_timer(
100, [(self._update_window, (), {})])
self._timer.start()
def button_pressed(self):
if self.sender().text() == 'Stop':
self._timer.stop()
if self.sender().text() == 'Start':
self._timer.start()
def _update_window(self):
self._dynamic_ax.clear()
global x, y1, y2, y3, N, count_iter, last_number_clicks
x.append(x[count_iter] + 0.01)
y1.append(np.random.random())
idx_inf = max([count_iter-N, 0])
if last_number_clicks < len(clicks):
for new_click in clicks[last_number_clicks:(len(clicks))]:
rowPosition = self.parent.table_widget.table_clicks.rowCount()
self.parent.table_widget.table_clicks.insertRow(rowPosition)
self.parent.table_widget.table_clicks.setItem(rowPosition,0, QtWidgets.QTableWidgetItem(str(new_click)))
self.parent.table_widget.table_clicks.setItem(rowPosition,1, QtWidgets.QTableWidgetItem("Descripcion"))
last_number_clicks = len(clicks)
self._dynamic_ax.plot(x[idx_inf:count_iter], y1[idx_inf:count_iter],'-o', color='b')
count_iter += 1
self._dynamic_ax.figure.canvas.draw()
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super(ApplicationWindow, self).__init__()
self._title = 'Prueba real-time'
self.setWindowTitle(self._title)
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
main_layout = QtWidgets.QHBoxLayout(self._main)
self.plot_widget = PlotWidget(self)
main_layout.addWidget(self.plot_widget)
self.table_widget = TableWidget(self)
main_layout.addWidget(self.table_widget)
#%%
if __name__ == "__main__":
pressed_key = {}
clicks = []
last_number_clicks = len(clicks)
N = 25
y1 = [np.random.random()]
x = [0]
count_iter = 0
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
There are a few parts to this problem, so let me summarize. I'm trying to be able to create an animation in a PySide window, and then later destroy and reanimate it based on new information. I've created a sample script to demonstrate this, but it's a bit long, so here's an outline:
Create a QtGui main window using PySide
Create a widget in the window for drawing the animation
Animate the widget using FuncAnimation
On a button press:
Delete the widget and animation
Recreate the widget using different parameters for plotting
Reanimate
Everything works until the button press, and I get the following stack trace:
Traceback (most recent call last):
File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_qt4.py", line 366, in idle_draw
self.draw()
File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_qt4agg.py", line 149, in draw
self.update()
RuntimeError: Internal C++ object (MplWidget) already deleted.
How can I get rid of the current animation so that I can redraw a separate one? Here's the code.
from matplotlib import pyplot as p
from mpl_toolkits.mplot3d import Axes3D # #UnusedImport
from PySide import QtGui, QtCore
import matplotlib
import matplotlib.animation as animation
import sys
# specify the use of PySide
matplotlib.rcParams['backend.qt4'] = "PySide"
# import the figure canvas for interfacing with the backend
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg \
as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
from math import pi, cos, sin
class Ui_MainWindow(object):
def setupUi(self, MainWindow, direction, maxRadius):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 500)
self.centralwidget = QtGui.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.pushButton = QtGui.QPushButton(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum,
QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.pushButton.sizePolicy().hasHeightForWidth())
self.pushButton.setSizePolicy(sizePolicy)
self.pushButton.setMaximumSize(QtCore.QSize(150, 16777215))
self.pushButton.setObjectName("pushButton")
self.pushButton.setText("Change Direction")
self.horizontalLayout.addWidget(self.pushButton)
self.frame = QtGui.QFrame(self.centralwidget)
self.frame.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame.setFrameShadow(QtGui.QFrame.Raised)
self.frame.setObjectName("frame")
self.gridLayout = QtGui.QGridLayout(self.frame)
self.gridLayout.setObjectName("gridLayout")
self.horizontalLayout.addWidget(self.frame)
# ------
self.mplWidget = MplWidget(self.frame, direction, maxRadius)
# ------
MainWindow.setCentralWidget(self.centralwidget)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.direction = 'up'
self.maxRadius = 0.3
self.ui.setupUi(self, self.direction, self.maxRadius)
self.ui.pushButton.clicked.connect(self.changeStuff)
self.animation = self.ui.mplWidget.animate()
def changeStuff(self):
del self.animation
self.ui.mplWidget.deleteLater()
dirs = {'up' :'down', 'down':'up'}
rads = {0.3:1, 1:0.3}
self.direction = dirs[self.direction]
self.maxRadius = rads[self.maxRadius]
self.ui.mplWidget = MplWidget(self.ui.frame, self.direction,
self.maxRadius)
self.animation = self.ui.mplWidget.animate()
class MplWidget(FigureCanvas):
def __init__(self, parent=None, direction='up', maxRadius=0.3):
self.figure = Figure()
super(MplWidget, self).__init__(self.figure)
self.setParent(parent)
self.axes = self.figure.add_subplot(111, projection='3d')
self.axes.set_xlabel("x label")
self.axes.set_ylabel("y label")
self.axes.set_zlabel("z label")
self.axes.set_xlim3d([-1, 1])
self.axes.set_ylim3d([-1, 1])
self.axes.set_zlim3d([-1, 1])
self.axes.set_aspect('equal')
if direction == 'up':
self.c = 1
elif direction == 'down':
self.c = -1
else:
self.c = 1
self.maxRadius = maxRadius
self.frames = 50
self.plot_handle = self.func_plot(self.frames)
def func_plot(self, z):
z /= float(self.frames) * self.c
theta = np.arange(0, 2 * pi + pi / 50, pi / 50)
xdata = self.maxRadius * z * np.array([cos(q) for q in theta])
ydata = self.maxRadius * z * np.array([sin(q) for q in theta])
zdata = z * np.ones(np.shape(xdata))
if not hasattr(self, 'plot_handle'):
plot_handle = self.axes.plot(xdata, ydata, zdata)[0]
else:
plot_handle = self.plot_handle
plot_handle.set_data(xdata, ydata)
plot_handle.set_3d_properties(zdata)
return plot_handle
def animate(self):
return animation.FuncAnimation(
fig=self.figure, func=self.func_plot, frames=self.frames,
interval=1000.0 / self.frames, blit=False)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Version information:
matplotlib - 1.2.1
PySide - 1.1.2
Python - 2.7.4
Edit:
I followed the matplotlib Installation Instructions to do a source build from git. I now have version 1.4.x installed. However, the original problem persists.
This is related to a bug in matplotlib qtbackend that tears things down in the wrong order (see here). Adding
self.ui.mplWidget.close_event()
in ChangeStuff takes care of the exception you have, but the newly created canvas isn't shown.
On the other hand, I don't understand why any canvas is shown at all as the widget is never added to a layout. If you tweak the set up a bit and explicitly add/remove the widget to the gridLayout it does what you want:
from mpl_toolkits.mplot3d import Axes3D # #UnusedImport
from PySide import QtGui, QtCore
import matplotlib
import matplotlib.animation as animation
import sys
# specify the use of PySide
matplotlib.rcParams['backend.qt4'] = "PySide"
# import the figure canvas for interfacing with the backend
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg \
as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
from math import pi, cos, sin
class Ui_MainWindow(object):
def setupUi(self, MainWindow, direction, maxRadius):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 500)
self.centralwidget = QtGui.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtGui.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.pushButton = QtGui.QPushButton(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum,
QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.pushButton.sizePolicy().hasHeightForWidth())
self.pushButton.setSizePolicy(sizePolicy)
self.pushButton.setMaximumSize(QtCore.QSize(150, 16777215))
self.pushButton.setObjectName("pushButton")
self.pushButton.setText("Change Direction")
self.horizontalLayout.addWidget(self.pushButton)
self.frame = QtGui.QFrame(self.centralwidget)
self.frame.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame.setFrameShadow(QtGui.QFrame.Raised)
self.frame.setObjectName("frame")
self.gridLayout = QtGui.QGridLayout(self.frame)
self.gridLayout.setObjectName("gridLayout")
self.horizontalLayout.addWidget(self.frame)
# ------
self.mplWidget = MplWidget(None, direction, maxRadius)
self.gridLayout.addWidget(self.mplWidget)
# ------
MainWindow.setCentralWidget(self.centralwidget)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.ui = Ui_MainWindow()
self.direction = 'up'
self.maxRadius = 0.3
self.ui.setupUi(self, self.direction, self.maxRadius)
self.ui.pushButton.clicked.connect(self.changeStuff)
self.animation = self.ui.mplWidget.animate()
def changeStuff(self):
self.ui.mplWidget.close_event() # mpl clean up
self.ui.mplWidget.deleteLater() # QT cleanup
self.ui.gridLayout.removeWidget(self.ui.mplWidget)
dirs = {'up': 'down', 'down': 'up'}
rads = {0.3: 1, 1: 0.3}
self.direction = dirs[self.direction]
self.maxRadius = rads[self.maxRadius]
self.ui.mplWidget = MplWidget(self.ui.frame, self.direction,
self.maxRadius)
self.ui.gridLayout.addWidget(self.ui.mplWidget)
self.animation = self.ui.mplWidget.animate()
print self.ui.frame.children()
print 'finished change stuff'
class MplWidget(FigureCanvas):
def __init__(self, parent=None, direction='up', maxRadius=0.3):
self.figure = Figure()
super(MplWidget, self).__init__(self.figure)
self.setParent(parent)
self.axes = self.figure.add_subplot(111, projection='3d')
self.axes.set_xlabel("x label")
self.axes.set_ylabel("y label")
self.axes.set_zlabel("z label")
self.axes.set_xlim3d([-1, 1])
self.axes.set_ylim3d([-1, 1])
self.axes.set_zlim3d([-1, 1])
self.axes.set_aspect('equal')
if direction == 'up':
self.c = 1
elif direction == 'down':
self.c = -1
else:
self.c = 1
self.maxRadius = maxRadius
self.frames = 50
self.plot_handle = self.func_plot(self.frames)
def func_plot(self, z):
z /= float(self.frames) * self.c
theta = np.arange(0, 2 * pi + pi / 50, pi / 50)
xdata = self.maxRadius * z * np.array([cos(q) for q in theta])
ydata = self.maxRadius * z * np.array([sin(q) for q in theta])
zdata = z * np.ones(np.shape(xdata))
if not hasattr(self, 'plot_handle'):
plot_handle = self.axes.plot(xdata, ydata, zdata)[0]
else:
plot_handle = self.plot_handle
plot_handle.set_data(xdata, ydata)
plot_handle.set_3d_properties(zdata)
return plot_handle
def animate(self):
return animation.FuncAnimation(
fig=self.figure, func=self.func_plot, frames=self.frames,
interval=1000.0 / self.frames, blit=False)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())