在 PyQt (pyqtgraph) 中嵌入“图形类型”Seaborn 图

Posted

技术标签:

【中文标题】在 PyQt (pyqtgraph) 中嵌入“图形类型”Seaborn 图【英文标题】:Embedding "Figure Type" Seaborn Plot in PyQt (pyqtgraph) 【发布时间】:2017-05-31 00:35:30 【问题描述】:

我正在使用 PyQt (pyqtgraph) 的包装器来构建 GUI 应用程序。 我希望使用MatplotlibWidget 在其中嵌入Seaborn 图。但是,我的问题是 FacetGrid 等 Seaborn 包装方法不接受外部图形句柄。此外,当我尝试使用FacetGrid 生成的图形更新 MatplotlibWidget 对象底层图形(.fig)时,它不起作用(draw 之后没有绘图)。有什么解决方法的建议吗?

【问题讨论】:

这个问题真的与 pyqtgraph 的 MatplotlibWidget 相关,还是会在通常的 matplotlib 图中以相同的方式发生?您能否指定“当我尝试使用 FacetGrid 生成的图形更新 MatplotlibWidget 对象底层图形(.fig)时”的意思?如何用图形更新图形?你能展示一下尝试这个的代码吗? 【参考方案1】:

Seaborn 的 Facetgrid 提供了一个方便的功能,可以快速将 pandas 数据帧连接到 matplotlib pyplot 接口。

但是在 GUI 应用程序中,您很少想要使用 pyplot,而是使用 matplotlib API。

您在这里面临的问题是Facetgrid 已经创建了自己的matplotlib.figure.Figure 对象(Facetgrid.fig)。此外,MatplotlibWidget 创建自己的图形,因此您最终得到两个图形。

现在,让我们退后一步: 原则上,可以在 PyQt 中使用 seaborn Facetgrid 绘图,首先创建绘图,然后将生成的图形提供给图形画布 (matplotlib.backends.backend_qt4agg.FigureCanvasQTAgg)。以下是如何执行此操作的示例。

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_())

虽然这很好用,但它是否有用还是有点问题。在大多数情况下,在 GUI 内创建绘图的目的是根据用户交互进行更新。在上面的示例中,这是非常低效的,因为它需要创建一个新的图形实例,用这个图形创建一个新的画布,并用新的画布实例替换旧的画布实例,将其添加到布局中。

请注意,这个问题特定于 seaborn 中的那些绘图函数,它们在图形级别上工作,例如 lmplotfactorplotjointplotFacetGrid 以及可能的其他函数。regplotboxplotkdeplot 等其他函数在轴级别上工作,并接受 matplotlib axes 对象作为参数 (sns.regplot(x, y, ax=ax1))。


可能的解决方案是首先创建子图轴,然后绘制到这些轴,例如使用pandas plotting functionality。

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

ax 应设置为之前创建的坐标区。 这允许在 GUI 中更新绘图。请参见下面的示例。当然,普通的 matplotlib 绘图 (ax.plot(x,y)) 或使用上面讨论的 seaborn 轴水平函数同样有效。

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_())


关于 pyqtgraph 的最后一句话:我不会将 pyqtgraph 称为 PyQt 的包装器,而是将其称为扩展。尽管 pyqtgraph 附带了自己的 Qt(这使其可移植并且开箱即用),但它也是一个可以在 PyQt 中使用的包。因此,您可以简单地将GraphicsLayoutWidget 添加到 PyQt 布局中
self.pgcanvas = pg.GraphicsLayoutWidget()
self.layout().addWidget(self.pgcanvas) 

MatplotlibWidget (mw = pg.MatplotlibWidget()) 也是如此。虽然您可以使用这种小部件,但它只是一个方便的包装器,因为它所做的只是找到正确的 matplotlib 导入并创建一个 Figure 和一个 FigureCanvas 实例。除非您使用其他 pyqtgraph 功能,否则导入完整的 pyqtgraph 包只是为了节省 5 行代码对我来说似乎有点矫枉过正。

【讨论】:

这是一个彻底而好的答案,但你的第一句话并不准确。 @mwaskom 如果我弄错了,请随时纠正我。如果不是便利功能,您还会将FacetGrid 称为什么? 首先,它是一个类,而不是一个函数。 需要补充的是PyQt5PyQt5.QtWidgets.QSizePolicy.Expanding【参考方案2】:

这是接受的答案的精确副本,但使用 PYQT5

from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import sys
import seaborn as sns

tips = sns.load_dataset("tips")

class MainWindow(QtWidgets.QMainWindow):
    send_fig = QtCore.pyqtSignal(str)

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

        self.main_widget = QtWidgets.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(QtWidgets.QSizePolicy.Expanding, 
                                  QtWidgets.QSizePolicy.Expanding)
        self.canvas.updateGeometry()

        self.dropdown1 = QtWidgets.QComboBox()
        self.dropdown1.addItems(["sex", "time", "smoker"])
        self.dropdown2 = QtWidgets.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 = QtWidgets.QLabel("A plot:")

        self.layout = QtWidgets.QGridLayout(self.main_widget)
        self.layout.addWidget(QtWidgets.QLabel("Select category for subplots"))
        self.layout.addWidget(self.dropdown1)
        self.layout.addWidget(QtWidgets.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__':
    import sys
    app = QtWidgets.QApplication(sys.argv) 
    ex = MainWindow()
    sys.exit(app.exec_())

虽然任何 matplotlib 绘图都可以以相同的方式嵌入到 pyqt5 中,但重要的是要注意 UI 可能会随着数据集大小的增长而变慢。但我发现这些方法通过使用正则表达式功能可以很方便地解析和绘制日志文件。

【讨论】:

以上是关于在 PyQt (pyqtgraph) 中嵌入“图形类型”Seaborn 图的主要内容,如果未能解决你的问题,请参考以下文章

在 PyQt4 #2 中使用 PyQtGraph 进行实时绘图

将 pyqtgraph 嵌入到 enaml 中......如何?

使用 PySide2 在 Qt-Designer 中嵌入 PyQtGraph

PyQt4 中的 QThreading PyQtGraph PlotWidgets

将 Pyqtgraph 嵌入到 PySide2

将 pyqtgraph 图嵌入到 QT .ui 中?