matplotlibpyqtseabornpyqtgraphmatplotlib-widget

Embedding "Figure Type" Seaborn Plot in PyQt (pyqtgraph)


I am using a wrapper of PyQt (pyqtgraph) to build a GUI application. I wish to embed a Seaborn plot within it using the MatplotlibWidget. However, my problem is that the Seaborn wrapper method such as FacetGrid do not accept an external figure handle. Moreover, when I try to update the MatplotlibWidget object underlying figure (.fig) with the figure produced by the FacetGrid it doesn't work (no plot after draw). Any suggestion for a workaround?


Solution

  • Seaborn's Facetgrid provides a convenience function to quickly connect pandas dataframes to the matplotlib pyplot interface.

    However in GUI applications you rarely want to use pyplot, but rather the matplotlib API.

    The problem you are facing here is that Facetgrid already creates its own matplotlib.figure.Figure object (Facetgrid.fig). Also, the MatplotlibWidget creates its own figure, so you end up with two figures.

    Now, let's step back a bit: In principle it is possible to use a seaborn Facetgrid plot in PyQt, by first creating the plot and then providing the resulting figure to the figure canvas (matplotlib.backends.backend_qt4agg.FigureCanvasQTAgg). The following is an example of how to do that.

    from PyQt4 import QtGui, QtCore
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    import sys
    import seaborn as sns
    import matplotlib.pyplot as plt
    
    tips = sns.load_dataset("tips")
    
    
    def seabornplot():
        g = sns.FacetGrid(tips, col="sex", hue="time", palette="Set1",
                                    hue_order=["Dinner", "Lunch"])
        g.map(plt.scatter, "total_bill", "tip", edgecolor="w")
        return g.fig
    
    
    class MainWindow(QtGui.QMainWindow):
        send_fig = QtCore.pyqtSignal(str)
    
        def __init__(self):
            super(MainWindow, self).__init__()
    
            self.main_widget = QtGui.QWidget(self)
    
            self.fig = seabornplot()
            self.canvas = FigureCanvas(self.fig)
    
            self.canvas.setSizePolicy(QtGui.QSizePolicy.Expanding,
                          QtGui.QSizePolicy.Expanding)
            self.canvas.updateGeometry()
            self.button = QtGui.QPushButton("Button")
            self.label = QtGui.QLabel("A plot:")
    
            self.layout = QtGui.QGridLayout(self.main_widget)
            self.layout.addWidget(self.button)
            self.layout.addWidget(self.label)
            self.layout.addWidget(self.canvas)
    
            self.setCentralWidget(self.main_widget)
            self.show()
    
    
    if __name__ == '__main__':
        app = QtGui.QApplication(sys.argv)
        win = MainWindow()
        sys.exit(app.exec_())
    

    While this works fine, it is a bit questionable, if it's useful at all. Creating a plot inside a GUI in most cases has the purpose of beeing updated depending on user interactions. In the example case from above, this is pretty inefficient, as it would require to create a new figure instance, create a new canvas with this figure and replace the old canvas instance with the new one, adding it to the layout.

    Note that this problematics is specific to those plotting functions in seaborn, which work on a figure level, like lmplot, factorplot, jointplot, FacetGrid and possibly others.
    Other functions like regplot, boxplot, kdeplot work on an axes level and accept a matplotlib axes object as argument (sns.regplot(x, y, ax=ax1)).


    A possibile solution is to first create the subplot axes and later plot to those axes, for example using the pandas plotting functionality.

    df.plot(kind="scatter", x=..., y=..., ax=...)
    

    where ax should be set to the previously created axes.
    This allows to update the plot within the GUI. See the example below. Of course normal matplotlib plotting (ax.plot(x,y)) or the use of the seaborn axes level function discussed above work equally well.

    from PyQt4 import QtGui, QtCore
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.figure import Figure
    import sys
    import seaborn as sns
    
    tips = sns.load_dataset("tips")
    
    class MainWindow(QtGui.QMainWindow):
        send_fig = QtCore.pyqtSignal(str)
    
        def __init__(self):
            super(MainWindow, self).__init__()
    
            self.main_widget = QtGui.QWidget(self)
    
            self.fig = Figure()
            self.ax1 = self.fig.add_subplot(121)
            self.ax2 = self.fig.add_subplot(122, sharex=self.ax1, sharey=self.ax1)
            self.axes=[self.ax1, self.ax2]
            self.canvas = FigureCanvas(self.fig)
    
            self.canvas.setSizePolicy(QtGui.QSizePolicy.Expanding, 
                                      QtGui.QSizePolicy.Expanding)
            self.canvas.updateGeometry()
    
            self.dropdown1 = QtGui.QComboBox()
            self.dropdown1.addItems(["sex", "time", "smoker"])
            self.dropdown2 = QtGui.QComboBox()
            self.dropdown2.addItems(["sex", "time", "smoker", "day"])
            self.dropdown2.setCurrentIndex(2)
    
            self.dropdown1.currentIndexChanged.connect(self.update)
            self.dropdown2.currentIndexChanged.connect(self.update)
            self.label = QtGui.QLabel("A plot:")
    
            self.layout = QtGui.QGridLayout(self.main_widget)
            self.layout.addWidget(QtGui.QLabel("Select category for subplots"))
            self.layout.addWidget(self.dropdown1)
            self.layout.addWidget(QtGui.QLabel("Select category for markers"))
            self.layout.addWidget(self.dropdown2)
    
            self.layout.addWidget(self.canvas)
    
            self.setCentralWidget(self.main_widget)
            self.show()
            self.update()
    
        def update(self):
    
            colors=["b", "r", "g", "y", "k", "c"]
            self.ax1.clear()
            self.ax2.clear()
            cat1 = self.dropdown1.currentText()
            cat2 = self.dropdown2.currentText()
            print cat1, cat2
    
            for i, value in enumerate(tips[cat1].unique().get_values()):
                print "value ", value
                df = tips.loc[tips[cat1] == value]
                self.axes[i].set_title(cat1 + ": " + value)
                for j, value2 in enumerate(df[cat2].unique().get_values()):
                    print "value2 ", value2
                    df.loc[ tips[cat2] == value2 ].plot(kind="scatter", x="total_bill", y="tip", 
                                                    ax=self.axes[i], c=colors[j], label=value2)
            self.axes[i].legend()   
            self.fig.canvas.draw_idle()
    
    
    if __name__ == '__main__':
        app = QtGui.QApplication(sys.argv)
        win = MainWindow()
        sys.exit(app.exec_())
    

    enter image description here


    A final word about pyqtgraph: I wouldn't call pyqtgraph a wrapper for PyQt but more an extention. Although pyqtgraph ships with its own Qt (which makes it portable and work out of the box), it is also a package one can use from within PyQt. You can therefore add a GraphicsLayoutWidget to a PyQt layout simply by

    self.pgcanvas = pg.GraphicsLayoutWidget()
    self.layout().addWidget(self.pgcanvas) 
    

    The same holds for a MatplotlibWidget (mw = pg.MatplotlibWidget()). While you can use this kind of widget, it's merely a convenience wrapper, since all it's doing is finding the correct matplotlib imports and creating a Figure and a FigureCanvas instance. Unless you are using other pyqtgraph functionality, importing the complete pyqtgraph package just to save 5 lines of code seems a bit overkill to me.