Анатомия фигуры Matplotlib

Figure — холст, Axes — система координат, Axis — ось. Путаница между ними рождает 90% вопросов на форумах.

«Matplotlib кажется хаотичным, пока вы не увидите его иерархию объектов. После этого всё встаёт на места».

Matplotlib строится на чёткой иерархии. Figure — это весь холст (окно/картинка). Внутри неё живут одна или несколько Axes — это отдельные графики (системы координат). У каждой Axes есть две (или три) Axis — собственно оси X и Y с делениями и подписями. Всё остальное — линии, точки, заголовки, легенда — это художественные элементы (artists), размещённые внутри Axes.

Figure  (весь холст, fig)
  |
  +-- Axes  (один график, ax)
  |     |
  |     +-- Axis X  -> ticks, labels, xlabel
  |     +-- Axis Y  -> ticks, labels, ylabel
  |     +-- Title
  |     +-- Artists -> Line2D, Bar, Scatter ...
  |     +-- Legend
  |
  +-- Axes  (второй график при subplots)

Ключевое осознание: вы почти всегда работаете с ax (Axes), а не с plt напрямую. plt.title() ставит заголовок «текущей» Axes, а ax.set_title() — конкретной. Когда графиков несколько, «текущая» становится ловушкой.

Почему так важно держать эту иерархию в голове? Потому что подавляющее большинство «странных» проблем новичка — это путаница между уровнями. Подпись попала не на тот график — перепутали Figure и Axes. Заголовок всей картинки наезжает на верхний subplot — нужен fig.suptitle() (заголовок Figure), а не ax.set_title() (заголовок одной Axes). Легенда задваивается или пропадает — обратились не к тому уровню. Когда вы чётко понимаете, что Figure отвечает за общий холст, размер и сохранение, Axes — за систему координат и данные, а Axis — лишь за оси с делениями, документация Matplotlib читается как карта: у каждого метода есть свой «адресат».

Полезно знать и про родственные термины. plt.subplots(2, 2) создаёт одну Figure и массив из четырёх Axes — обращаться к ним нужно по индексам (axes[0, 1]). Отдельная сущность — colorbar: технически это ещё одна маленькая Axes, пристроенная сбоку, поэтому её настраивают как полноценный объект, а не как свойство основного графика. А fig.add_axes([...]) позволяет вручную поместить Axes в произвольное место холста по относительным координатам 0..1 — так делают вставки «график в графике» (inset). Всё это — варианты одной и той же трёхуровневой модели, и знание модели снимает страх перед любой компоновкой.

Как работает под капотом

Когда вы зовёте fig, ax = plt.subplots(), Matplotlib создаёт объект Figure и вкладывает в него Axes, возвращая ссылки на оба. Дальнейшие вызовы ax.plot(...), ax.set_xlabel(...) добавляют artists в эту конкретную Axes. Рендеринг (backend) превращает дерево объектов в пиксели или в SVG только в момент show() или savefig().

Сетка делений на оси — это просто выбранные «круглые» числа в диапазоне данных. Сгенерируем их вручную.

# Как Matplotlib выбирает деления оси: "круглые" шаги внутри диапазона
data_min, data_max = 3.0, 47.0

def nice_ticks(lo, hi, count=5):
    span = hi - lo
    raw_step = span / count
    # округляем шаг до 1, 2, 5 * 10^k
    import math
    mag = 10 ** math.floor(math.log10(raw_step))
    for m in (1, 2, 5, 10):
        if raw_step <= m * mag:
            step = m * mag
            break
    start = math.floor(lo / step) * step
    ticks = []
    t = start
    while t <= hi + step:
        ticks.append(round(t, 4))
        t += step
    return ticks

print(nice_ticks(data_min, data_max))

«Попробуй сам ▶» — поменяйте диапазон и увидите, как меняются деления. Matplotlib делает это автоматически, но теперь вы знаете логику.

Частые ошибки

Смешивать plt.* и ax.* в одном скрипте и удивляться, что подпись попала «не туда». Забыть, что при нескольких subplots «текущая» Axes — последняя созданная. Звать plt.show() несколько раз и терять график (после show фигура очищается в некоторых backend).

Ещё несколько типичных граблей растут из непонимания иерархии. Путать fig.suptitle() и ax.set_title(): первый ставит общий заголовок всей фигуры, второй — отдельной панели; перепутали — и заголовок наезжает на верхний график. Менять размер картинки через ax, хотя размер — это свойство Figure (figsize при создании или fig.set_size_inches()). Сохранять файл из «текущей» фигуры (plt.savefig()) в скрипте, где фигур несколько — уйдёт не та; надёжнее fig.savefig() на конкретный объект. Забывать про constrained_layout=True или fig.tight_layout() и получать наезжающие подписи между subplots. И, наконец, обращаться к axes[i] как к одному объекту, когда plt.subplots вернул двумерный массив — там нужны два индекса axes[row, col] или предварительный axes.ravel().

Best practices

  • Всегда начинайте с fig, ax = plt.subplots() и работайте через ax.
  • Сохраняйте ссылки на fig и ax — это объектно-ориентированный стиль, рекомендованный документацией.
  • Один savefig до show: показ может очистить фигуру.

Итог: иерархия Figure → Axes → Axis — это карта Matplotlib. Дальше разберём два интерфейса: pyplot и объектно-ориентированный, и почему второй надёжнее.

Проверьте себя
1. Что из перечисленного является контейнером верхнего уровня (весь холст)?
AAxes
BAxis
CFigure
DArtist
2. Почему ax.set_title() предпочтительнее plt.title() при нескольких графиках?
AОн быстрее
BОн ставит заголовок конкретной Axes, а не «текущей»
Cplt.title() не существует
DОн рисует крупнее