Создание интерфейса средствами библиотеки Matplotlib
Дата публикации: 02.03.2017
Дата последней правки: 18.04.2023
Содержание
- Введение
- Создание кнопок (класс Button)
- Создание слайдеров (классы Slider и RangeSlider)
- Создание радиокнопок (класс RadioButtons)
- Создание флажков (класс CheckButtons)
- Создание поля ввода (класс TextBox)
- Заключение
- Похожие статьи
- Комментарии
Введение
В статье Как рисовать графики с помощью 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 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(), в которую в качестве параметра будет передаваться список из четырех чисел:
- координата X левой границы;
- координата Y нижней границы;
- ширина;
- высота.
Точка (0; 0) располагается в левом нижнем углу окна, а правый верхний угол окна имеет координату (1.0; 1.0).
Конструктор класса Button принимает два обязательных параметра:
- ax - Экземпляр класса Axes.
- label - надпись на кнопке.
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 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 принимает несколько обязательных и необязательных параметров. Обязательные параметры это:
- ax - Экземпляр класса Axes.
- label - текстовая метка около слайдера.
- valmin - минимальное значение, которое можно установить с помощью слайдера.
- valmax - максимальное значение, которое можно установить с помощью слайдера.
Необязательных параметров существует больше, но в данном примере мы будем использовать два из них:
- valinit - начальное значение для слайдера (по умолчанию устанавливается на значение 0.5). На следующей картинке обратите внимание на красные линии на слайдерах - это и есть отметки для начального значения. Класс Slider имеет метод reset() для установки значения к начальному значению.
- valfmt - строка форматирования текущего значения, установленного с помощью слайдера (значение по умолчанию - '%1.2f'). С помощью этого параметра можно имитировать случай, когда требуется устанавливать целочисленные значения с помощью слайдера, поскольку у этого виджета нет такого понятия как дискрет перемещения, и он всегда хранит число с плавающей точкой.
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 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 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 в конструкторе также должен быть кортежем из двух элементов и содержать начальные минимальное и максимальное значения, установленные с помощью такого слайдера.
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.
Пример, который позволяет выбирать цвет для линии выглядит следующим образом:
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, если не установлен).
Добавим в предыдущий пример один флажок, с помощью которого можно будет выбирать, нужно ли отображать отображать сетку на графике:
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() экземпляра класса осей графика.
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.
Похожие статьи
- Другие статьи про Matplotlib
- Обработка событий мыши и клавиатуры на графиках Matplotlib
- Как рисовать графики с помощью Matplotlib при использовании wxPython
- Применение объектно-ориентированного стиля
Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.