5

What is the best way to embed an interactive 3D plot in a PySide GUI? I have looked at some examples on here of 2D plots embedded in a PySide GUI:

Getting PySide to Work With Matplotlib
Matplotlib Interactive Graph Embedded In PyQt
Python/Matplotlib/Pyside Fast Timetrace Scrolling

However, the functionality that I'm looking for is not quite the same. The figure needs to rotate and zoom based on mouse input from the user in the same way as if it were drawn in a separate window.

I'm trying to avoid having to go in manually and write functions for transforming mouse click + move into a figure rotate and canvas repaint--even if that's the only way, I'm not even sure how to do that. But I figure (no pun intended) that there should be a way to reuse the functionality already present for creating 3D plots in their own windows.

Here's my code. It works as intended, but the plot is not interactive. Any advice is appreciated!

EDIT: I fixed the use of FigureCanvas according to tcaswell's corrections. I also added a bit from the matplotlib Event Handling and Picking documentation to show that the figure seems to be getting the events upon mouseclick.

Final Edit: The following code now produces the plot as desired.

# -*- coding: utf-8 -*-

from PySide import QtCore, QtGui
import numpy as np

import matplotlib
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

# import 3D plotting
from mpl_toolkits.mplot3d import Axes3D    # @UnusedImport
from matplotlib.figure import Figure


# Auto-generated code from QT Designer ----------------------------------------

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(750, 497)
        self.centralwidget = QtGui.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout_2 = QtGui.QHBoxLayout(self.centralwidget)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.frame_2 = QtGui.QFrame(self.centralwidget)
        self.frame_2.setFrameShape(QtGui.QFrame.StyledPanel)
        self.frame_2.setFrameShadow(QtGui.QFrame.Raised)
        self.frame_2.setObjectName("frame_2")
        self.verticalLayout = QtGui.QVBoxLayout(self.frame_2)
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = QtGui.QLabel(self.frame_2)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.label_2 = QtGui.QLabel(self.frame_2)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.lineEdit = QtGui.QLineEdit(self.frame_2)
        sizePolicy = QtGui.QSizePolicy(
                            QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
                                self.lineEdit.sizePolicy().hasHeightForWidth())
        self.lineEdit.setSizePolicy(sizePolicy)
        self.lineEdit.setObjectName("lineEdit")
        self.verticalLayout.addWidget(self.lineEdit)
        spacerItem = QtGui.QSpacerItem(
                20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.horizontalLayout_2.addWidget(self.frame_2)
        self.frame_plot = QtGui.QFrame(self.centralwidget)
        self.frame_plot.setMinimumSize(QtCore.QSize(500, 0))
        self.frame_plot.setFrameShape(QtGui.QFrame.StyledPanel)
        self.frame_plot.setFrameShadow(QtGui.QFrame.Raised)
        self.frame_plot.setObjectName("frame_plot")
        self.horizontalLayout_2.addWidget(self.frame_plot)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QtGui.QApplication.translate(
            "MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8))
        self.label.setText(QtGui.QApplication.translate("MainWindow",
                    "This is a qlabel.", None, QtGui.QApplication.UnicodeUTF8))
        self.label_2.setText(QtGui.QApplication.translate("MainWindow",
            "And this is another one.", None, QtGui.QApplication.UnicodeUTF8))
        self.lineEdit.setText(QtGui.QApplication.translate("MainWindow",
                    "Text goes here.", None, QtGui.QApplication.UnicodeUTF8))

# Auto-generated code from QT Designer ----------------------------------------

class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        # intialize the window
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # create the matplotlib widget and put it in the frame on the right
        self.ui.plotWidget = Mpwidget(parent=self.ui.frame_plot)

class Mpwidget(FigureCanvas):
    def __init__(self, parent=None):

        self.figure = Figure(facecolor=(0, 0, 0))
        super(Mpwidget, self).__init__(self.figure)
        self.setParent(parent)

        # plot random 3D data
        self.axes = self.figure.add_subplot(111, projection='3d')
        self.data = np.random.random((3, 100))
        self.axes.plot(self.data[0, :], self.data[1, :], self.data[2, :])

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()

    # adjust the frame size so that it fits right after the window is shown
    s = mw.ui.frame_plot.size()
    mw.ui.plotWidget.setGeometry(1, 1, s.width() - 2, s.height() - 2)

    sys.exit(app.exec_())
Community
  • 1
  • 1
Bryant
  • 664
  • 2
  • 9
  • 23
  • Have you looked at VPython? I know it does what you're looking for, but I have not tried embedding it in a PySide GUI before. – Chris Barker Aug 15 '13 at 19:07
  • Yes I have. I was very impressed at its power and simplicity--five minutes, and I had an animation of a ball bouncing up and down with gravity. However, after researching it a bit more, I couldn't find any cases where someone had tried embedding it in PySide. – Bryant Aug 15 '13 at 19:18
  • checkout how the `qt4agg` backend code does this – tacaswell Aug 15 '13 at 19:36
  • So, basically what I'm getting is that the backends are designed for rendering purposes, not for input. So the only way to get the functionality I'm looking for is to tap into the parent widget of the plot, define functions for handling the mouse click + move events, and repaint the canvas according to those events. Does that sound right? Or am I still missing it? – Bryant Aug 16 '13 at 13:33
  • 1
    The backend `qt4agg` is just `matplotlib` embedded in `Qt`. It can do the interactive stuff you want -> it can be done in `Qt` more-or-less out of the box. The rotations are done via the `mpl` call back system, you should be able to do it with out touching the `Qt` layer. – tacaswell Aug 16 '13 at 16:43

2 Answers2

6

You are not using FigureCanvas right:

class Mpwidget(FigureCanvas):
    def __init__(self, parent=None):
        self.figure = Figure(facecolor=(0, 0, 0))
        super(Mpwidget, self).__init__(self.figure) # this object _is_ your canvas
        self.setParent(parent)

        # plot random 3D data
        self.axes = self.figure.add_subplot(111, projection='3d')
        self.data = np.random.random((3, 100))
        self.axes.plot(self.data[0, :], self.data[1, :], self.data[2, :])

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()

    # adjust the frame size so that it fits right after the window is shown
    s = mw.ui.frame_plot.size()
    mw.ui.plotWidget.setGeometry(1, 1, s.width() - 2, s.height() - 2)

    sys.exit(app.exec_())

Every time you called FigureCanvas(..) you were attaching the figure to a new canvas (which were not the FigureCanvas you were seeing) hence the call-backs were never firing (because they were listening on a FigureCanvas that you couldn't see).

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • Thanks mate. I appreciate the help. Edited the original post to include your changes; it is now printing out info from the mouse click event to show that the events do indeed register. However, it doesn't appear to support rotating and zooming out of the box. Is this what you expected? Or did I mess something up again? haha – Bryant Aug 19 '13 at 14:40
  • If this solved the problem you asked in the OP, you should accept the answer (big gray check box on the left). If you have discover new issues you should ask a new question. – tacaswell Aug 19 '13 at 15:20
  • and the printing is because of your `on_click` function the you register. Remove that and the printing will go away – tacaswell Aug 19 '13 at 15:21
  • @Bryant and I suspect that your `on_click` is snarfing the events so the rotation code doesn't get it – tacaswell Aug 19 '13 at 15:22
  • Sorry for the miscommunication--I put the snippet to print out the `onclick` event to show that there was indeed something happening because it still wasn't rotating. I don't think it's stealing the events, because it wouldn't rotate before I added the connection. And it certainly fixed one issue, but it still won't rotate, so it doesn't quite answer the question :/ I'll post if I figure anything out though! Thanks again for the help. – Bryant Aug 19 '13 at 15:28
  • @Bryant you still have a rouge assignment to `mw.ui.plotWidget.canvas = FigureCanvas(mw.ui.plotWidget.figure)` in your main – tacaswell Aug 19 '13 at 15:36
  • Found it! Can't believe I missed that. Works like a charm now. Answer accepted. – Bryant Aug 19 '13 at 15:50
1

1) Create the FigureCanvas before adding the axes. See https://stackoverflow.com/a/9007892/3962328

canvas = FigureCanvas(fig)
ax = figure.add_subplot(111, projection='3d')

or

class MyFigureCanvas(FigureCanvas):
    def __init__(self):
        self.figure = Figure()
        super(FigureCanvas, self).__init__(self.figure)
        self.axes = self.figure.add_subplot(111, projection='3d')

2) Try ax.mouse_init() to restore the connection:

...
ax = fig.gca(projection="3d")
...
canvas = FigureCanvas(fig)
ax.mouse_init()
Community
  • 1
  • 1
lrb
  • 21
  • 3