2025, Oct 17 12:16

Заполненная слоёная x–y‑диаграмма в Jupyter: рисуем прямоугольники по min/max вместо линий

Пошагово: слоёная x–y‑визуализация в Jupyter/Matplotlib без лишней заливки. Группируем по PVR Group и рисуем прямоугольники по min_x, max_x, min_y, max_y.

Создать заполненную, «слоёную» x–y‑визуализацию в Jupyter непросто, если исходить из линий и пытаться закрашивать пространство между ними. Когда категории нужно складывать по полю “PVR Group”, а геометрия задана минимальными и максимальными границами каждого прямоугольника, есть более прямой путь: рисовать сами прямоугольники. Ниже — короткий разбор: начинаем с линейного подхода и плавно переходим к решению на столбцах, которое сразу даёт нужную заливку.

Постановка задачи

В данных для каждой категории уже указаны явные границы прямоугольника: min_x, max_x — по горизонтали и min_y, max_y — по вертикали. Первая идея — превратить их в контуры, нарисовать линии по группам, а затем попытаться заливать промежутки между соседними контурами для каждой категории.

import pandas as pd
import matplotlib.pyplot as plt
# Пример: группировка и построение контурных серий по категориям
frame_xy = df_xy_columns.groupby('PVR Group')
xs_map = {}
ys_map = {}
for grp_name, grp_df in frame_xy:
    xs = grp_df[["min_x", "min_x", "max_x", "max_x"]].values.flatten()
    xs_map[grp_name] = pd.Series(xs, name=f"x_vals_{grp_name}")
    ys = grp_df[["min_y", "max_y", "max_y", "min_y"]].values.flatten()
    ys_map[grp_name] = pd.Series(ys, name=f"y_vals_{grp_name}")
plt.xlabel("Cumulative " + x_hdr)
plt.ylabel(y_hdr)
plt.title(y_hdr + " Bookshelf Chart by " + agg_tag)
plt.grid(False)
for key in xs_map:
    plt.plot(xs_map[key], ys_map[key], linestyle='-')

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

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

Каждая строка уже описывает прямоугольник. Восстанавливать многоугольники как линии и затем вычислять области заливки — лишняя работа. Если нужен сложенный, заполненный по категориям график, столбцы напрямую соответствуют модели данных: ширина — это max_x − min_x, высота — max_y − min_y, нижняя граница — min_y. Положение по оси x — центр столбца: min_x + (max_x − min_x)/2.

Есть и ещё одна деталь: дублирующиеся столбцы. Если в фрейме встречаются пары вроде min_x и min_x2, или max_y и max_y2, удаление дублей упрощает построение.

Решение: рисовать прямоугольники напрямую

Переход на столбцы избавляет от логики «контур + заливка»: одна функция сразу рисует нужную геометрию. Категории остаются раздельными по группам, а заливка получается автоматически.

import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
# Оставляем только уникальные столбцы, если есть дубликаты
trimmed_df = df.T.drop_duplicates().T
fig_obj, ax_obj = plt.subplots(figsize=(15, 3))
palette = {"1_Low": "blue", "2_Med": "green", "3_High": "red"}
for cat_name, chunk in trimmed_df.groupby("PVR Group"):
    ax_obj.bar(
        (chunk['min_x'] + (chunk['max_x'] - chunk['min_x']) / 2),
        chunk['max_y'] - chunk['min_y'],
        width=(chunk['max_x'] - chunk['min_x']),
        bottom=chunk['min_y'],
        label=cat_name,
        alpha=.5,
        color=palette[cat_name],
        edgecolor=matplotlib.colors.colorConverter.to_rgba(palette[cat_name], alpha=.7)
    )
ax_obj.legend()
ax_obj.set_facecolor('#EBEBEB')
ax_obj.grid(color='white', linewidth=.5, alpha=.5)
for side in list(ax_obj.spines):
    ax_obj.spines[side].set_visible(False)
plt.xlim(xmin=0.0)
plt.show()

Соответствие точное: центр по оси x — min_x плюс половина ширины, высота — вертикальный размах, базовая линия — min_y. В итоге получается заполненный, сложенный по «PVR Group» график без каких‑либо манипуляций с заливкой между линиями. Цвета можно задать явно под нужный дизайн или оставить значения по умолчанию — оба варианта подходят.

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

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

Итоги

Если в DataFrame уже есть минимальные и максимальные границы, строите график по этим примитивам, а не пытайтесь выводить заливки из линейных путей. Сгруппируйте по категориальному полю, вычислите центр, ширину, высоту и нижнюю границу прямо из bounds и рисуйте столбцами. Если во фрейме есть дублирующиеся столбцы, удалите их перед построением, чтобы конвейер оставался чистым. Цвета задавайте явно, когда нужен строгий контроль, или оставляйте значения по умолчанию — и при желании позже предоставьте пользователям выбор палитры. Так код короче, намерение яснее, а визуализация точнее отражает данные.

Статья основана на вопросе на StackOverflow от rfulks и ответе от strawdog.