При работе с визуализациями данных в Matplotlib часто возникает необходимость тонкой настройки легенды. Легенда является ключевым элементом, связывающим графические объекты на канвасе (линии, точки, столбцы) с их текстовыми описаниями. Чтобы управлять легендой программно, необходимо понимать, как Matplotlib связывает эти две сущности. Эту связь обеспечивают дескрипторы (handles) и метки (labels).
Что такое дескрипторы (handles) в Matplotlib?
В контексте легенды Matplotlib, дескриптор — это объект, который визуально представляет элемент на графике, отображаемый в легенде. Это может быть линия (Line2D), маркер (PathCollection для scatter-графиков), объект прямоугольника (Rectangle для bar-графиков) или любой другой художник (artist), который Matplotlib умеет отображать в легенде. Дескриптор служит своего рода "визитной карточкой" графического элемента для системы построения легенды.
Зачем нужны дескрипторы и метки?
Прямой доступ к дескрипторам и меткам необходим, когда стандартные возможности автоматического создания легенды недостаточны. Типичные сценарии включают:
Тонкая настройка внешнего вида легенды: Изменение размера маркеров, толщины линий, цветов, шрифтов текста меток.
Изменение порядка элементов: Легенда автоматически упорядочивает элементы в порядке их добавления на Axes, но иногда требуется другой порядок (например, по алфавиту или по значению).
Фильтрация элементов: В сложной визуализации может потребоваться отобразить в легенде не все элементы, а только подмножество.
Динамическое обновление легенды: Изменение меток или дескрипторов в процессе выполнения программы, например, при интерактивном взаимодействии.
Создание кастомной легенды: Построение легенды с нуля, используя нестандартные дескрипторы.
Обзор структуры легенды Matplotlib
Легенда в Matplotlib представлена объектом matplotlib.legend.Legend. Этот объект создается либо автоматически при вызове ax.legend(), либо вручную. Объект Legend содержит коллекцию записей (LegendEntry), каждая из которых состоит из двух частей: дескриптора (визуального представления) и соответствующего текстового объекта (метки). Методы и атрибуты объекта Legend позволяют получить доступ к этим составляющим для дальнейшей манипуляции.
Получение дескрипторов и меток из объекта Axes
Наиболее распространенный способ получить дескрипторы и метки, связанные с объектами на Axes, — это использование метода ax.get_legend_handles_labels(). Этот метод сканирует дочерние объекты Axes, ищет тех художников (artists), которые могут быть представлены в легенде, и возвращает соответствующие дескрипторы и их метки.
Использование ax.get_legend_handles_labels() для получения дескрипторов и меток
Метод ax.get_legend_handles_labels() возвращает кортеж из двух списков: первый список содержит дескрипторы (объекты Artist), а второй — соответствующие им метки (строки).
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.artist import Artist
from typing import List, Tuple
def get_handles_and_labels(axis: plt.Axes) -> Tuple[List[Artist], List[str]]:
"""
Получает дескрипторы и метки для всех подходящих художников на заданном Axes.
Args:
axis: Объект Axes, с которого нужно получить данные.
Returns:
Кортеж из двух списков: дескрипторов и их соответствующих меток.
"""
handles, labels = axis.get_legend_handles_labels()
return handles, labels
Важно отметить, что этот метод ищет потенциальные элементы легенды среди дочерних объектов Axes, независимо от того, была ли легенда уже создана или нет. Он использует атрибут _label у художников для определения текста метки.
Пример: Получение дескрипторов и меток из простой диаграммы рассеяния
Рассмотрим пример с диаграммой рассеяния, где точки сгруппированы по категориям, и каждая категория представлена своим цветом и меткой для легенды.
import matplotlib.pyplot as plt
import numpy as np
# Генерация синтетических данных, имитирующих пользователей по каналам привлечения
np.random.seed(42)
num_points = 100
channels = ['Organic', 'Paid Search', 'Social', 'Referral']
data_x = np.random.rand(num_points) * 100 # Метрика 1 (например, стоимость привлечения)
data_y = np.random.rand(num_points) * 1000 # Метрика 2 (например, доход от пользователя)
categories = np.random.choice(channels, num_points) # Канал привлечения
fig, ax = plt.subplots(figsize=(10, 6))
# Строим диаграмму рассеяния для каждой категории
for i, channel in enumerate(channels):
# Фильтруем данные по каналу
x_channel = data_x[categories == channel]
y_channel = data_y[categories == channel]
# Строим scatter plot для канала, присваивая метку для легенды
ax.scatter(x_channel, y_channel, label=channel, alpha=0.7, s=50)
# Получаем дескрипторы и метки
handles, labels = ax.get_legend_handles_labels()
print(f"Получено дескрипторов: {len(handles)}")
print(f"Получено меток: {len(labels)}")
print("Метки:", labels)
# Теперь можно использовать полученные handles и labels
# Например, создать легенду (если она еще не создана)
# ax.legend(handles, labels)
# Или выполнить другие манипуляции...
# plt.show() # Не вызываем plt.show() в примере для статьи
В этом примере ax.get_legend_handles_labels() просканирует Axes после добавления всех scatter-объектов и вернет список объектов PathCollection (дескрипторы) и список строк с названиями каналов (метки), которые были указаны в аргументе label при вызове ax.scatter().
Разбор возвращаемых значений: дескрипторы и метки
Дескрипторы (handles): Это список объектов Artist. Каждый объект в этом списке соответствует визуальному элементу, который может быть отображен в легенде. Тип объекта зависит от типа построенного графика: Line2D для plot(), PathCollection для scatter(), Rectangle для bar() и так далее. Эти объекты можно инспектировать и модифицировать (например, изменить их цвет или стиль) перед построением легенды.
Метки (labels): Это список строк. Каждая строка — это текст, который будет ассоциирован с соответствующим дескриптором в легенде. Эти строки берутся из аргумента label, переданного при создании художника, либо из других источников, если label не указан (что нежелательно для автоматического создания легенды). Метки можно изменять, фильтровать или переупорядочивать.
Полученные списки дескрипторов и меток всегда имеют одинаковую длину и соответствующий порядок. Это позволяет легко управлять парами (дескриптор, метка).
Работа с дескрипторами и метками напрямую из легенды
После того как объект легенды Legend создан (с помощью ax.legend() или fig.legend()), вы можете получить доступ к дескрипторам и меткам непосредственно через этот объект. Это полезно, если вам нужно изменить легенду после ее создания, например, обновить ее внешний вид или содержимое без пересоздания.
Получение объекта Legend
Объект Legend можно получить, сохранив результат вызова метода .legend():
# ... код построения графика ...
legend_obj = ax.legend()
Если легенда уже существует на Axes, ее можно получить через атрибут ax.get_legend(). Этот метод вернет объект Legend, если он присутствует, или None, если легенда еще не была создана.
existing_legend: plt.legend.Legend | None = ax.get_legend()
if existing_legend is not None:
print("Легенда найдена")
# Дальнейшая работа с existing_legend
else:
print("Легенда не найдена")
Доступ к дескрипторам и меткам через объект Legend
Объект Legend предоставляет доступ к своим внутренним дескрипторам и текстовым объектам:
legend_obj.legendHandles: Список дескрипторов, используемых в легенде. Это те же объекты, что возвращает ax.get_legend_handles_labels(), но только для тех элементов, которые фактически отображены в созданной легенде.
legend_obj.get_texts(): Список объектов matplotlib.text.Text для меток легенды. В отличие от ax.get_legend_handles_labels(), который возвращает строки, этот метод возвращает объекты Text, которые можно напрямую модифицировать (например, изменить шрифт, цвет текста и т.д.). Чтобы получить сами строки меток из этих объектов, нужно вызвать .get_text() на каждом из них.
# При условии, что legend_obj получен
legend_handles: List[Artist] = legend_obj.legendHandles
legend_texts: List[plt.text.Text] = legend_obj.get_texts()
print(f"Дескрипторов в объекте Legend: {len(legend_handles)}")
print(f"Текстовых объектов в объекте Legend: {len(legend_texts)}")
# Получение строковых меток из текстовых объектов
string_labels: List[str] = [text_obj.get_text() for text_obj in legend_texts]
print("Метки из объекта Legend:", string_labels)
Эти списки также имеют соответствующий порядок.
Пример: Изменение порядка элементов в легенде
Предположим, мы хотим изменить порядок элементов в уже существующей легенде, отсортировав их метки по алфавиту.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.artist import Artist
from typing import List, Tuple
# (Используем тот же код генерации данных и построения графика, что и выше)
# ... (код построения scatter-графика с метками) ...
# Создаем легенду в исходном порядке
original_legend = ax.legend(title="Канал")
print("Исходный порядок меток:", [t.get_text() for t in original_legend.get_texts()])
# Получаем дескрипторы и метки (можно из Axes или из объекта легенды, если он уже создан)
# Получение из Axes более надежно, если легенда могла быть создана с фильтрацией и т.п.
handles, labels = ax.get_legend_handles_labels()
# Сортируем пары (дескриптор, метка) по метке
# zip объединяет два списка в список кортежей (handle, label)
# sorted сортирует эти кортежи по второму элементу (label)
# *zip(*) "разворачивает" отсортированный список кортежей обратно в два списка
sorted_handles, sorted_labels = zip(*sorted(zip(handles, labels), key=lambda t: t[1]))
print("Отсортированный порядок меток:", list(sorted_labels))
# Удаляем старую легенду
original_legend.remove()
# Создаем новую легенду с отсортированными дескрипторами и метками
ax.legend(sorted_handles, sorted_labels, title="Канал (сортировка)")
# fig.canvas.draw_idle() # Обновить канвас, если работаем в интерактивном режиме
# plt.show() # Не вызываем plt.show() в примере для статьи
Этот пример демонстрирует типичный шаблон: получить текущие дескрипторы и метки, выполнить с ними нужные манипуляции (в данном случае — сортировку) и затем пересоздать легенду, передав ей измененные списки.
Продвинутые методы работы с дескрипторами и метками
Гибкий доступ к дескрипторам и меткам открывает двери для более сложных сценариев управления легендой.
Фильтрация дескрипторов и меток (например, для скрытия определенных элементов)
Предположим, мы хотим скрыть в легенде элементы с метками ‘Referral’. Мы можем отфильтровать списки дескрипторов и меток перед передачей их в ax.legend().
# ... (код построения графика) ...
handles, labels = ax.get_legend_handles_labels()
# Фильтруем, оставляя только те пары (handle, label), где label не 'Referral'
filtered_handles = [h for h, l in zip(handles, labels) if l != 'Referral']
filtered_labels = [l for h, l in zip(handles, labels) if l != 'Referral']
# Создаем легенду только для отфильтрованных элементов
ax.legend(filtered_handles, filtered_labels, title="Канал (без Referral)")
# plt.show()
Этот подход позволяет точно контролировать, какие элементы появятся в легенде, особенно полезно для сложных графиков с большим количеством однотипных объектов.
Изменение меток динамически (например, на основе данных)
Метки могут быть сгенерированы или изменены после построения графика на основе каких-либо расчетов. Например, можно добавить к метке процентное соотношение или суммарное значение для категории.
# ... (код генерации данных и построения графика по каналам) ...
handles, labels = ax.get_legend_handles_labels()
# Рассчитываем общее количество точек для каждого канала
channel_counts = {channel: np.sum(categories == channel) for channel in channels}
# Формируем новые метки, добавляя количество в скобках
updated_labels = [f"{label} ({channel_counts.get(label, 0)})" for label in labels]
# Создаем легенду с обновленными метками
ax.legend(handles, updated_labels, title="Канал (кол-во)")
# plt.show()
Таким образом, метки в легенде становятся более информативными, отражая сводные данные по каждой категории.
Создание пользовательских дескрипторов для легенды
В некоторых случаях может потребоваться добавить в легенду элементы, которые не связаны напрямую с художниками на Axes, или использовать нестандартные визуальные представления. Matplotlib позволяет создавать