Оформление графиков: легенды, аннотации, сетки и стили 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()
График отображается под редактором
legend() без аргументов собрал подписи со всех линий текущих axes. Передать список вручную — ax.legend(["Днём", "Ночью"]) — тоже можно, но хрупко: список привязан к порядку отрисовки, и после перестановки линий подписи соврут. label= у линии хранит имя вместе с данными, поэтому надёжнее.
Отдельный случай — pie из пятого урока: там имена категорий передаются параметром labels= прямо в plt.pie(), а legend() собирает их же в легенду, когда подписи вокруг круга не помещаются. Приём работает и для столбцов: у примитивов, которые возвращает ax.bar, можно задать label= — и легенда объяснит, что за группы перед вами.
| loc | Куда встанет легенда |
|---|---|
| best | Matplotlib сам подберёт свободное место (значение по умолчанию) |
| 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:
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 — он тоже безопасен.
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("Файл сохранён")
Файл сохранён
Ориентиры: 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"])
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 — сколько пикселей будет в файле?
Оформите отчёт кафе: две линии — выручка и расходы за неделю, каждая со своим label. Легенда в левом верхнем углу, аннотация со стрелкой «Пик: 62» к точке максимума выручки, сетка только по оси Y пунктиром. Координаты категорий: «Пн» — это 0, «Сб» — это 5.
Как добавить легенду в 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Matplotlib / Seaborn · Урок 2
Линейные графики в Matplotlib: plot, цвета, стили и подписи
plt.plot() под микроскопом: цвета, маркеры, стили линий, легенды, диапазоны осей и кейс с двумя температурными кривыми за месяц.
Matplotlib / Seaborn · Урок 5
Круговые диаграммы и subplots: несколько графиков на одном полотне
pie и donut с честными границами применимости, сетка графиков через plt.subplots, общий заголовок suptitle и мини-дашборд 2 на 2 в финале.
Matplotlib / Seaborn · Урок 7
Seaborn для начинающих: красивые графики одной строкой
Seaborn рисует статистические графики из DataFrame одной строкой: темы set_theme, countplot, barplot с доверительными интервалами и разбивка hue — прямо в браузере.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
openpyxl · Урок 13
„Умные“ таблицы Excel в openpyxl: объект Table
Превращаем прайс в «умную» таблицу Excel: Table с диапазоном ref, TableStyleInfo с полосками, фильтры в шапке — и правила имени, из-за которых openpyxl чаще всего ругается.
openpyxl добавить таблицуpython excel для начинающих
openpyxl · Урок 5
Форматирование ячеек: шрифт, заливка и границы в openpyxl
Шапка прайса — жирный белый на синем, границы на всю таблицу: Font, PatternFill и Border, и главное правило — один стиль на весь диапазон.
openpyxl стили ячеекopenpyxl стили в цикле
Matplotlib / Seaborn · Урок 1
Визуализация данных в Python: первое знакомство с Matplotlib
Первый график в Python за три строки кода, анатомия figure и axes, шпаргалка «какой график для каких данных» и сохранение в PNG. Всё запускается прямо на странице.
matplotlibmatplotlib для начинающих
Проверьте знания по Matplotlib / Seaborn
В челлендже — 20 задач по Matplotlib / Seaborn, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Matplotlib / Seaborn: 20 задач