В предыдущем уроке мы рассмотрели построение графиков в PyQt5 с использованием PyQtGraph. PyQtGraph использует QGraphicsScene на основе векторов Qt для рисования графиков и предоставляет отличный интерфейс для интерактивного и высокопроизводительного построения графиков.

Однако есть еще одна библиотека графиков для Python, которая используется гораздо шире и предлагает более богатый ассортимент графиков - Matplotlib. Если вы переносите существующий инструмент анализа данных в графический интерфейс PyQt или просто хотите получить доступ к набору возможностей построения графиков, которые предлагает Matplotlib, вам нужно знать, как включить графики Matplotlib в свое приложение.

В этом руководстве мы расскажем, как встраивать графики Matplotlib в ваши приложения PyQt.

Установка Matplotlib

В следующих примерах предполагается, что у вас установлен Matplotlib. Если нет, вы можете установить его как обычно, используя Pip, со следующим:

pip install matplotlib

Простой пример

В следующем минимальном примере настраивается холст Matplotlib FigureCanvasQTAgg, который создает Figure и добавляет к нему один набор осей. Этот объект холста также является QWidget и поэтому может быть встроен прямо в приложение, как и любой другой виджет Qt.

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt5 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # Create the maptlotlib FigureCanvas object, 
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])
        self.setCentralWidget(sc)

        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

В этом случае мы добавляем наш виджет MplCanvas в качестве центрального виджета в окно с помощью .setCentralWidget(). Это означает, что он займет все окно и изменит размер вместе с ним. Построенные данные [0,1,2,3,4], [10,1,20,3,40] представлены в виде двух списков чисел (x и y соответственно) в соответствии с требованиями метода .plot.

Управление сюжетом

Графики из Matplotlib, отображаемые в PyQt5, фактически визуализируются как простые (растровые) изображения серверной частью Agg. Класс FigureCanvasQTAgg обертывает этот бэкэнд и отображает полученное изображение в виджете Qt. Эффект этой архитектуры заключается в том, что Qt не знает положения линий и других элементов графика - только координаты x, y любых щелчков и перемещений мыши по виджету.

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

Полный код, импортирующий виджет панели инструментов NavigationToolbar2QT и добавляющий его в интерфейс в QVBoxLayout, показан ниже:

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt5 import QtCore, QtGui, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])

        # Create toolbar, passing canvas as first parament, parent (self, the MainWindow) as second.
        toolbar = NavigationToolbar(sc, self)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(sc)

        # Create a placeholder widget to hold our toolbar and canvas.
        widget = QtWidgets.QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

Мы внесем изменения в пошаговые инструкции.

Сначала мы импортируем виджет панели инструментов из matplotlib.backends.backend_qt5agg.NavigationToolbar2QT, переименовывая его в более простое имя NavigationToolbar. Мы создаем экземпляр панели инструментов, вызывая NavigationToolbar с двумя параметрами: сначала объект холста sc, а затем родительский объект для панели инструментов, в данном случае наш MainWindow объект self. Передача холста связывает с ним созданную панель инструментов, позволяя управлять ею. Результирующий объект панели инструментов сохраняется в переменной toolbar.

Нам нужно добавить в окно два виджета, один над другим, поэтому мы используем QVBoxLayout. Сначала мы добавляем наш виджет панели инструментов toolbar, а затем виджет холста sc в этот макет. Наконец, мы устанавливаем этот макет в наш простой контейнер макета widget, который устанавливается как центральный виджет для окна.

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

Кнопки, предоставленные NavigationToolbar2QT, позволяют выполнять следующие действия:

  • Home, Back / Forward, Pan & Zoom, которые используются для навигации по графикам. Кнопки «Назад / Вперед» позволяют перемещаться вперед и назад по шагам навигации, например, увеличение масштаба и последующее нажатие «Назад» вернет к предыдущему масштабированию. Дом возвращается в исходное состояние сюжета.
  • Конфигурация полей / положения графика, позволяющая регулировать график в пределах окна.
  • Редактор стилей осей / кривых, в котором можно изменять названия графиков и масштабы осей, а также настраивать цвета линий графика и стили линий. Для выбора цвета используется палитра цветов по умолчанию, позволяющая выбрать любые доступные цвета.
  • Сохранить, чтобы сохранить получившуюся фигуру как изображение (все форматы, поддерживаемые Matplotlib).

Некоторые из этих параметров конфигурации показаны ниже.

Для получения дополнительной информации о навигации и настройке графиков Matplotlib обратитесь к официальной документации панели инструментов Matplotlib.

Обновление участков

Довольно часто в приложениях вам нужно обновить данные, отображаемые на графиках, будь то в ответ на ввод от пользователя или обновленные данные из API. Есть два способа обновить графики в Matplotlib:

  1. очистка и перерисовка холста (проще, но медленнее) или,
  2. сохраняя ссылку на построенную линию и обновляя данные.

Если для вашего приложения важна производительность, рекомендуется сделать последнее, но первое проще.

Очистить и перерисовать

Сначала мы начнем с простого метода очистки и перерисовки -

import sys
import random
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt5 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QtCore.QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]
        self.canvas.axes.cla()  # Clear the canvas.
        self.canvas.axes.plot(self.xdata, self.ydata, 'r')
        # Trigger the canvas to update and redraw.
        self.canvas.draw()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

В этом примере мы переместили построение графика в update_plot метод, чтобы он оставался автономным. В этом методе мы берем наш массив ydata и отбрасываем первое значение с помощью [1:], затем добавляем новое случайное целое число от 0 до 10. Это дает эффект прокрутки данных влево.

Чтобы перерисовать, мы просто вызываем axes.cla(), чтобы очистить оси (весь холст), и axes.plot(...), чтобы заново построить данные, включая обновленные значения. Полученный холст затем перерисовывается в виджет путем вызова canvas.draw().

Метод update_plot вызывается каждые 100 мсек с использованием QTimer. Метод очистки и обновления достаточно быстр, чтобы график обновлялся с такой скоростью, но, как мы вскоре увидим, дает сбой при увеличении скорости.

Перерисовка на месте

Изменения, необходимые для обновления построенных линий на месте, довольно минимальны, требуя только дополнительной переменной для хранения и извлечения ссылки на построенную линию. Обновленный код MainWindow показан ниже.

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]

        # We need to store a reference to the plotted line 
        # somewhere, so we can apply the new data to it.
        self._plot_ref = None
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QtCore.QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]

        # Note: we no longer need to clear the axis.       
        if self._plot_ref is None:
            # First time we have no plot reference, so do a normal plot.
            # .plot returns a list of line <reference>s, as we're
            # only getting one we can take the first element.
            plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
            self._plot_ref = plot_refs[0]
        else:
            # We have a reference, we can use it to update the data for that line.
            self._plot_ref.set_ydata(self.ydata)

        # Trigger the canvas to update and redraw.
        self.canvas.draw()

Во-первых, нам нужна переменная для хранения ссылки на построенную линию, которую мы хотим обновить, которую мы здесь называем _plot_ref. Мы инициализируем self._plot_ref с помощью None, чтобы мы могли проверить его значение позже, чтобы определить, была ли линия уже нарисована - если значение все еще равно None, мы еще не нарисовали линию.

T ›Если вы рисуете несколько линий, вы, вероятно, захотите использовать структуру данных list или dict для хранения нескольких ссылок и отслеживания того, что есть что.

Наконец, мы обновляем ydata данные, как и раньше, вращая их влево и добавляя новое случайное значение. Тогда мы либо -

  1. если self._plotref равно None (т.е. мы еще не нарисовали линию), нарисуйте линию и сохраните ссылку в self._plot_ref, или
  2. обновите строку на месте, позвонив self._plot_ref.set_ydata(self.ydata)

Ссылку на построенное получаем при звонке .plot. Однако .plot возвращает список (для поддержки случаев, когда один вызов .plot может рисовать более одной линии). В нашем случае мы рисуем только одну линию, поэтому нам просто нужен первый элемент в этом списке - единственный объект Line2D. Чтобы получить это единственное значение в нашей переменной, мы можем присвоить временную переменную plot_refs, а затем присвоить первый элемент нашей переменной self._plot_ref.

plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
self._plot_ref = plot_refs[0]

Вы также можете использовать распаковку кортежей, выбрав первый (и единственный) элемент в списке с помощью -

self._plot_ref, = self.canvas.axes.plot(self.xdata, self.ydata, 'r')

Если вы запустите полученный код, заметной разницы в производительности между этим и предыдущим методом на этой скорости не будет. Однако, если вы попытаетесь обновить график быстрее (например, каждые 10 мс), вы начнете замечать, что очистка графика и повторное рисование занимает больше времени, а обновления не успевают за таймером. Мы можем сравнить две версии ниже -

Оба используют таймер 100 мс, очистка и перерисовка слева, обновление на месте справа.

Оба используют таймер 10 мс, очистка и перерисовка слева, обновление на месте справа.

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

Встраивание сюжетов из панд

Pandas - это пакет Python, ориентированный на работу с таблицами (фреймами данных) и структурами данных рядов, что особенно полезно для рабочих процессов анализа данных. Он поставляется со встроенной поддержкой построения графиков с помощью Matplotlib, и здесь мы кратко рассмотрим, как встроить эти графики в PyQt5. Благодаря этому вы сможете начать создавать приложения для анализа данных PyQt5, построенные на базе Pandas.

Функции построения графиков Pandas доступны напрямую из объектов DataFrame. Сигнатура функции довольно сложна и дает множество опций для управления отрисовкой графиков.

DataFrame.plot(
    x=None, y=None, kind='line', ax=None, subplots=False,
    sharex=None, sharey=False, layout=None, figsize=None,
    use_index=True, title=None, grid=None, legend=True, style=None,
    logx=False, logy=False, loglog=False, xticks=None, yticks=None,
    xlim=None, ylim=None, rot=None, fontsize=None, colormap=None,
    table=False, yerr=None, xerr=None, secondary_y=False, 
    sort_columns=False, **kwargs
)

Параметр, который нас больше всего интересует, - это ax, который позволяет нам передать наш собственный matplotlib.Axes экземпляр, на котором Pandas будет строить DataFrame.

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt5 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure

import pandas as pd


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # Create the maptlotlib FigureCanvas object, 
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)

        # Create our pandas DataFrame with some simple
        # data and headers.
        df = pd.DataFrame([
           [0, 10], [5, 15], [2, 20], [15, 25], [4, 10], 
        ], columns=['A', 'B'])

        # plot the pandas DataFrame, passing in the 
        # matplotlib Canvas axes.
        df.plot(ax=sc.axes)

        self.setCentralWidget(sc)
        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

Ключевым шагом здесь является передача осей холста при вызове метода построения графика в DataFrame в строке df.plot(ax=sc.axes). Вы можете использовать этот же шаблон для обновления графика в любое время, хотя имейте в виду, что Pandas очищает и перерисовывает весь холст, а это означает, что он не идеален для высокопроизводительного построения.

Результирующий график, созданный с помощью Pandas, показан ниже -

Как и раньше, вы можете добавить панель инструментов Matplotlib и поддержку управления к графикам, созданным с помощью Pandas, что позволит вам масштабировать / панорамировать и изменять их в реальном времени. Следующий код объединяет наш предыдущий пример панели инструментов с примером Pandas.

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt5 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure

import pandas as pd


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # Create the maptlotlib FigureCanvas object, 
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)

        # Create our pandas DataFrame with some simple
        # data and headers.
        df = pd.DataFrame([
           [0, 10], [5, 15], [2, 20], [15, 25], [4, 10], 
        ], columns=['A', 'B'])

        # plot the pandas DataFrame, passing in the 
        # matplotlib Canvas axes.
        df.plot(ax=sc.axes)

        # Create toolbar, passing canvas as first parament, parent (self, the MainWindow) as second.
        toolbar = NavigationToolbar(sc, self)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(sc)

        # Create a placeholder widget to hold our toolbar and canvas.
        widget = QtWidgets.QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

Запустив это, вы должны увидеть следующее окно, показывающее график Pandas, встроенный в PyQt5, рядом с панелью инструментов Matplotlib.

Что дальше

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

Matplotlib - это огромная библиотека, которую невозможно здесь подробно описать. Если вы не знакомы с построением графиков в Matplotlib и хотите попробовать, посмотрите документацию и примеры графиков, чтобы увидеть, что это возможно. Если вы знакомы с ним, теперь вы сможете применить эти навыки в своих приложениях PyQt5!

Первоначально опубликовано на https://www.learnpyqt.com 22 января 2020 г.