Шпаргалка по визуализации данных в Python с помощью Plotly
Plotly — библиотека для визуализации данных, состоящая из нескольких частей:
- Front-End на JS
- Back-End на Python (за основу взята библиотека Seaborn)
- Back-End на R
В этой простыне все примеры разобраны от совсем простых к более сложным, так что разработчикам с опытом будет скучно. Так же эта «шпаргалка» не заменит на 100% примеры из документации.
Извиняюсь за замыленные gif'ки это происходит при конвертации из видео, записанного с экрана.
Jupyter Notebook со всеми примерами из статьи:
Документация
Так же на базе plotly и веб-сервера Flask существует специальная библиотека для создания дашбордов Dash.
- Plotly — бесплатная библиотека, которую вы можете использовать в коммерческих целях
- Plotly работает offline
- Plotly позволяет строить интерактивные визуализации
Т.е. с помощью Plotly можно как изучать какие-то данные «на лету» (не перестраивая график в matplotlib, изменяя масштаб, включая/выключая какие-то данные), так и построить полноценный интерактивный отчёт (дашборд).
Для начала необходимо установить библиотеку, т.к. она не входит ни в стандартный пакет, ни в Anaconda. Для этого рекомендуется использовать pip:
pip install plotly
Если вы используете Jupyter Notebook, то можно использовать мэджик "!", поставив данный символ перед командой:
!pip install plotly
Перед началом работы необходимо импортировать модуль. В разных частях шпаргалки для разных задач нам понадобятся как основной модуль, так и один из его подмодулей, поэтому полный набор инструкций импорта у нас.
Так же нам понадобятся библиотеки Pandas и Numpy для работы с сырыми данными
import plotly
import plotly.graph_objs as go
import plotly.express as px
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd
Линейный график
Начнём с простой задачи построения графика по точкам.
Используем функцию f(x)=x2
Сперва поступим совсем просто и «в лоб»:
- Создадим график с помощью функции scatter из подмодуля plotly.express (внутрь передадим 2 списка точек: координаты X и Y)
- Тут же «покажем» его с помозью метода show()
Обратите внимание — график интерактивный, если навести на него курсор, то можно его приближать и удалять, выделять участки, по наведению курсора на точку получать подробную информацию, возвращать картинку в исходное положение, а при необходимости «скриншотить» и сохранять как файл.
Всё это делается с помощью JS в вашем браузере. А значит, при желании вы можете этим управлять уже после построения фигуры (но мы этого делать пожалуй не будем, т.к. JS != Python)
x = np.arange(0, 5, 0.1)
def f(x):
return x**2
px.scatter(x=x, y=f(x)).show()
Более читабельно и правильно записать тот же в код в следующем виде:
fig = px.scatter(x=x, y=f(x))
fig.show()
- Создаём фигуру
- Рисуем график
- Показываем фигуру
2 строчки и готовый результат. Т.к. мы используем Express. Быстро и просто.
Но маловато гибкости, поэтому мы практически сразу переходим к более продвинутому уровню — сразу создадим фигуру и нанесём на неё объекты.
Так же сразу выведем фигуру для показа с помощью метода show().
В отличие от Matplotlib отдельные объекты осей не создаются, хотя мы с ними ещё столкнёмся, когда захотим построить несколько графиков вместе
fig = go.Figure()
#Здесь будет код
fig.show()
Как видим, пока пусто.
Чтобы добавить что на график нам понадобится метод фигуры add_trace.
fig.add_trace(ТУТ_ТО_ЧТО_ХОТИМ_ПЕРЕДАТЬ_ДЛЯ_ОТОБРАЖЕНИЯ_И_ГДЕ)
Но ЧТО мы хотим нарисовать? График по точкам. График мы уже рисовали с помощью Scatter в Экспрессе, у Объектов есть свой Scatter, давайте глянем что он делает:
go.Scatter(x=x, y=f(x))
А теперь объединим:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.show()
Как видим, отличия не только в коде, но и в результате — получилась гладкая кривая.
Кроме того, такой способ позволит нам нанести на график столько кривых, сколько мы хотим:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.add_trace(go.Scatter(x=x, y=x))
fig.show()
Погодите, что это такое? Справа появилась ещё и легенда!
Впрочем, логично, пока график был один, зачем нам легенда?
Но магия Plotly тут не заканчивается. Нажмите на любую из подписей в легенде и соответствующий график исчезнет, а надпись станет более бледной. Вернуть их позволит повторный клик.
Подписи графиков
Добавим атрибут name, в который передадим строку с именем графика, которое мы хотим отображать в легенде.
Plotly поддерживает LATEX в подписях (аналогично matplotlib через использование $$ с обеих сторон).
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='$$f(x)=x^2$$'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.show()
К сожалению, это имеет свои ограничения, как можно заметить подсказка при наведении на график отображается в «сыром» виде, а не в LATEX.
Победить это можно, если использовать HTML разметку в подписях. В данном примере я буду использовать тег sup. Так же заметьте, что шрифт для LATEX и HTML отличается начертанием.
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.show()
С увеличением длины подписи графика, легенда начала наезжать на график. Мне это не нравится, поэтому перенесём легенду вниз.
Для этого применим к фигуре метод update_layout, у которого нас интересует атрибут legend_orientation fig.update_layout(legend_orientation="h")
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h")
fig.show()
Хорошо, но слишком большая часть рабочего пространства ноутбука не используется. Особенно это заметно сверху — большой отступ сверху до поля графика.
По умолчанию поля графика имеют отступ 20 пикселей. Мы можем задать свои значения отступам с помощью update_layout, у которого есть атрибут margin, принимающий словарь из отступов:
- l — отступ слева
- r — отступ справа
- t — отступ сверху
- b — отступ снизу
Зададим везде нулевые отступы fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
update_layout можно применять последовательно несколько раз, либо можно передать все аргументы в одну функцию (мы сделаем именно так)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
margin=dict(l=0, r=0, t=0, b=0))
fig.show()
Поскольку подписи в легенде короткие, мне не нравится, что они расположены слева. Я бы предпочёл выровнять их по центру.
Для этого можно использовать у update_layout атрибут legend, куда передать словарь с координатами для сдвига (сдвиг может быть и по вертикали, но мы используем только горизонталь).
Сдвиг задаётся в долях от ширины всей фигуры, но важно помнить, что сдвигается левый край легенды. Т.е. если мы укажем 0.5 (50% ширины), то надпись будет на самом деле чуть сдвинута вправо.
Т.к. реальная ширина зависит от особенностей вашего экрана, браузера, шрифтов и т.п., то этот параметр часто приходится подгонять. Лично у меня для этого примера неплохо работает 0.43.
Чтобы не шаманить с шириной, можно легенду относительно точки сдвига с помощью аргумента xanchor.
В итоге для легенды мы получим такой словарь:
legend=dict(x=.5, xanchor="center")
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.show()
Стоит сразу задать подписи к осям и графику в целом. Для этого нам вновь понадобится update_layout, у которого добавится 3 новых аргумента:
title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title",
Следует заметить, что сдвиги, которые мы задали ранее могут негавтивно сказаться на читаемости подписей (так заголовок графика вообще вытесняется из области видимости, поэтому я увеличу отступ сверху с 0 до 30 пикселей
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title",
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Вернёмся к самим графикам, и вспомним, что они состоят из точек. Выделим их с помощью атрибута mode у самих объектов Scatter.
Используем разные варианты выделения для демонстрации:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.show()
Теперь особенно заметно, что LATEX в функции g(x)=x отображается некорректно при наведении курсора мыши на точки.
Давайте скроем эту информацию.
Зададим для всех графиков с помощью метода update_traces поведение при наведении. Это регулирует атрибут hoverinfo, в который передаётся маска из имён атрибутов, например, «x+y» — это только информация о значениях аргумента и функции:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="x+y")
fig.show()
Как-то недостаточно наглядно, не находите?
Давайте разрешим использовать информацию из всех аргументов и сами зададим шаблон подсказки.
- hoverinfo=«all»
- в hovertemplate передаём строку, используем HTML для форматирования, а имена переменных берём в фигурные скобки и выделяем %, например, %{x}
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='g(x)=x'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
А что если мы хотим сравнить информацию на 2 кривых в точках, например, с одинаковых аргументом?
Т.к. это касается всей фигуры, нам нужен update_layout и его аргумент hovermode.
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='g(x)=x'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Кстати, маркерами можно управлять для конкретной кривой и явно.
Для этого используется аргумент marker, который принимает на вход словарь. Подробный пример.
А мы лишь ограничимся баловством:
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Кажется теперь на графике плохо видно ту часть, где кривые пересекаются (вероятно наиболее интересную для нас).
Для этого у нас есть методы фигуры:
- update_yaxes — ось Y (вертикаль)
- update_xaxes — ось X (горизонталь)
С их помощью зададим интервалы отображения для осей (это только начальное положение, ничто не мешает нам сменить масштаб в процессе взаимодействия с графиком).
fig = go.Figure()
fig.update_yaxes(range=[-0.5, 1.5])
fig.update_xaxes(range=[-0.5, 1.5])
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Хорошо, но правильно было бы нанести осевые линии.
Для этого у тех же функций есть 3 атрибута:
- zeroline — выводить или нет осевую линию
- zerolinewidth — задаёт толщину осевой (в пикселях)
- zerolinecolor — задаёт цвет осевой (строка, можно указать название цвета, можно его код, как принято в HTML-разметке)
fig = go.Figure()
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink')
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000')
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Давайте добавим больше разных функций на наш график, но сделаем так, чтобы по умолчанию их не было видно.
Для этого у объекта Scatter есть специальный атрибут:
visible='legendonly'
Т.к. мы центрировали легенду относительно точки сдвига, то нам не пришлось менять величину сдвига с увеличением числа подписей.
def h(x):
return np.sin(x)
def k(x):
return np.cos(x)
def m(x):
return np.tan(x)
fig = go.Figure()
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink')
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000')
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'))
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Наверное всё же не следует смешивать вместе тригонометрические и арифметические функции. Давайте отобразим их на разных, соседних графиках.
Для этого нам потребуется создать фигуру с несколькими осями.
Фигура с несколькими графиками создаётся с помощью подмодуля make_subplots.
Необходимо указать количество:
- row — строк
- col — столбцов
А при построении графика передать «координаты» графика в этой «матрице» (сперва строка, потом столбец)
fig = make_subplots(rows=1, cols=2, specs=[[{'type':'domain'}, {'type':'domain'}]])
fig = make_subplots(rows=1, cols=2)
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink')
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000')
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1)
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Заметили, наши изменения осей применились к обоим графикам?
Естественно, если у метода, изменяющего оси указать аргументы:
- row — координата строки
- col — координата столбца
то можно изменить ось только на конкретном графике:
fig = make_subplots(rows=1, cols=2)
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1)
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
А вот если бездумно использовать title, xaxis_title и yaxis_title для update_layout, то может выйти казус — подписи применятся только к 1 графику:
fig = make_subplots(rows=1, cols=2)
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1)
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=30, b=0))
fig.update_layout(title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title")
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Поэтому заголовки графиков можно задать, при создании фигуры, передав в аргумент subplot_titles кортеж/список с названиями.
Подписи осей под графиками можно поменять с помощью методов фигуры:
- fig.update_xaxes
- fig.update_yaxes
Передавая в них номер строки и колонки (т.е. «координаты изменяемого графика»)
fig = make_subplots(rows=1, cols=2, subplot_titles=("Plot 1", "Plot 2"))
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1)
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=40, b=0))
fig.update_layout(title="Plot Title")
fig.update_xaxes(title='Ось X графика 1', col=1, row=1)
fig.update_xaxes(title='Ось X графика 2', col=2, row=1)
fig.update_yaxes(title='Ось Y графика 1', col=1, row=1)
fig.update_yaxes(title='Ось Y графика 2', col=2, row=1)
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
И конечно, если необходимо сделать так, чтобы один график был больше, а другой меньше, то для этого используется аргументы
- column_widths — задаёт отношения ширины графиков (в одной строке)
- row_heights — задаёт отношения высот графиков (в одном столбце)
Каждый из этих параметров принимает список чисел, которых должно быть столько, сколько графиков в строке/столбце. Отношения чисел задают отношения ширин или высот.
Рассмотрим на примере ширин. Сделаем левый график вдвое шире правого, т.е. зададим соотношение 2:1.
fig = make_subplots(rows=1, cols=2, column_widths=[2, 1])
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 1, 1)
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
А что если мы хотим выделить одному из графиков больше места, чем другим, например, 2 строки или наоборот, 2 столбца?
В matplotlib мы использовали бы несколько фигур, либо оси с заданными размерами, здесь у нас есть другой инструмент. Мы можем сказать каким-то осям объединиться вдоль колонок или вдоль строк.
Для этого нам потребуется написать спецификацию на фигуру (для начала очень простую).
Спецификация — это список (если точнее, то даже матрица из списков), каждый объект внутри которого — словарь, описывающий одни из осей.
Если каких-то осей нет (например, если их место занимают растянувшиеся соседи, то вместо словаря передаётся None.
Давайте сделаем матрицу 2х2 и объединим вместе левые графики, получив одни высокие вертикальные оси. Для этого первому графику передадим атрибут «rowspan» равный 2, а его нижнего соседа уничтожим (None):
specs=[
[{"rowspan": 2}, {}],
[None, {}]
]
fig = make_subplots(rows=2, cols=2,
specs=[[{"rowspan": 2}, {}], [None, {}]])
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 1, 2)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 1, 2)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Как видим, в вертикальный график идеально вписался тангенс, который отныне не невидим.
Для объединения используется:
- rowspan — по вертикали
- colspan — по горизонтали
fig = make_subplots(rows=2, cols=2,
specs=[[{"colspan": 2}, None], [{}, {}]])
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 2, 1)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 2, 1)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Больше примеров использования make_subplots
Последний вариант получился слишком узким по вертикали.
Высоту легко увеличить в помощью атрибута height у метода update_layout.
Размеры фигуры регулируются 2 атрибутами:
- width — ширина (в пикселях)
- height — высота (в пикселях)
Но следует помнить, если вы встраиваете фигуры plotly куда-то (а это логично, если вы делаете дашборд, например), то фигура занимает всё отведённое пространство по ширине, поэтому не изменять ширину в plotly будет не лучшей идеей. Лучше использовать стили в разметке.
fig = make_subplots(rows=2, cols=2,
specs=[[{"colspan": 2}, None], [{}, {}]])
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink', col=2)
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000', col=2)
fig.add_trace(go.Scatter(x=x, y=h(x), name='h(x)=sin(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=k(x), name='k(x)=cos(x)'), 2, 2)
fig.add_trace(go.Scatter(x=x, y=m(x), name='m(x)=tg(x)'), 1, 1)
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'), 2, 1)
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))), 2, 1)
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0),
height=1000,
width=600)
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Увеличиваем плотность информации
Тепловая карта
Вернёмся к 1 графику, но постараемся уместить на нём больше информации, используя цветовую кодировку (что-то вроде тепловой карты — чем выше значение некой величины, тем «теплее» цвет).
Для этого у объекта go.Scatter используем уже знакомый атрибут marker (напомним, он принимает словарь). Передаём следующие атрибуты в словарь:
- color — список значений по которым будут выбираться цвета. Элементов списка должно быть столько же, сколько и точек.
- colorbar — словарь, описывающий индикационную полосу цветов справа от графика. Принимает на вход словарь. Нас интересует пока только 1 значение словаря — title — заголовок полосы.
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"))
))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
В предыдущем примере цветовая шкала не очень похожа на тепловую карту.
На самом деле цвета на шкале можно изменить, для этого служит атрибут colorscale, в который передаётся имя палитры.
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno')
))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Можно ли добавить больше информации? Конечно можно, но тут возникают хитрости.
Для ещё одного измерения можно использовать размер маркеров.
Важно. Размер — задаётся в пикселях, т.е. величина не отрицательная (в отличие от цвета), поэтому мы будем использовать модуль одной из функций.
так же, величины меньше 2 пикселей обычно плохо видны на экране, поэтому для размера мы добавим множитель.
Размеры задаётся атрибутом size того же словаря внутри marker. Этот атрибут принимает 1 значение (число), либо список (чисел).
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno',
size=50*abs(h(x)))
))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=abs(h(x)), name='h_mod(x)=|sin(x)|'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Анимация
Можно ли ещё уплотнить информацию на графике? Да, можно, если использовать «четвёртое измерение» — время. Это так же может быть полезно и само по себе для оживленя вашего графика.
Вернёмся на пару шагов назад. Мы будем анимировать график построения параболы. Для этого нам понадобятся:
- Начальное состояние
- Кнопки (анимация не должна начинаться сама по себе, поэтому для начала мы создадим простую кнопку, её запускающую, а постепенно перейдём к временной шкале)
- Фреймы (или кадры) — промежуточные состояния
1. Начальное состояние
Это то, что будет на графике до начала анимации. В нашем случае это будет стартовая точка.
Уберём практически всё лишнее из предыдущих шагов
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
2. Кнопка
Код минимальной работоспособной кнопки выглядит так:
"updatemenus": [{"type": "buttons",
"buttons": [{"label": "Your Label",
"method": "animate",
"args": [See Below]}]}]
updatemenus — это один из элементов слоя, т.е. layout фигуры, а значит, мы добавим кнопку с помощью метода update_layout.
Пока она не будет ничего делать, т.к. у нас нечего анимировать.
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
3. Фреймы
Это список «кадров» из которых состоит наша анимация.
Каждый фрейм должен содержать внутри себя целиком готовый график, который просто будет отображаться на нашей фигуре, как в декорациях.
Фрейм создаётся с помощью go.Frame()
График передаётся внутрь фрейма в аргумент data.
Таким образом, если мы хотим построить последовательность графиков (от 1 точки до целой фигуры), нам надо просто пройти в цикле:
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i], y=f(x[:i]))]))
После этого фреймы необходимо передать в фигуру. У каждой фигуры есть атрибут frames, который мы и будем использовать:
fig.frames = frames
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Другой способ задать начальное состояние, слой (с кнопками) и фреймы — сразу передать всё в объект go.Figure:
- data — атрибут для графика с начальным состоянием
- layout — описание «декораций» включая кнопки
- frames — фреймы (кадры) анимации
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]))]))
fig = go.Figure(data=go.Scatter(x=[x[0]], y=[f(x[0])], mode='lines+markers', name='f(x)=x<sup>2</sup>'),
frames=frames,
layout=dict(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0)))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Естественно, если добавить на графики (как на начальный, так и те, что во фреймах) маркеры с указанием цвета, цветовой шкалы и размера, то анимация будет более сложного графика.
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0])))
))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Заметим, что код простейшей кнопки, которая запускает воспроизведение видео выглядит так:
dict(label="Play", method="animate", args=[None])
или
dict(label="", method="animate", args=[None])
Если мы хотим добавить кнопку «пауза» (в отличие от стандартной паузы повторное нажатие не будет вызывать воспроизведение, для начала воспроизведения придётся нажат Play), код усложнится:
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])
Правда, если добавить 2 такие кнопки, то вы заметите, что кнопка play, нажатая после паузы, в итоге начинает воспроизведение с начала. Это не совсем интуитивное поведение, поэтому ей следует добавить ещё 1 аргумент:
dict(label="", method="animate", args=[None, {"fromcurrent": True}])
Теперь полный набор из 2 наших кнопок будет выглядеть так:
buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])]
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0])))
))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Иногда полезно перенести кнопки в другоме место. Рассмотрим некоторые из атрибутов, которые с этим помогут:
- direction — направление расположения кнопок (по умолчанию сверху-вниз, но если указать «left», то будет слева-направо)
- x, y — положение (в долях от фигуры)
- xanchor, yanchor — как выравнивать кнопки. У нас была раньше проблема с выравниванием легенд, тут та же история. Если хотим выровнять по центру, то x=0.5 и xanchor=«center» помогут.
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0])))
))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(direction="left", x=0.5, xanchor="center", y=0,
type="buttons", buttons=[dict(label="►", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None],