Создание интерфейса средствами библиотеки Matplotlib | jenyay.net

Создание интерфейса средствами библиотеки Matplotlib

Дата публикации: 02.03.2017
Дата последней правки: 18.04.2023

Содержание

Введение

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

Matplotlib включает в себя очень небольшое количество элементов управления, которые в терминах Matplotlib называются виджетами. Они располагаются пакете matplotlib.widgets. В этом пакете содержатся также виджеты для взаимодействия с графиками (виджеты для выделения областей), но в данной статье мы сосредоточимся только на виджетах, добавляющих элементы управления. Таких элементов всего шесть (в Matplotlib версии 3.7):

  • Button - простая кнопка.
  • Slider - ползунок.
  • RangeSlider - ползунок с выбором минимального и максимального значения.
  • RadioButtons - переключатель из нескольких вариантов.
  • CheckButtons - флажок.
  • TextBox - поле ввода текста.

Все эти классы являются производными от класса matplotlib.widgets.AxesWidget, который в свою очередь является производным от matplotlib.widgets.Widget. Диаграмма классов показана на следующем рисунке:

Здесь показаны только классы, относящиеся к теме данной статьи. Более полную картину об архитектуре классов можно увидеть в документации.

Напишем небольшое приложение, которое мы будем развивать на протяжении всей статьи. Для начала наше приложение будет отображать функцию Гаусса:

В этой формуле σ отвечает за ширину графика, а μ - за смещение относительно нуля по оси X.

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

import numpy as np
import matplotlib.pyplot as plt


def gaussian(sigma, mu, x):
    """Отображаемая фукнция"""
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi))
            * np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def addPlot(graph_axes, sigma, mu):
    """Добавить график к осям"""
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)
    graph_axes.plot(x, y)


if __name__ == "__main__":
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.2)

    # Добавить график
    addPlot(graph_axes, current_sigma, current_mu)

    plt.show()

В этом примере с помощью метода subplots_adjust() мы указали, какую часть окна должен занимать график. Внизу мы оставили побольше места для будущих виджетов, задав параметр bottom=0.2. В методе subplots_adjust() координаты отсчитываются от левого нижнего угла окна.

На данный момент результат работы выглядит следующим образом:

Здесь мы используем объектно-ориентированный стиль создания графиков, чтобы явно указывать, в какие оси нужно добавлять график. Далее станет ясно, почему это важно (и намного удобнее).

Создание кнопок (класс Button)

Обзор виджетов начнем с кнопки. Создадим кнопку, которая будет добавлять новую кривую на график с удвоенной σ. Для этого нам надо создать экземпляр класса matplotlib.widgets.Button, конструктор которого (как и все классы, производные от matplotlib.widgets.AxesWidget) в качестве первого параметра принимает экземпляр класса matplotlib.axes.Axes. То есть для кнопки нужно сначала создать оси, а потом их передать конструктору класса Button. Сделаем это (восклицательными знаками в комментариях в этом и последующих примерах помечены новые строки, на которые стоит обратить внимание):

Для создания осей будем использовать функцию matplotlib.pyplot.axes(), в которую в качестве параметра будет передаваться список из четырех чисел:

  1. координата X левой границы;
  2. координата Y нижней границы;
  3. ширина;
  4. высота.

Точка (0; 0) располагается в левом нижнем углу окна, а правый верхний угол окна имеет координату (1.0; 1.0).

Конструктор класса Button принимает два обязательных параметра:

  1. ax - Экземпляр класса Axes.
  2. label - надпись на кнопке.
import numpy as np
import matplotlib.pyplot as plt

# !!! Импортируем класс кнопки
from matplotlib.widgets import Button


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def addPlot(graph_axes, sigma, mu):
    '''Добавить график к осям'''
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)
    graph_axes.plot(x, y)


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.2)

    # !!! Создадим ось для кнопки
    axes_button_add = plt.axes([0.7, 0.05, 0.25, 0.075])

    # !!! Создадим кнопку
    button_add = Button(axes_button_add, 'Добавить')

    # Добавить график
    addPlot(graph_axes, current_sigma, current_mu)

    plt.show()

Результат работы программы выглядит следующим образом:

Пока созданная кнопка ничего не делает. Добавим ей функциональность. Для того, чтобы подписаться на событие, возникающее при нажатии кнопки, нужно вызвать метод on_clicked(), передав ему ссылку на функцию, которая будет принимать один параметр типа matplotlib.backend_bases.MouseEvent о котором говорилось в статье Обработка событий мыши и клавиатуры на графиках Matplotlib.

Важно сохранить в скрипте ссылку на экземпляр виджета (в нашем случае в переменной axes_button_add), т.к. функции обратного вызова сохраняются с помощью слабых ссылок (weak refs), и если объект виджета будет уничтожен сборщиком мусора, то функция обратного вызова не будет вызвана.

Приведенный ниже пример не очень хорош с точки зрения архитектуры приложения из-за того, что используются глобальные переменные (без необходимости не делайте так), в реальности лучше использовать более объектно-ориентированный подход и инкапсулировать требуемые данные в какой-нибудь класс, но примеры в этой статье написаны таким образом, чтобы быть как можно более компактным в ущерб архитектурной красоте.

import numpy as np
import matplotlib.pyplot as plt

# Импортируем класс кнопки
from matplotlib.widgets import Button


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def addPlot(graph_axes, sigma, mu):
    '''Добавить график к осям'''
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)
    graph_axes.plot(x, y)

    # !!! Нужно для обновления графика
    plt.draw()


def onButtonAddClicked(event):
    ''' !!! Обработчик события для кнопки "Добавить"'''
    global current_sigma
    global current_mu
    global graph_axes

    current_sigma *= 2
    addPlot(graph_axes, current_sigma, current_mu)


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.2)

    # Создадим оси для кнопки
    axes_button_add = plt.axes([0.7, 0.05, 0.25, 0.075])

    # Создадим кнопку
    button_add = Button(axes_button_add, 'Добавить')

    # !!! Подпишемся на событие обработки нажатия кнопки
    button_add.on_clicked(onButtonAddClicked)

    # Добавить график
    addPlot(graph_axes, current_sigma, current_mu)

    plt.show()

Обратите внимание, что мы добавили вызов функции plt.draw() в функцию addPlot(), чтобы перерисовывать график после добавления очередной кривой. Обработчик нажатия кнопки onButtonAddClicked удваивает значение глобальной переменной current_sigma, а затем добавляет новую кривую на график.

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

Метод on_clicked() возвращает целочисленный идентификатор, который затем можно использовать, чтобы отписаться от события. Для этого предназначен метод disconnect(). В частности, если бы мы хотели после подписки на событие сразу же от него отписаться, то могли бы использовать примерно следующий код:

    # Создание кнопки
    button_add = Button(axes_button_add, 'Добавить')

    # !!! Подпишемся на событие обработки нажатия кнопки
    on_clicked_id = button_add.on_clicked(onButtonAddClicked)

    # !!! Отпишемся от события on_clicked
    button_add.disconnect(on_clicked_id)

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

Создание слайдеров (классы Slider и RangeSlider)

Давайте теперь добавим два слайдера (ползунка), с помощью которых пользователь сможет задавать значения σ и μ для добавляемой кривой. Для каждого виджета нужно создать свои оси, а затем создать два экземпляра класса matplotlib.widgets.Slider.

Кроме того, мы изменим поведение обработчика события нажатия кнопки таким образом, чтобы значения σ и μ брались не из глобальных переменных, а в виде значений, установленных с помощью слайдеров. Таким образом, переменные current_sigma и current_mu нам больше не понадобятся. Оси для отображения графика мы немного уменьшим по вертикали, чтобы уместились слайдеры.

Конструктор класса matplotlib.widgets.Slider принимает несколько обязательных и необязательных параметров. Обязательные параметры это:

  1. ax - Экземпляр класса Axes.
  2. label - текстовая метка около слайдера.
  3. valmin - минимальное значение, которое можно установить с помощью слайдера.
  4. valmax - максимальное значение, которое можно установить с помощью слайдера.

Необязательных параметров существует больше, но в данном примере мы будем использовать два из них:

  1. valinit - начальное значение для слайдера (по умолчанию устанавливается на значение 0.5). На следующей картинке обратите внимание на красные линии на слайдерах - это и есть отметки для начального значения. Класс Slider имеет метод reset() для установки значения к начальному значению.
  2. valfmt - строка форматирования текущего значения, установленного с помощью слайдера (значение по умолчанию - '%1.2f'). С помощью этого параметра можно имитировать случай, когда требуется устанавливать целочисленные значения с помощью слайдера, поскольку у этого виджета нет такого понятия как дискрет перемещения, и он всегда хранит число с плавающей точкой.
import numpy as np
import matplotlib.pyplot as plt

# !!! Импортируем классы кнопки и слайдера
from matplotlib.widgets import Button, Slider


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def addPlot(graph_axes, sigma, mu):
    '''Добавить график к осям'''
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)
    graph_axes.plot(x, y)

    # !!! Нужно для обновления графика
    plt.draw()


def onButtonAddClicked(event):
    '''Обработчик события для кнопки "Добавить"'''
    # !!! Будем использовать sigma и mu, установленные с помощью слайдеров
    global slider_sigma
    global slider_mu
    global graph_axes

    # !!! Используем атрибут val, чтобы получить значение слайдеров
    addPlot(graph_axes, slider_sigma.val, slider_mu.val)


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим оси для кнопки
    axes_button_add = plt.axes([0.7, 0.05, 0.25, 0.075])

    # Создадим кнопку
    button_add = Button(axes_button_add, 'Добавить')
    button_add.on_clicked(onButtonAddClicked)

    # !!! Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.05, 0.25, 0.85, 0.04])
    slider_sigma = Slider(axes_slider_sigma,
                          label='σ',
                          valmin=0.1,
                          valmax=1.0,
                          valinit=0.5,
                          valfmt='%1.2f')

    # !!! Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.05, 0.17, 0.85, 0.04])
    slider_mu = Slider(axes_slider_mu,
                       label='μ',
                       valmin=-4.0,
                       valmax=4.0,
                       valinit=0.0,
                       valfmt='%1.2f')
    plt.show()

Теперь мы можем произвольно менять значения σ и μ с помощью слайдеров и добавлять соответствующие графики на оси. Вот как теперь может выглядеть окно после добавления нескольких кривых:

Далее в этом примере просто напрашивается добавить кнопку "Очистить", предназначенную для удаления всех кривых. Мы теперь знаем, как это сделать, и добавить ее не составит труда:

import numpy as np
import matplotlib.pyplot as plt

# Импортируем классы кнопки и слайдера
from matplotlib.widgets import Button, Slider


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def addPlot(graph_axes, sigma, mu):
    '''Добавить график к осям'''
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)
    graph_axes.plot(x, y)

    # Нужно для обновления графика
    plt.draw()


def onButtonAddClicked(event):
    '''Обработчик события для кнопки "Добавить"'''
    # Будем использовать sigma и mu, установленные с помощью слайдеров
    global slider_sigma
    global slider_mu
    global graph_axes

    # Используем атрибут val, чтобы получить значение слайдеров
    addPlot(graph_axes, slider_sigma.val, slider_mu.val)


def onButtonClearClicked(event):
    '''!!! Обработчик события нажатия кнопки "Очистить"'''
    global graph_axes

    graph_axes.clear()
    graph_axes.grid()
    plt.draw()


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим оси для кнопки
    axes_button_add = plt.axes([0.55, 0.05, 0.4, 0.075])

    # Создадим кнопку "Добавить"
    button_add = Button(axes_button_add, 'Добавить')
    button_add.on_clicked(onButtonAddClicked)

    # !!! Создадим кнопку "Очистить"
    axes_button_clear = plt.axes([0.05, 0.05, 0.4, 0.075])
    button_clear = Button(axes_button_clear, 'Очистить')
    button_clear.on_clicked(onButtonClearClicked)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.05, 0.25, 0.85, 0.04])
    slider_sigma = Slider(axes_slider_sigma,
                          label='σ',
                          valmin=0.1,
                          valmax=1.0,
                          valinit=0.5,
                          valfmt='%1.2f')

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.05, 0.17, 0.85, 0.04])
    slider_mu = Slider(axes_slider_mu,
                       label='μ',
                       valmin=-4.0,
                       valmax=4.0,
                       valinit=0.0,
                       valfmt='%1.2f')
    plt.show()

Теперь окно программы будет выглядеть следующим образом:

У слайдеров также имеется событие on_changed, подписавшись на которое, можно получать уведомления о том, что значение слайдера изменилось. Функция-обработчик события on_changed должна принимать один параметр, который будет представлять собой новое значение, установленное на слайдере (тип параметра - numpy.float64). Никакого идентификатора объекта, который создал событие, не передается. Таким образом, если нужно различать события от разных виджетов, то вы должны подписаться на события с помощью разных функций.

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

Код примера теперь выглядит таким образом:

import numpy as np
import matplotlib.pyplot as plt

# Импортируем класс слайдера
from matplotlib.widgets import Slider


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def updateGraph():
    '''!!! Функция для обновления графика'''
    # Будем использовать sigma и mu, установленные с помощью слайдеров
    global slider_sigma
    global slider_mu
    global graph_axes

    # Используем атрибут val, чтобы получить значение слайдеров
    sigma = slider_sigma.val
    mu = slider_mu.val
    x = np.linspace(-5.0, 5.0, 300)
    y = gaussian(sigma, mu, x)

    graph_axes.clear()
    graph_axes.plot(x, y)
    plt.draw()


def onChangeValue(value: np.float64):
    '''!!! Обработчик события изменения значений слайдеров'''
    updateGraph()


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.05, 0.25, 0.85, 0.04])
    slider_sigma = Slider(axes_slider_sigma,
                          label='σ',
                          valmin=0.1,
                          valmax=1.0,
                          valinit=0.5,
                          valfmt='%1.2f')

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.05, 0.17, 0.85, 0.04])
    slider_mu = Slider(axes_slider_mu,
                       label='μ',
                       valmin=-4.0,
                       valmax=4.0,
                       valinit=0.0,
                       valfmt='%1.2f')

    # !!! Подпишемся на события при изменении значения слайдеров.
    slider_sigma.on_changed(onChangeValue)
    slider_mu.on_changed(onChangeValue)

    updateGraph()
    plt.show()

Результат работы выглядит так:

При изменении значений σ и μ с помощью слайдеров кривая на графике перемещается влево-вправо и меняет свою ширину (а вместе с ней и высоту).

Помимо простого слайдера в Matplotlib есть также слайдер, который позволяет выбирать интервал - минимальное и максимальное значение какой-либо величины. Это RangeSlider. Для демонстрации его работы дополним предыдущий пример виджетом RangeSlider с помощью которого будем выбирать интервал отображения графика функции (xmin и xmax).

Работа с виджетом RangeSlider осуществляется точно так же, как и с виджетом Slider с тем лишь исключением, что свойство val устанавливает и возвращает кортеж из двух элементов типа float - (min_val, max_val). Такой же кортеж будет передан в обработчик события on_changed. Параметр valinit в конструкторе также должен быть кортежем из двух элементов и содержать начальные минимальное и максимальное значения, установленные с помощью такого слайдера.

from typing import Tuple

import numpy as np
import matplotlib.pyplot as plt

# Импортируем классы слайдеров
from matplotlib.widgets import RangeSlider, Slider


def gaussian(sigma, mu, x):
    '''Отображаемая фукнция'''
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi)) *
            np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def updateGraph():
    '''!!! Функция для обновления графика'''
    global slider_sigma
    global slider_mu
    global slider_x_range
    global graph_axes
    global x_min
    global x_max

    # Используем атрибут val, чтобы получить значение слайдеров
    sigma = slider_sigma.val
    mu = slider_mu.val

    # !!! Получаем значение интервала
    x_min, x_max = slider_x_range.val

    x = np.arange(x_min, x_max, 0.01)
    y = gaussian(sigma, mu, x)

    graph_axes.clear()
    graph_axes.plot(x, y)
    graph_axes.set_xlim(x_min, x_max)
    plt.draw()


def onChangeValue(value: np.float64):
    '''Обработчик события изменения значений μ и σ'''
    updateGraph()


def onChangeXRange(value: Tuple[np.float64, np.float64]):
    '''Обработчик события измерения значения интервала по оси X'''
    updateGraph()


if __name__ == '__main__':
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0
    x_min = -5
    x_max = 5

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.05, 0.25, 0.75, 0.04])
    slider_sigma = Slider(axes_slider_sigma,
                          label='σ',
                          valmin=0.1,
                          valmax=10.0,
                          valinit=0.5,
                          valfmt='%1.2f')

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.05, 0.17, 0.75, 0.04])
    slider_mu = Slider(axes_slider_mu,
                       label='μ',
                       valmin=-20.0,
                       valmax=20.0,
                       valinit=0.0,
                       valfmt='%1.2f')

    # !!! Создадим слайдер для задания интервала по оси X
    axes_slider_x_range = plt.axes([0.05, 0.09, 0.75, 0.04])
    slider_x_range = RangeSlider(axes_slider_x_range,
                                 label='x',
                                 valmin=-20.0,
                                 valmax=20.0,
                                 valinit=(x_min, x_max),
                                 valfmt='%1.2f')

    # Подпишемся на события при изменении значения слайдеров.
    slider_sigma.on_changed(onChangeValue)
    slider_mu.on_changed(onChangeValue)

    # !!! Подпишемся на событие изменения интервала по оси X
    slider_x_range.on_changed(onChangeXRange)

    updateGraph()
    plt.show()

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

Создание радиокнопок (класс RadioButtons)

Следующий виджет, который мы рассмотрим, - это наборы радиокнопок (переключателей, radio buttons). На мой взгляд, что радиокнопки, что флажки (CheckButtons), о которых мы поговорим чуть позже, не очень красиво выглядят, но если хочется обойтись для создания интерфейса только средствами Matplotlib без сторонних библиотек, то выбора у нас нет.

Изменим предыдущий пример таким образом, чтобы мы могли с помощью радиокнопок выбирать цвет линии для графика. Для простоты ограничимся тремя цветами: красным, синим и зеленым.

Группа радиокнопок создается аналогично другим виджетам: сначала надо создать оси, где они будут располагаться, а в конструктор класса RadioButtons (обратите внимание на "s" в конце имени класса) кроме созданных осей нужно передать список строковых меток - те надписи, которые будут соответствовать пунктам, один из которых будет выбран.

Класс RadioButtons также посылает событие on_clicked(), когда пользователь щелкает на них мышкой и меняет выбранное значение (или не меняет, если щелкает на уже выбранный переключатель). В отличие от одноименного события класса Button в качестве параметра функции-обработчика событий передается строковое значение, соответствующее метке выбранного переключателя.

У класса RadioButtons нет свойств или методов узнать номер выбранной в данный момент метки, мы можем узнать только текст метки выбранного пункта с помощью свойства value_selected.

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

from typing import Tuple

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import RangeSlider, Slider, RadioButtons


def gaussian(sigma, mu, x):
    """Отображаемая фукнция"""
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi))
            * np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def updateGraph():
    """!!! Функция для обновления графика"""
    global slider_sigma
    global slider_mu
    global slider_x_range
    global graph_axes
    global x_min
    global x_max

    # !!!
    global radiobuttons_color

    # !!! Словарь соответсвий текста и стиля линии
    colors = {"Красный": "r", "Синий": "b", "Зеленый": "g"}

    # Используем атрибут val, чтобы получить значение слайдеров
    sigma = slider_sigma.val
    mu = slider_mu.val

    # Получаем значение интервала
    x_min, x_max = slider_x_range.val

    x = np.arange(x_min, x_max, 0.01)
    y = gaussian(sigma, mu, x)

    # !!! Выберем стиль линии по выбранному значению радиокнопок
    style = colors[radiobuttons_color.value_selected]

    graph_axes.clear()
    graph_axes.plot(x, y, style)
    graph_axes.set_xlim(x_min, x_max)
    plt.draw()


def onRadioButtonsClicked(value: str):
    """!!! Обработчик события при клике по RadioButtons"""
    updateGraph()


def onChangeValue(value: np.float64):
    """Обработчик события изменения значений μ и σ"""
    updateGraph()


def onChangeXRange(value: Tuple[np.float64, np.float64]):
    """Обработчик события измерения значения интервала по оси X"""
    updateGraph()


if __name__ == "__main__":
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0
    x_min = -5
    x_max = 5

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()
    graph_axes.grid()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.3, 0.25, 0.5, 0.04])
    slider_sigma = Slider(
        axes_slider_sigma,
        label="σ",
        valmin=0.1,
        valmax=10.0,
        valinit=0.5,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.3, 0.17, 0.5, 0.04])
    slider_mu = Slider(
        axes_slider_mu,
        label="μ",
        valmin=-20.0,
        valmax=20.0,
        valinit=0.0,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания интервала по оси X
    axes_slider_x_range = plt.axes([0.3, 0.09, 0.5, 0.04])
    slider_x_range = RangeSlider(
        axes_slider_x_range,
        label="x",
        valmin=-20.0,
        valmax=20.0,
        valinit=(x_min, x_max),
        valfmt="%1.2f",
    )

    # !!! Создадим оси для переключателей
    axes_radiobuttons = plt.axes([0.05, 0.09, 0.17, 0.2])

    # !!! Создадим переключатель
    radiobuttons_color = RadioButtons(
        axes_radiobuttons, ["Красный", "Синий", "Зеленый"]
    )

    # Подпишемся на события при изменении значения слайдеров.
    slider_sigma.on_changed(onChangeValue)
    slider_mu.on_changed(onChangeValue)

    # Подпишемся на событие изменения интервала по оси X
    slider_x_range.on_changed(onChangeXRange)

    # !!! Подпишемся на событие при переключении радиокнопок
    radiobuttons_color.on_clicked(onRadioButtonsClicked)

    updateGraph()
    plt.show()

В этом примере по сравнению с прошлым также были изменены координаты левых границ и ширины осей для слайдеров. Результат работы программы выглядит следующим образом:

Создание флажков (класс CheckButtons)

Следующий виджет, который нам надо рассмотреть, - это набор флажков - CheckButtons (так же обратите внимание на букву "s" в конце имени класса).

При создании класса CheckButtons в конструктор помимо осей передаются два списка. Первый из них (labels) должен содержать строки, которые будут являться метками для флажков (по этому параметру определяется количество флажков), а второй список (actives) является не обязательным, но если он передан, он должен иметь такую же длину, что и список labels, и должен содержать булевы значения, которые соответствуют начальному состоянию флажков (True - флажок установлен, False - не установлен). Если в конструктор не предать параметр actives, то по умолчанию флажки не будут установлены.

Для отслеживания изменения состояния флажков нужно подписаться на событие on_clicked. Функция-обработчик события будет принимать один строковый параметр, который будет равен метке флажка, на который нажали (как и в случае с классом RadioButtons).

Для определения состояний флажков, в классе CheckButtons предусмотрен метод get_status(), который возвращает список булевых значений, длина которого равна количеству флажков. Каждый элемент этого списка показывает, установлен ли соответствующий флажок (он равен True, если флажок установлен и False, если не установлен).

Добавим в предыдущий пример один флажок, с помощью которого можно будет выбирать, нужно ли отображать отображать сетку на графике:

from typing import Tuple

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import RangeSlider, Slider, RadioButtons, CheckButtons


def gaussian(sigma, mu, x):
    """Отображаемая фукнция"""
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi))
            * np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def updateGraph():
    """!!! Функция для обновления графика"""
    global slider_sigma
    global slider_mu
    global slider_x_range
    global graph_axes
    global x_min
    global x_max
    global radiobuttons_color
    global checkbuttons_grid

    # Словарь соответсвий текста и стиля линии
    colors = {"Красный": "r", "Синий": "b", "Зеленый": "g"}

    # Используем атрибут val, чтобы получить значение слайдеров
    sigma = slider_sigma.val
    mu = slider_mu.val

    # Получаем значение интервала
    x_min, x_max = slider_x_range.val

    x = np.arange(x_min, x_max, 0.01)
    y = gaussian(sigma, mu, x)

    # Выберем стиль линии по выбранному значению радиокнопок
    style = colors[radiobuttons_color.value_selected]

    graph_axes.clear()
    graph_axes.plot(x, y, style)
    graph_axes.set_xlim(x_min, x_max)

    # !!! Определим, нужно ли показывать сетку на графике
    grid_visible = checkbuttons_grid.get_status()[0]
    graph_axes.grid(grid_visible)

    plt.draw()


def onCheckClicked(value: str):
    """!!! Обработчик события при нажатии на флажок"""
    updateGraph()


def onRadioButtonsClicked(value: str):
    """Обработчик события при клике по RadioButtons"""
    updateGraph()


def onChangeValue(value: np.float64):
    """Обработчик события изменения значений μ и σ"""
    updateGraph()


def onChangeXRange(value: Tuple[np.float64, np.float64]):
    """Обработчик события измерения значения интервала по оси X"""
    updateGraph()


if __name__ == "__main__":
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0
    x_min = -5
    x_max = 5

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.3, 0.25, 0.5, 0.04])
    slider_sigma = Slider(
        axes_slider_sigma,
        label="σ",
        valmin=0.1,
        valmax=10.0,
        valinit=0.5,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.3, 0.17, 0.5, 0.04])
    slider_mu = Slider(
        axes_slider_mu,
        label="μ",
        valmin=-20.0,
        valmax=20.0,
        valinit=0.0,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания интервала по оси X
    axes_slider_x_range = plt.axes([0.3, 0.09, 0.5, 0.04])
    slider_x_range = RangeSlider(
        axes_slider_x_range,
        label="x",
        valmin=-20.0,
        valmax=20.0,
        valinit=(x_min, x_max),
        valfmt="%1.2f",
    )

    # Создадим оси для переключателей
    axes_radiobuttons = plt.axes([0.05, 0.09, 0.17, 0.2])

    # Создадим переключатель
    radiobuttons_color = RadioButtons(
        axes_radiobuttons, ["Красный", "Синий", "Зеленый"]
    )

    # !!! Создадим оси для флажка
    axes_checkbuttons = plt.axes([0.05, 0.01, 0.17, 0.07])

    # !!! Создадим флажок
    checkbuttons_grid = CheckButtons(axes_checkbuttons, ["Сетка"], [True])

    # Подпишемся на события при изменении значения слайдеров.
    slider_sigma.on_changed(onChangeValue)
    slider_mu.on_changed(onChangeValue)

    # Подпишемся на событие изменения интервала по оси X
    slider_x_range.on_changed(onChangeXRange)

    # Подпишемся на событие при переключении радиокнопок
    radiobuttons_color.on_clicked(onRadioButtonsClicked)

    # !!! Подпишемся на событие при клике по флажку
    checkbuttons_grid.on_clicked(onCheckClicked)

    updateGraph()
    plt.show()

Результат работы этого скрипта выглядит следующим образом:

Не сказал бы, что флажок выглядит очень красиво, но что есть, то есть. В Matplotlib 3.7 появилась возможность влиять на внешний вид CheckButtons с помощью параметров конструктора frame_props и check_props, но в данной статье мы их рассматривать не будем.

Создание поля ввода (класс TextBox)

И последний виджет, который нам осталось рассмотреть, - это поле для ввода текста, которое создается с помощью класса TextBox. Конструктор этого класса принимает два обязательный параметра: экземпляр осей, где будет располагаться виджет, и строковый параметр (label), который должен содержать надпись около поля ввода. Можно задать также строковый параметр initial, который обозначает, какая строка будет введена в поле ввода по умолчанию. Кроме того есть еще несколько параметров, влияющих на внешний вид виджета, но мы их использовать не будем.

Виджет TextBox позволяет подписаться на два события:

  • on_text_change() срабатывает при изменении текста в поле ввода.
  • on_submit() срабатывает, когда пользователь нажимает клавишу Enter, а фокус при этом находится в поле ввода.

Оба события вызывают функцию-обработчик, которая должна принимать один параметр, в качестве которого будет передана строка из поля ввода. Кроме того, текущую строку в поле ввода можно узнать через свойство text виджета TextBox.

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

from typing import Tuple

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import (RangeSlider, Slider,
                                RadioButtons, CheckButtons,
                                TextBox)


def gaussian(sigma, mu, x):
    """Отображаемая фукнция"""
    return (1.0 / (sigma * np.sqrt(2.0 * np.pi))
            * np.exp(-((x - mu) ** 2) / (2 * sigma * sigma)))


def updateGraph():
    """!!! Функция для обновления графика"""
    global slider_sigma
    global slider_mu
    global slider_x_range
    global graph_axes
    global x_min
    global x_max
    global radiobuttons_color
    global checkbuttons_grid

    # Словарь соответсвий текста и стиля линии
    colors = {"Красный": "r", "Синий": "b", "Зеленый": "g"}

    # Используем атрибут val, чтобы получить значение слайдеров
    sigma = slider_sigma.val
    mu = slider_mu.val

    # Получаем значение интервала
    x_min, x_max = slider_x_range.val

    x = np.arange(x_min, x_max, 0.01)
    y = gaussian(sigma, mu, x)

    # Выберем стиль линии по выбранному значению радиокнопок
    style = colors[radiobuttons_color.value_selected]

    graph_axes.clear()
    graph_axes.plot(x, y, style)
    graph_axes.set_xlim(x_min, x_max)

    # Определяем, нужно ли показывать сетку на графике
    grid_visible = checkbuttons_grid.get_status()[0]
    graph_axes.grid(grid_visible)

    plt.draw()


def onTitleChange(value: str):
    """!!! Обработчик события при изменении текста в поле ввода"""
    global graph_axes
    graph_axes.set_title(value)
    plt.draw()


def onCheckClicked(value: str):
    """Обработчик события при нажатии на флажок"""
    updateGraph()


def onRadioButtonsClicked(value: str):
    """Обработчик события при клике по RadioButtons"""
    updateGraph()


def onChangeValue(value: np.float64):
    """Обработчик события изменения значений μ и σ"""
    updateGraph()


def onChangeXRange(value: Tuple[np.float64, np.float64]):
    """Обработчик события измерения значения интервала по оси X"""
    updateGraph()


if __name__ == "__main__":
    # Начальные параметры графиков
    current_sigma = 0.2
    current_mu = 0.0
    x_min = -5
    x_max = 5

    # Создадим окно с графиком
    fig, graph_axes = plt.subplots()

    # Выделим область, которую будет занимать график
    fig.subplots_adjust(left=0.07, right=0.95, top=0.95, bottom=0.4)

    # Создадим слайдер для задания sigma
    axes_slider_sigma = plt.axes([0.3, 0.25, 0.5, 0.04])
    slider_sigma = Slider(
        axes_slider_sigma,
        label="σ",
        valmin=0.1,
        valmax=10.0,
        valinit=0.5,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания mu
    axes_slider_mu = plt.axes([0.3, 0.17, 0.5, 0.04])
    slider_mu = Slider(
        axes_slider_mu,
        label="μ",
        valmin=-20.0,
        valmax=20.0,
        valinit=0.0,
        valfmt="%1.2f",
    )

    # Создадим слайдер для задания интервала по оси X
    axes_slider_x_range = plt.axes([0.3, 0.09, 0.5, 0.04])
    slider_x_range = RangeSlider(
        axes_slider_x_range,
        label="x",
        valmin=-20.0,
        valmax=20.0,
        valinit=(x_min, x_max),
        valfmt="%1.2f",
    )

    # Создадим оси для переключателей
    axes_radiobuttons = plt.axes([0.05, 0.09, 0.17, 0.2])

    # Создадим переключатель
    radiobuttons_color = RadioButtons(
        axes_radiobuttons, ["Красный", "Синий", "Зеленый"]
    )

    # Создадим оси для флажка
    axes_checkbuttons = plt.axes([0.05, 0.01, 0.17, 0.07])

    # Создадим флажок
    checkbuttons_grid = CheckButtons(axes_checkbuttons, ["Сетка"], [True])

    # !!! Создадим оси для текстового поля
    axes_textbox = plt.axes([0.4, 0.01, 0.4, 0.05])

    # !!! Создадим текстовое поле
    textbox_title = TextBox(axes_textbox, "Заголовок")

    # Подпишемся на события при изменении значения слайдеров.
    slider_sigma.on_changed(onChangeValue)
    slider_mu.on_changed(onChangeValue)

    # Подпишемся на событие изменения интервала по оси X
    slider_x_range.on_changed(onChangeXRange)

    # Подпишемся на событие при переключении радиокнопок
    radiobuttons_color.on_clicked(onRadioButtonsClicked)

    # Подпишемся на событие при клике по флажку
    checkbuttons_grid.on_clicked(onCheckClicked)

    # !!! Подпишемся на события текстового поля
    textbox_title.on_text_change(onTitleChange)
    textbox_title.on_submit(onTitleChange)

    updateGraph()
    plt.show()

Результат работы этого скрипта выглядит следующим образом:

Заключение

Мы рассмотрели те виджеты из пакета matplotlib.widgets, которые относятся к созданию элементов интерфейса. Мы научились создавать кнопки (класс Button), ползунки (слайдеры, классы Slider и RangeSlider), группы переключателей (радиокнопок, класс RadioButtons), группы флажков (класс CheckButtons) и поле ввода (класс TextBox), рассмотрели события, которые они могут посылать, и параметры, которые принимают обработчики событий. В этот пакет еще входят виджеты для различных выделений областей на графиках, но это тема для отдельной статьи.

При создании виджета нужно сначала создать оси с помощью функции matplotlib.pyplot.axes (экземпляр класса matplotlib.axes.Axes), в которых будут располагаться создаваемые виджеты.

Но если вам все же не хватает возможностей, предоставляемых пакетом matplotlib.widgets, то для создания интерфейса можно воспользоваться, например, библиотекой wxPython.

Похожие статьи

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 4.6/5. Всего 21 голос(а, ов)



 22.03.2023 - 13:43


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 712