2
votes

I'm creating a live data monitor GUI with PyQt4 and matplotlib to create the plots. The GUI displays multiple plots at a time (around 6 or 7). To give the GUI thread more time and slightly better response time I only do the drawing canvas.draw() and all other plotting commands I do in the thread that updates the plot data. So in the non-GUI thread I do commands like line.set_ydata, ax.set_ylim, and other things that might need to be updated.

The two threads have access to the figure and canvas objects through a dictionary that is passed to the two threads at initialization. When the non-GUI thread gets data and updates the plot, it then signals the GUI thread to redraw the canvas using Qts signals (auto-connection). My threading experience tells me that I should use a lock or make sure the non-GUI thread is blocked on the redraw in some way, but in my coding rush I never put it in and forgot about it until now. The key point to this situation is that I want to see every update of the plot, not redraw in the middle of the other thread updating or even miss an update (if that makes sense). Currently, I think I'm just getting lucky with timing and things seem to be working ok.

Another thing that might be helpful to know is that I'm creating threads by moving a QObject to a QThread by using moveToThread.

My questions are:

  • Am I just getting lucky or is Qt just doing something magical?
  • What is the best way to accomplish the blocking on the matplotlib canvas/figure?

I should probably note that this was my first attempt at making the GUI more responsive (moving matplotlib commands into the data thread) and I may be moving to a blit animation style drawing of only updating the parts of the plot that change. But I'm still curious as to how lucky I am.

Thanks for any help.

Update/Clarification/Continuation from comments: I wanted the entire monitor system to be easily changed/updated by scientists who may only be familiar with matlab and maybe matplotlib. I'm not completely against changing to pyqwt for plotting for speed. And in terms of frames per second, I don't really need a lot at all since the data that is being plotted only comes in every 0.5 seconds nominal (0.2 seconds at the fastest). The GUI responsiveness just seems to "eat **" because there are so many plots. I've done a proof of concept hacking of my code with matplotlib blitting and it seems to help a ton, pyqwt will happen if needed. My previous questions still stand.

1
matplotlib is very slow without the use of blitting. Even with it, for data monitoring i recommend to use something else. Have a look at packages.python.org/guiqwt/index.html - tillsten
I did look at qwt since a lot of people said it was much faster than matplotlib. The reason I didn't want to move to it right away was to make it easier for people (scientists) who are familiar with matplotlib (actually matlab) to add plots to the overall data monitor package. And I haven't looked into it too much, but if its not too hard maybe I'll make an interface so its similar to using matplotlib. - djhoese
I absolutely love matplotlib, i use it for all my publication graphichs. But for interactive usage you have to use blitting for reasonable speed, which is a hassle, else the speed is too bad for my needs (around 20 fps for multiple plots). - tillsten
Implementing something similar right now. Dave, do you think you can share some code? Here is my thoughts. You have to use multiple classes, one would be based on PyQt's QDialog or similar for the GUI output. The other one would be worker class based on QThread. Once QThread is done plotting it would pass figure and canvas instances to the GUI object using PyQt signaling engine. I tried combining QDialog and QThread into one class but apparently you can not do it. (I am sure you can do it but it is easier to use 2 separate classes) - Barmaley
@Barmaley this question is almost 5 years old. I'm not sure my code from this even works with newer versions of mpl. I suggest you do more research on how Qt threads are supposed to be used (QObject that is "owned" by a QThread). You probably don't want to be passing the figure and canvas through signals. They are large complex objects and the canvas is actually a GUI element so it stay in the GUI thread. Also, why QDialog? Just make a normal QWidget with a mpl canvas in it. After some research if you still need guidance you should make your own stackoverflow question or ask the mpl mail list. - djhoese

1 Answers

1
votes

I stumbled upon similar problem. I had a lot of plots to draw (~250) so my GUI window would eventually show up as hanging in Windows. I modified figure class to perform plotting as a separate thread. The result - my GUI does not hang anymore, plot window shows up once the plotting is done. To use it, you create PlotDialog instance and call draw_plots method with plot_data argument. plot_data is a list of dictionaries, each dictionary represents subplot. Each subplot has following keys (and corresponding data): title, xlabel, ylabel and data. Hope that makes sense.

Here is my code:

import math

from PyQt4 import QtCore, QtGui

from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar


class MyFigure(Figure, QtCore.QThread):
    def __init__(self, parent, *args, **kwargs):
        QtCore.QThread.__init__(self, parent)
        Figure.__init__(self, *args, **kwargs)

        self.plot_data = list()

    def start_plotting_thread(self, plot_data, on_finish=None):
        """ Start plotting """
        self.plot_data = plot_data

        if on_finish is not None:
            self.finished.connect(on_finish)

        self.start()

    def run(self):
        """ Run as a thread """
        # Figure out rows and columns
        total_plots = len(self.plot_data)

        columns = int(math.sqrt(total_plots))
        if columns < 1:
            columns = 1

        rows = int(total_plots / columns)
        if (total_plots % columns) > 0:
            rows += 1
        if rows < 1:
            rows = 1

        # Plot Data
        for plot_index, _plot_data in enumerate(self.plot_data):
            plot_number = plot_index + 1
            args = (rows, columns, plot_number)
            kwargs = {
                'title': _plot_data['title'],
                'xlabel': _plot_data['xlabel'],
                'ylabel': _plot_data['ylabel']
            }

            figure = self.add_subplot(*args, **kwargs)

            figure.plot(_plot_data['data'])


class PlotDialog(QtGui.QDialog):
    def __init__(self, parent):
        super(PlotDialog, self).__init__(parent, QtCore.Qt.WindowMinMaxButtonsHint | QtCore.Qt.WindowCloseButtonHint)

        self.figure = MyFigure(self)
        self.canvas = FigureCanvas(self.figure)
        self.toolbar = NavigationToolbar(self.canvas, self)

        self.layout = QtGui.QGridLayout()
        self.setLayout(self.layout)

        layout = [
            [self.canvas],
            [self.toolbar],
        ]

        for row_index, columns in enumerate(layout):
            if type(columns) is list:
                for column_index, widget in enumerate(columns):
                    if widget is not None:
                        self.layout.addWidget(widget, row_index, column_index)

    def draw_plots(self, plot_data):
        """ Plot Plots """
        self.figure.start_plotting_thread(plot_data, on_finish=self.finish_drawing_plots)

    def finish_drawing_plots(self):
        """ Finish drawing plots """
        self.canvas.draw()
        self.show()