2025, Oct 31 19:46

Как выровнять горизонтальные stacked barh в pandas и matplotlib без смещения

Пошаговое решение смещения в горизонтальных диаграммах barh на pandas и matplotlib: единая ось, синхронизация twin-оси, метки, zorder и аккуратная легенда.

Горизонтальные столбчатые диаграммы в pandas и matplotlib кажутся простыми, пока в дело не вступают составные столбцы и вторая ось. Частая ловушка — смещение столбцов, когда один из слоёв рисуется на связанной оси (twin). Несовпадение едва заметно, но оно ломает историю, которую пытается донести график. Ниже — как вернуть выравнивание, не теряя подписей категорий, оставив фоновую полосу позади и сохранив аккуратную легенду.

Как воспроизвести проблему

Следующий фрагмент создаёт простой DataFrame и рисует два слоя горизонтальных столбцов на разных осях. Составные столбцы попадают на дополнительную ось y, а полупрозрачная фоновая полоса — на основную ось y. В итоге столбцы смещаются, а в компоновке появляются артефакты.

import pandas as pd
import matplotlib.pyplot as plt

records = {'Name': ["A", "B", "C", "D", 'E'],
           'Todo': [4, 5, 6, 7, 3],
           'Done': [6, 2, 6, 8, 6],
           'TimeRemaining': [4, 4, 4, 4, 4]}

frame = pd.DataFrame(records)

canvas, left_ax = plt.subplots(figsize=(10, 8))
right_ax = left_ax.twinx()

pick_cols = frame.columns[0:2].to_list()

bar_ax = frame.plot(kind='barh', y=pick_cols, stacked=True, ax=right_ax)
for box in bar_ax.containers:
    labels = [r.get_width() if r.get_width() > 0 else '' for r in box]
    bar_ax.bar_label(box, fmt=lambda val: f'{val:.0f}' if val > 0 else '', label_type='center')

frame.set_index('Name').plot(kind='barh', y=["TimeRemaining"], color='whitesmoke',
                             alpha=0.3, ax=left_ax, align='center', width=0.8,
                             edgecolor='blue')

right_ax.tick_params(axis='y', labelright=False, right=False)
right_ax.set_yticklabels([])
left_ax.get_legend().remove()

plt.title('Status Chart')
plt.tight_layout()
plt.show()

Что на самом деле происходит

Задействованы две разные оси: одна для составных столбцов и одна для полупрозрачной полосы. Каждая ось сама управляет позициями по y. Поскольку столбцы нарисованы на разных осях, центры категорий не совпадают — и слои не выравниваются. Если перенести всё на одну ось, столбцы выстроятся ровно, но вы потеряете левые подписи категорий и получите лишний элемент легенды, а полупрозрачная полоса окажется сверху.

Решение

Поместите оба слоя на одну ось, чтобы центры столбцов совпали, а связанную ось используйте только для отображения левых подписей. Синхронизируйте левую ось y с положениями правой оси, спрячьте подписи делений справа, оставьте только нужную легенду и отправьте фоновую полосу на задний план с помощью zorder. Если на правой оси появляется подпись, очистите её явно.

import pandas as pd
import matplotlib.pyplot as plt

records = {'Name': ["A", "B", "C", "D", 'E'],
           'Todo': [4, 5, 6, 7, 3],
           'Done': [6, 2, 6, 8, 6],
           'TimeRemaining': [4, 4, 4, 4, 4]}

frame = pd.DataFrame(records)

canvas, left_ax = plt.subplots(figsize=(10, 8))
right_ax = left_ax.twinx()

stack_cols = frame.columns[1:3].to_list()

bars_ax = frame.set_index('Name').plot(kind='barh', y=stack_cols, stacked=True, ax=right_ax)
for grp in bars_ax.containers:
    labels = [seg.get_width() if seg.get_width() > 0 else '' for seg in grp]
    bars_ax.bar_label(grp, fmt=lambda v: f'{v:.0f}' if v > 0 else '', label_type='center')

frame.set_index('Name').plot(kind='barh', y=["TimeRemaining"], color='whitesmoke',
                             alpha=0.3, ax=right_ax, align='center', width=0.8,
                             edgecolor='blue', zorder=0)

right_ax.tick_params(axis='y', labelright=False, right=False)
left_ax.tick_params(axis='y', labelleft=True, left=True)

left_ax.set_ylim(right_ax.get_ylim())
left_ax.set_yticks(right_ax.get_yticks())
left_ax.set_yticklabels(frame['Name'])

handles, labels = right_ax.get_legend_handles_labels()
right_ax.legend([handles[0]], [labels[0]], loc='upper right')
right_ax.set_ylabel('')

plt.title('Status Chart')
plt.tight_layout()
plt.show()

Почему это важно

Выравнивание категорий в горизонтальных столбчатых диаграммах легко упустить и трудно «развидеть». Когда слои показывают разные метрики, смещение может подсказать неверные связи. Общая ось для обоих слоёв сохраняет точность и читабельность, особенно когда нужны составные значения, полупрозрачная базовая полоса, центрированные подписи и опрятная легенда.

Итоги

Стройте все слои столбцов на одной оси, чтобы гарантировать выравнивание. Пусть связанная ось служит для левых подписей, отзеркальте пределы и деления по y и держите фоновую полосу позади через z-order. Приведите в порядок легенду и уберите лишнюю подпись правой оси. В результате получится ровная, читабельная горизонтальная диаграмма, которая доносит верную идею без сюрпризов в вёрстке.

Статья основана на вопросе с StackOverflow от moys и ответе Bhargav.