Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 6 из 10 Средний 40 мин 120 XP

Оформление графиков: легенды, аннотации, сетки и стили Matplotlib

Легенда, аннотации со стрелками, сетка, готовые стили и rcParams — доводим график Matplotlib до презентационного уровня и сохраняем в PNG для слайдов.

Редакция Питоники

К пятому уроку вы умеете строить все базовые типы графиков: линии, столбцы, гистограммы, диаграммы рассеяния, панели subplots. Теперь взгляните на любую картинку из прошлых уроков честно: серый фон по умолчанию, линии без имён, ни одной подписи на важных точках. Данные верные — а показать руководителю стыдно. Оформление графика и есть разница между черновиком для себя и картинкой для других.

План урока: легенды и их позиции, аннотации со стрелками, сетка, готовые стили plt.style, словарь глобальных настроек rcParams, палитры для дальтоников и экспорт с нужным dpi. В конце соберём всё в один презентационный график и превратим его в чек-лист.

Легенда: как добавить легенду в Matplotlib

Самый частый вопрос новичка — как добавить легенду matplotlib. Механика двухшаговая: каждой линии присваивается имя параметром label=, затем ax.legend() собирает имена в блок. Пропустите любой из двух шагов — легенды не будет. Вот температура за неделю двумя линиями, с именами (цвета и параметры линий мы разбирали в уроке про линейные графики plot: цвета, маркеры и стили):

Две линии и легенда
import matplotlib.pyplot as plt

days = [1, 2, 3, 4, 5, 6, 7]
tmax = [24, 26, 27, 25, 28, 30, 29]
tmin = [14, 15, 16, 15, 17, 19, 18]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(days, tmax, label="Днём")     # label попадёт в легенду
ax.plot(days, tmin, label="Ночью")
ax.legend()                            # собирает все label автоматически
ax.set_title("Температура за неделю")
plt.show()
Вывод
График отображается под редактором
Первый запуск загружает matplotlib — это займёт до минуты

legend() без аргументов собрал подписи со всех линий текущих axes. Передать список вручную — ax.legend(["Днём", "Ночью"]) — тоже можно, но хрупко: список привязан к порядку отрисовки, и после перестановки линий подписи соврут. label= у линии хранит имя вместе с данными, поэтому надёжнее.

Отдельный случай — pie из пятого урока: там имена категорий передаются параметром labels= прямо в plt.pie(), а legend() собирает их же в легенду, когда подписи вокруг круга не помещаются. Приём работает и для столбцов: у примитивов, которые возвращает ax.bar, можно задать label= — и легенда объяснит, что за группы перед вами.

locКуда встанет легенда
bestMatplotlib сам подберёт свободное место (значение по умолчанию)
upper rightПравый верхний угол
upper leftЛевый верхний угол
lower leftЛевый нижний угол
center leftСлева, по вертикальному центру
upper centerСверху, по горизонтали по центру

Куда поставить легенду: loc и вынос за пределы графика

Значения loc покрывают девять случаев из десяти. Но если данные плотные, легенда в любом углу что-то перекроет. Тогда её выносят за пределы axes через bbox_to_anchor: координаты задаются в долях графика, и точка (0.5, -0.15) означает «по центру снизу, чуть ниже оси X»:

Легенда под графиком
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3], [2, 5, 4], label="Коммуналка")
ax.plot([1, 2, 3], [3, 4, 6], label="Продукты")

# Легенда за пределами осей: по центру снизу
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.15), ncols=2)
ax.set_title("Легенда вынесена под график")
plt.show()
Вывод
График отображается под редактором

Аннотации в Matplotlib: annotate со стрелками

Легенда отвечает на вопрос «что здесь за линия», аннотация — «что здесь произошло». Синтаксис ax.annotate(текст, xy=..., xytext=...): xy — точка, куда указывает стрелка, xytext — где стоит текст, arrowprops — вид стрелки. Отметим пик выручки за неделю:

Стрелка на пике выручки
import matplotlib.pyplot as plt

days = [1, 2, 3, 4, 5, 6, 7]
revenue = [42, 38, 45, 51, 66, 89, 74]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(days, revenue, marker="o")
ax.annotate("Суббота: пик 89 тыс.",
            xy=(6, 89),          # точка данных, куда указывает стрелка
            xytext=(3.2, 84),    # где разместить текст
            arrowprops=dict(arrowstyle="->", color="black"))
ax.set_title("Выручка магазина за неделю")
ax.set_xlabel("День недели")
ax.set_ylabel("Тыс. руб.")
plt.show()
Вывод
График отображается под редактором

И xy, и xytext по умолчанию задаются в системе данных — тех же числах, что на осях. Хотите смещать текст не в единицах данных, а в пунктах? Добавьте textcoords="offset points": xytext=(0, 15) поднимет подпись на 15 пунктов над точкой, и она не разъедется при смене масштаба.

Простой текст: ax.text

Когда стрелка не нужна, достаточно ax.text(x, y, "строка") — те же координаты данных, никаких стрелок и рамок. Удобно для ремарок: «смена поставщика», «начало отпуска», пороговое значение. Стиль задают fontsize и color:

Ремарка без стрелки
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3, 4, 5, 6], [12, 13, 15, 14, 17, 19])
ax.text(2.2, 15.8, "Рост ускорился с марта", fontsize=11, color="0.3")
ax.set_title("ax.text — подпись без стрелки")
plt.show()
Вывод
График отображается под редактором

Пороговые линии: axhline и axvline

Аннотации часто идут в паре с горизонтальными и вертикальными отсечками: план продаж, граница нормы, день релиза. Их рисуют axhline(y) и axvline(x) — линии через весь график с обычными linestyle и color. Комбинация «пороговая линия + подпись с числом» — готовый паттерн отчётности: глаз видит границу, число снимает вопросы. Заметьте в примере textcoords="offset points" — текст поднят на 8 пунктов над линией и никуда не съедет при смене масштаба:

Продажи против плана
import matplotlib.pyplot as plt

months = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн"]
sales = [68, 74, 71, 79, 84, 82]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(months, sales, marker="o")
ax.axhline(75, color="#D55E00", linestyle="--", linewidth=1.5)
ax.annotate("План: 75", xy=(0.5, 75), xytext=(0, 8),
            textcoords="offset points", color="#D55E00")
ax.set_title("Продажи против плана")
plt.show()
Вывод
График отображается под редактором

Сетка на графике: ax.grid

Сетка — опора для глаза: без неё значение «на глаз» не снять. ax.grid(True) включает линии по обеим осям, но на практике чаще нужна только горизонтальная: в столбчатых диаграммах и временных рядах читают высоту, а вертикальные линии только рябят. Сравните:

Два варианта сетки рядом
import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5, 6]
y = [4, 7, 6, 9, 8, 12]

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))

ax1.plot(x, y, marker="o")
ax1.set_title("grid(True): по обеим осям")
ax1.grid(True)

ax2.plot(x, y, marker="o")
ax2.set_title("Только Y, пунктир, прозрачность")
ax2.grid(True, axis="y", linestyle=":", alpha=0.6)

plt.show()
Вывод
График отображается под редактором

Три параметра закрывают почти всё: axis="y" — сетка только по нужной оси, linestyle=":" — пунктир вместо сплошных линий, alpha=0.6 — прозрачность, чтобы сетка не спорила с данными. Забыли, что вообще можно менять у линии? plt.setp(ax.lines[0]) напечатает весь список её свойств. Это мой дефолт для отчётных графиков: пунктир по Y, полупрозрачный. Сплошная сетка по двум осям оправдана разве что на технических чертежах.

Готовые стили Matplotlib: plt.style

Прописывать оформление каждому графику вручную — путь к туннельному синдрому. В Matplotlib есть готовые стили: наборы настроек, включаемые одним вызовом plt.style.use(). Посмотрим, что вообще доступно в установленной версии:

Проверяем наличие стилей
import matplotlib.pyplot as plt

print("ggplot" in plt.style.available)
print("fivethirtyeight" in plt.style.available)
print(len(plt.style.available) > 20)
Вывод
True
True
True

В plt.style.available — несколько десятков стилей, и я проверил их через in, а не считал по списку: состав зависит от версии библиотеки, а вот наличие конкретных имён стабильно. Вот стили, которые стоит знать в лицо:

СтильНа что похож
ggplotТёмно-серый фон и сетка в духе знаменитого пакета ggplot2 из R
fivethirtyeightКрупные линии и сетка а-ля сайт FiveThirtyEight
dark_backgroundЧёрный фон — для слайдов в тёмном зале
bmhМягкие тона из книги Bayesian Methods for Hackers
seaborn-v0_8-whitegridБелый фон с сеткой — то, что в уроке 7 включит set_theme
grayscaleОттенки серого — для чёрно-белой печати

plt.style.use() включается глобально и действует до конца сессии. Покрасить только один график, не трогая остальные, удобнее контекстным менеджером plt.style.context: внутри блока работает стиль, на выходе всё возвращается. Вот четыре стиля на одних и тех же данных:

Один код — четыре стиля
import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [2, 5, 4, 8, 7]
styles = ["classic", "ggplot", "fivethirtyeight", "dark_background"]

fig, axes = plt.subplots(2, 2, figsize=(9, 6))
for ax, name in zip(axes.flat, styles):
    with plt.style.context(name):
        ax.plot(x, y, marker="o")
        ax.set_title(name, fontsize=9)
fig.suptitle("Один и тот же код — четыре стиля")
plt.show()
Вывод
График отображается под редактором

Стиль и ручные настройки не конфликтуют: сначала включается стиль, затем поверх — свои rcParams или параметры методов. Типовая связка для отчётов: plt.style.use("seaborn-v0_8-whitegrid") в начале скрипта плюс цвета Okabe-Ito и подписи в каждом графике. Две строки — и серый дефолт остался в прошлом. В уроке 7 увидите, как Seaborn включает похожее оформление через set_theme — идея та же, только приятнее.

rcParams: глобальные настройки оформления

Каждый винтик оформления хранится в словаре plt.rcParams: толщина линий, размер шрифта, цвет сетки, отступы. Присвоение plt.rcParams["lines.linewidth"] = 2.5 меняет настройку до конца сессии. Точечная временная настройка делается через rc_context — по аналогии со style.context:

rc_context: толстые линии только внутри блока
import matplotlib.pyplot as plt

print(plt.rcParams["lines.linewidth"])   # значение по умолчанию

with plt.rc_context({"lines.linewidth": 2.5, "font.size": 13}):
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [2, 5, 4], label="Пример")
    ax.legend()
    ax.set_title("Толстая линия и крупный шрифт")

print(plt.rcParams["lines.linewidth"])   # вернулось к дефолту

plt.show()
Вывод
1.5
1.5

Цвета, которые читают все: палитры для дальтоников

Около 8% мужчин имеют нарушения цветовосприятия — красно-зелёная пара линий для них неотличима, а аудитория отчётов шире, чем вам кажется. Универсальное решение — палитра Okabe-Ito: восемь цветов, подобранных так, чтобы различаться при любом типе зрения. Два самых ходовых: #0072B2 (синий) и #D55E00 (красно-оранжевый). Для непрерывных шкал вроде colorbar и heatmap берите colormap viridis — он тоже безопасен.

Две линии в цветах Okabe-Ito
import matplotlib.pyplot as plt

# Палитра Okabe-Ito: различима при любом типе зрения
OI = {"sky": "#56B4E9", "green": "#009E73", "vermillion": "#D55E00"}

months = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн"]
online = [120, 135, 128, 150, 172, 165]
offline = [140, 138, 125, 130, 122, 118]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(months, online, color=OI["green"], linewidth=2.5, label="Онлайн-заказы")
ax.plot(months, offline, color=OI["vermillion"], linewidth=2.5, label="Магазин")
ax.set_title("Два канала продаж: цвета Okabe-Ito")
ax.legend()
plt.show()
Вывод
График отображается под редактором

Графическое совершенство — это простое представление сложных данных: уходит всё, что не является данными, и остаётся самое важное.

— Эдвард Тафти, «The Visual Display of Quantitative Information»

Экспорт презентационного качества: dpi

Картинка хороша настолько, насколько её можно показать. dpi (dots per inch) — плотность точек при сохранении: пиксели файла равны дюймам figsize, умноженным на dpi. Фигура 10 на 4 дюйма при dpi=300 — это 3000 на 1200 пикселей: хватит и на слайд, и на печать.

Сохраняем слайд в высоком разрешении
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot([1, 2, 3, 4], [3, 7, 5, 9])
ax.set_title("Слайд для презентации")

# figsize 10x4 при dpi=300 даст 3000x1200 пикселей
fig.savefig("slide.png", dpi=300, bbox_inches="tight")
print("Файл сохранён")
Вывод
Файл сохранён
В браузерной песочнице файл уходит в виртуальную файловую систему и живёт до перезагрузки страницы — сохранение проверяйте локально. Формула пикселей: figsize, умноженный на dpi.

Ориентиры: dpi=300 — печать и журналы, 150–200 — презентации и веб, 100 — черновики. bbox_inches="tight" обрезает пустые поля вокруг графика — без него по краям остаётся рамка из воздуха. И помните ловушку из первого урока: savefig() вызывается до show(), иначе в файле окажется белый лист.

Кейс: доводим график до презентационного уровня

Соберём всё вместе. Задача — июльский отчёт о температуре: две линии (днём и ночью), лёгкая заливка между ними, стрелка на пике, сетка по Y, легенда, экспортный размер. Шесть строк данных и примерно десять — оформления:

Итоговый график: июльский отчёт
import matplotlib.pyplot as plt
import numpy as np

# Суточные температуры за июль
rng = np.random.default_rng(42)
days = np.arange(1, 31)
tmin = 14 + 2 * np.sin(days / 5) + rng.normal(0, 0.8, len(days))
tmax = 27 + 3 * np.sin(days / 6) + rng.normal(0, 1.1, len(days))
hottest = tmax.argmax()

fig, ax = plt.subplots(figsize=(10, 4.5))
ax.plot(days, tmax, color="#D55E00", linewidth=2, label="Днём")
ax.plot(days, tmin, color="#0072B2", linewidth=2, label="Ночью")
ax.fill_between(days, tmin, tmax, color="#56B4E9", alpha=0.15)

ax.annotate("Пик: " + str(round(tmax[hottest])) + " C",
            xy=(days[hottest], tmax[hottest]),
            xytext=(days[hottest] - 6, tmax.max() + 2.5),
            arrowprops=dict(arrowstyle="->", color="0.2"))

ax.set_title("Июль: суточный ход температуры")
ax.set_xlabel("День июля")
ax.set_ylabel("Температура, C")
ax.set_xlim(1, 30)
ax.set_ylim(10, 36)
ax.grid(True, axis="y", linestyle=":", alpha=0.7)
ax.legend(loc="lower left")

plt.show()
Вывод
График отображается под редактором

Обратите внимание на fill_between: полупрозрачная заливка между линиями делает амплитуду суточного хода видимой без чтения чисел. Приём тот же, что в диаграммах рассеяния из четвёртого урока: alpha не спорит с данными, а подсказывает. И ещё одна деталь: annotate поднял текст выше пика через tmax.max() + 2.5 — координата вычислена из данных, а не подобрана руками, поэтому график не сломается на другом месяце.

  • У графика есть заголовок, у осей — подписи с единицами измерения.
  • У каждой линии есть label, легенда не перекрывает данные.
  • Важная точка отмечена annotate со стрелкой, в тексте есть число.
  • Сетка полупрозрачная и только по читаемой оси.
  • Цвета различимы для дальтоников: Okabe-Ito или Seaborn-палитра colorblind.
  • Для файла задан dpi не ниже 200 и bbox_inches="tight".

Что дальше

Полный контроль — суперсила Matplotlib, но платят за неё временем: даже простой график требует полдюжины строк. Seaborn автоматизирует большую часть этой рутины — в следующем уроке Seaborn для начинающих: красивые графики одной строкой та же аккуратность будет занимать одну строку, от тем set_theme до countplot прямо по DataFrame. А если нужно освежить композицию из нескольких панелей, вернитесь к уроку про subplots и круговые диаграммы. Общая карта курса — самоучитель Matplotlib, оформление здесь шестая тема из десяти.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2], [1, 2], label="revenue")
ax.plot([1, 2], [2, 3], label="costs")
handles, labels = ax.get_legend_handles_labels()
print(len(labels))
import matplotlib.pyplot as plt

print(plt.rcParams["lines.linewidth"])
plt.rcParams["lines.linewidth"] = 2.5
print(plt.rcParams["lines.linewidth"])
Проверь себя
0 / 5

1. Что нужно, чтобы линия попала в легенду?

2. Куда поставит легенду вызов ax.legend(loc="lower left")?

3. Что означает xy=(0.5, 0.5) в ax.annotate() без дополнительных параметров?

4. Что сделает plt.style.use("ggplot")?

5. figsize=(10, 4) и dpi=300 при savefig — сколько пикселей будет в файле?

Карточки терминов
Запомнено: 0 / 6
Практика

Оформите отчёт кафе: две линии — выручка и расходы за неделю, каждая со своим label. Легенда в левом верхнем углу, аннотация со стрелкой «Пик: 62» к точке максимума выручки, сетка только по оси Y пунктиром. Координаты категорий: «Пн» — это 0, «Сб» — это 5.

practice.py
Вопросы и ответы по уроку

Как добавить легенду в Matplotlib?

Дайте каждой линии подпись ax.plot(x, y, label="Днём") и вызовите ax.legend(). Позиция задаётся loc="upper left" и подобными значениями, вынос за пределы графика — bbox_to_anchor=(0.5, -0.15).

Как вынести легенду за пределы графика?

ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.15)) — координаты задаются в долях графика, и (0.5, -0.15) означает «по центру снизу, чуть ниже оси X». ncols=2 расставит подписи в два столбца.

Почему легенда не отображается, хотя legend() вызван?

Ни у одной линии нет label=: Matplotlib пишет предупреждение No artists with labels found to put in legend и рисует пустой блок. Добавьте label= каждой линии и вызовите legend() заново.

Как подписать конкретную точку на графике Python?

ax.annotate("текст", xy=(x, y), xytext=(x2, y2), arrowprops=dict(arrowstyle="->")) — координаты задаются в системе данных графика. Для текста без стрелки есть ax.text(x, y, "строка").

Какое dpi выбрать, чтобы сохранить график для презентации?

Для слайдов достаточно 150–200, для печати — 300. Пиксели файла равны размеру фигуры в дюймах, умноженному на dpi, а bbox_inches="tight" обрезает пустые поля вокруг графика.

Понравился урок? Сошлитесь на него

«Оформление графика и есть разница между черновиком для себя и картинкой для других.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Matplotlib / Seaborn

В челлендже — 20 задач по Matplotlib / Seaborn, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Matplotlib / Seaborn: 20 задач