Встраивание графика Seaborn типа Figure в PyQt (pyqtgraph)

Я использую оболочку PyQt (pyqtgraph) для создания приложения с графическим интерфейсом. Я хочу встроить в него график Seaborn, используя MatplotlibWidget. Однако моя проблема заключается в том, что метод оболочки Seaborn, такой как FacetGrid, не принимает внешний дескриптор фигуры. Более того, когда я пытаюсь обновить объект MatplotlibWidget, лежащий в основе рисунка (.fig), с помощью рисунка, созданного FacetGrid, он не работает (нет графика после draw). Любое предложение для обходного пути?


person Hanan Shteingart    schedule 16.01.2017    source источник
comment
Действительно ли эта проблема связана с MatplotlibWidget pyqtgraph или она будет происходить таким же образом в обычном сюжете matplotlib? Можете ли вы указать, что вы имеете в виду, когда я пытаюсь обновить базовый рисунок объекта MatplotlibWidget (.fig) с помощью рисунка, созданного FacetGrid? Как вы обновляете фигуру с помощью фигуры? Можете ли вы показать код, который пытается это сделать?   -  person ImportanceOfBeingErnest    schedule 16.01.2017


Ответы (3)


Facetgrid от Seaborn предоставляет удобную функцию для быстрого подключения фреймов данных pandas к интерфейсу pyplot matplotlib.

Однако в приложениях с графическим интерфейсом вы редко хотите использовать pyplot, а скорее API matplotlib.

Проблема, с которой вы сталкиваетесь здесь, заключается в том, что Facetgrid уже создает свой собственный объект matplotlib.figure.Figure (Facetgrid.fig). Кроме того, MatplotlibWidget создает свою собственную фигуру, так что в итоге вы получите две фигуры.

Теперь давайте немного вернемся назад: в принципе, можно использовать морской график Facetgrid в PyQt, сначала создав график, а затем предоставив полученную фигуру на холсте фигуры (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_())

Хотя это работает нормально, это немного сомнительно, если это вообще полезно. Создание графика внутри графического интерфейса в большинстве случаев имеет целью обновление в зависимости от взаимодействия с пользователем. В приведенном выше примере это довольно неэффективно, так как потребуется создать новый экземпляр фигуры, создать новый холст с этой фигурой и заменить старый экземпляр холста новым, добавив его в макет.

Обратите внимание, что эта проблематика специфична для тех функций построения графиков в Seaborn, которые работают на уровне фигур, таких как lmplot, factorplot, jointplot, FacetGrid и, возможно, других.
Другие функции, такие как regplot, boxplot, kdeplot, работают на уровне осей и принимают объект matplotlib axes в качестве аргумента (sns.regplot(x, y, ax=ax1)).


Возможное решение состоит в том, чтобы сначала создать оси подграфика, а затем построить график по этим осям, например, используя функции построения графиков pandas.

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

где ax следует установить на ранее созданные оси.
Это позволяет обновлять график в графическом интерфейсе. См. пример ниже. Конечно, обычное построение графиков в matplotlib (ax.plot(x,y)) или использование функции уровня морских осей, описанной выше, работают одинаково хорошо.

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 строк кода кажется мне немного излишним.

person ImportanceOfBeingErnest    schedule 16.01.2017
comment
Это тщательный и хороший ответ, но ваше первое предложение неверно. - person mwaskom; 16.01.2017
comment
@mwaskom Пожалуйста, поправьте меня, если я ошибаюсь. Что бы вы еще назвали FacetGrid, если не функцию удобства? - person ImportanceOfBeingErnest; 16.01.2017
comment
Ну, для начала, это класс, а не функция. - person mwaskom; 16.01.2017
comment
следует добавить, что в PyQt5 это PyQt5.QtWidgets.QSizePolicy.Expanding - person Ilonpilaaja; 18.07.2019

Вот точная копия принятого ответа, но с использованием 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 таким же образом, важно отметить, что пользовательский интерфейс может работать медленнее по мере роста размера набора данных. Но я нашел такие подходы удобными для анализа и построения файлов журнала с использованием функций регулярных выражений.

person Zstack    schedule 21.11.2019

Есть лучший способ сделать это — настроить файл «Настройки макета виджета», чтобы у вас было меньше кода для ввода фактического кода основного приложения.

См. здесь: https://yapayzekalabs.blogspot.com/2018/11/pyqt5-gui-qt-designer-matplotlib.html

Полезно только изображение и процесс, которому вы должны следовать.

person momo668    schedule 08.02.2019
comment
Этот ответ был бы более полезным, если бы в сам пост было включено больше информации. Ссылки могут со временем разорваться, поэтому желательно иметь информацию здесь, на SO. - person jaco0646; 08.02.2019