1

Background

I'm currently working on a project where I want to embed a matplotlib plot into a pyqt5 GUI. The plot is interactive and allows for the drawing of shaded rectangles.

Problem

The problem is that the plot is not interactive when it is embedded in the pyqt window. When I run the program, line 147 in the below code (plt.show() in the mplWidget class) displays the matplotlib plot, and I can then draw a rectangle as you can see here:

matplotlib plot

However, when this window closes and the plot is embedded in the pyqt window, it becomes uneditable

pyqt uneditable plot

I want the GUI plot to function as the matplotlib figure does.

Proposed solution / Question

I know that this has to do with the fact that I have to provide the pyqt functionality with connect() statements, but I don't know where those go / how they would fit in to this program.
I don't know how to connect to a matplotlib. Do I just use connect() statements to the mplWidget class functions?
Any help is appreciated!
(I realize that I will need to take line 147 out (plt.show()) so that the figure frame does not pop up before the gui, but I just had it temporarily to show that the mpl class still functions as intended, and the problem is that it becomes "static" upon embedding)

Code

import numpy as np
import matplotlib.pyplot as plt
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar)

class topLevelWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        # Add a central widget to the main window and lay it out in a grid
        self.centralwidget = QtWidgets.QWidget(self)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout_5 = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout_5.setObjectName("gridLayout_5")

        # Create mainTabs object as well as the displayFrame and displaySettingsFrame that go in the central widget
        # Display Frame and Display settings frames
        self.displayFrame = QtWidgets.QFrame(self.centralwidget)
        self.verticalLayout_22 = QtWidgets.QVBoxLayout(self.displayFrame)
        self.verticalLayout_22.setObjectName("verticalLayout_22")

        self.gridLayout_5.addWidget(self.displayFrame, 1, 0, 1, 1)

        self.devConstructor = mplWidget()
        self.dynamic_canvas = FigureCanvasQTAgg(self.devConstructor.fig)
        self.verticalLayout_22.addWidget(self.dynamic_canvas)

        self._dynamic_ax = self.devConstructor.ax
        self.setCentralWidget(self.centralwidget)


        # Perform final windows setup (set buddies, translate, tab order, initial tabs, etc)
        _translate = QtCore.QCoreApplication.translate
        self.setWindowTitle(_translate("MainWindow", "MainWindow"))  # Was self.setWindowTitle

        QtCore.QMetaObject.connectSlotsByName(self)

        self.show()


class mplWidget(QtWidgets.QWidget): # n.b. changed this from Object to QWidget and added a super()

    def setSnapBase(self, base):
        return lambda value: int(base*round(float(value)/base))

    def onclick(self, event):
        if self.plotSnap is False:
            self.bottomLeftX = event.xdata
            self.bottomLeftY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.bottomLeftX = self.calculateSnapCoordinates(event.xdata)
            self.bottomLeftY = self.calculateSnapCoordinates(event.ydata)

        try:
            self.aspan.remove()
        except:
            pass

        self.moving = True

    def onrelease(self, event):
        if self.plotSnap is False:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            try:
                calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
                self.topRightX = calculateSnapCoordinates(event.xdata)
                self.topRightY = calculateSnapCoordinates(event.ydata)
            except:
                pass

        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)
        # ax.fill_between(x, y, color=defaultColors[0, :], alpha=.25)
        ylimDiff = self.ax.get_ylim()[1] - self.ax.get_ylim()[0]
        self.aspan = self.ax.axvspan(self.bottomLeftX, self.topRightX,
                                     (self.bottomLeftY-self.ax.get_ylim()[0])/ylimDiff,
                                    (self.topRightY-self.ax.get_ylim()[0])/ylimDiff,
                                     color=self.defaultColors[0, :], alpha=.25)

        self.moving = False

        self.fig.canvas.draw()

    def onmotion(self, event):
        if self.moving is False:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return

        if self.plotSnap is False:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.topRightX = self.calculateSnapCoordinates(event.xdata)
            self.topRightY = self.calculateSnapCoordinates(event.ydata)

        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)

        self.fig.canvas.draw()

    def __init__(self):
        super(mplWidget, self).__init__()
        # Set default colors array
        self.defaultColors = np.array([[0, 0.4470, 0.7410], [0.8500, 0.3250, 0.0980], [0.9290, 0.6940, 0.1250],
                                  [0.4660, 0.6740, 0.1880], [0.6350, 0.0780, 0.1840], [0.4940, 0.1840, 0.5560],
                                  [0.3010, 0.7450, 0.9330]])

        # Create a figure with axes
        self.fig = plt.figure()
        self.ax = self.fig.gca()

        # Form the plot and shading
        self.bottomLeftX = 0; self.bottomLeftY = 0; self.topRightX = 0; self.topRightY = 0
        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot, = self.ax.plot(self.x, self.y, color=self.defaultColors[0, :])
        self.aspan = self.ax.axvspan(self.bottomLeftX, self.topRightX, color= self.defaultColors[0, :], alpha=0)

        # Set moving flag false (determines if mouse is being clicked and dragged inside plot). Set graph snap
        self.moving = False
        self.plotSnap = 5

        # Set up connectivity
        self.cid = self.fig.canvas.mpl_connect('button_press_event', self.onclick)
        self.cid = self.fig.canvas.mpl_connect('button_release_event', self.onrelease)
        self.cid = self.fig.canvas.mpl_connect('motion_notify_event', self.onmotion)


        # Set plot limits and show it
        plt.ylim((-100, 100))
        plt.xlim((-100, 100))
        plt.show()

if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    MainWindow = topLevelWindow()

    sys.exit(app.exec_())

1 Answer 1

2

In your code there are many so I will only list them:

  • If you are going to use FigureCanvasQTAgg then you should not use pyplot anymore.
  • "mplWidget" is a class whose only task is to redraw the canvas, so does it have to be a QWidget ?.
  • If you are going to compare booleans do not use "is", for example if self.plotSnap is False:, just if not self.plotSnap: also I see it illogical to think that "plotSnap" is False, if you want to disable then set an impossible value, such as 0 or negative.

Considering the above I have made MplWidget inherit from FigureCanvasQTAgg, I have eliminated the use of pyplot:

import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg,
    NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.figure import Figure


class MplWidget(FigureCanvasQTAgg):
    def __init__(self, parent=None):
        fig = Figure()
        super(MplWidget, self).__init__(fig)
        self.setParent(parent)
        # Set default colors array
        self.defaultColors = np.array(
            [
                [0, 0.4470, 0.7410],
                [0.8500, 0.3250, 0.0980],
                [0.9290, 0.6940, 0.1250],
                [0.4660, 0.6740, 0.1880],
                [0.6350, 0.0780, 0.1840],
                [0.4940, 0.1840, 0.5560],
                [0.3010, 0.7450, 0.9330],
            ]
        )

        # Create a figure with axes

        self.ax = self.figure.add_subplot(111)

        # Form the plot and shading
        self.bottomLeftX = 0
        self.bottomLeftY = 0
        self.topRightX = 0
        self.topRightY = 0
        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )

        (self.myPlot,) = self.ax.plot(self.x, self.y, color=self.defaultColors[0, :])
        self.aspan = self.ax.axvspan(
            self.bottomLeftX, self.topRightX, color=self.defaultColors[0, :], alpha=0
        )
        self.ax.set_xlim((-100, 100))
        self.ax.set_ylim((-100, 100))

        # Set moving flag false (determines if mouse is being clicked and dragged inside plot). Set graph snap
        self.moving = False
        self.plotSnap = 5

        # Set up connectivity
        self.cid1 = self.mpl_connect("button_press_event", self.onclick)
        self.cid2 = self.mpl_connect("button_release_event", self.onrelease)
        self.cid3 = self.mpl_connect("motion_notify_event", self.onmotion)

    def setSnapBase(self, base):
        return lambda value: int(base * round(float(value) / base))

    def onclick(self, event):
        if self.plotSnap <= 0:
            self.bottomLeftX = event.xdata
            self.bottomLeftY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.bottomLeftX = self.calculateSnapCoordinates(event.xdata)
            self.bottomLeftY = self.calculateSnapCoordinates(event.ydata)

        try:
            self.aspan.remove()
        except:
            pass

        self.moving = True

    def onrelease(self, event):
        if self.plotSnap <= 0:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            try:
                calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
                self.topRightX = calculateSnapCoordinates(event.xdata)
                self.topRightY = calculateSnapCoordinates(event.ydata)
            except:
                pass

        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)
        # ax.fill_between(x, y, color=defaultColors[0, :], alpha=.25)
        ylimDiff = self.ax.get_ylim()[1] - self.ax.get_ylim()[0]
        self.aspan = self.ax.axvspan(
            self.bottomLeftX,
            self.topRightX,
            (self.bottomLeftY - self.ax.get_ylim()[0]) / ylimDiff,
            (self.topRightY - self.ax.get_ylim()[0]) / ylimDiff,
            color=self.defaultColors[0, :],
            alpha=0.25,
        )

        self.moving = False
        self.draw()

    def onmotion(self, event):
        if not self.moving:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return

        if self.plotSnap <= 0:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.topRightX = self.calculateSnapCoordinates(event.xdata)
            self.topRightY = self.calculateSnapCoordinates(event.ydata)

        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )
        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)

        self.draw()


class TopLevelWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.canvas = MplWidget()
        self.setCentralWidget(self.canvas)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = TopLevelWindow()
    w.show()

    sys.exit(app.exec_())
Sign up to request clarification or add additional context in comments.

3 Comments

What would be the alternative to using FigureCanvasQTAgg or why is it incompatible with pyplot? I made it a widget because this is going to be in one frame of a larger GUI and I thought that making it a QWidget would be the best way to insert it into a frame. Okay, I will change the plotSnap part, that was done quickly and you make a good point in saying I shouldn't mix booleans and floats
@WhoDatBoy pyplot is an easy way to create plots but for this reason it already creates a figure associated with a canvas with its own eventloop that may have conflicts with the Qt eventloop. If FigureCanvasQTAgg is going to be used then it is better for that canvas to handle its own logic. On the other hand, each class should have its own responsibility, so it is more natural for MplWidget to inherit from FigureCanvasQTAgg. I recommend you review the official examples: matplotlib.org/3.2.1/gallery/user_interfaces/…
Oh I see. I took a look at the documentation but I didn't realize pyplot was had its own eventloop, thanks for the advice!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.