2025, Oct 17 12:00

Stacked X–Y Visualization in Jupyter with Matplotlib: Render Filled Rectangles by PVR Group from min/max Bounds

Learn how to build a filled, stacked x–y chart in Jupyter with Matplotlib by drawing rectangles from min/max bounds, grouped by PVR Group, for accurate visuals.

Building a filled, stacked x–y visualization in Jupyter can go sideways if you start from line plots and try to shade between layers. When categories must be stacked by the “PVR Group” field and the actual geometry is defined by min/max bounds per rectangle, there’s a simpler path: draw the rectangles directly. Below is a concise walkthrough that starts from a line-based approach and lands on a bar-based solution that produces the intended filled result.

Problem setup

The data carries explicit rectangle bounds for each category: min_x, max_x for horizontal limits and min_y, max_y for vertical limits. The initial idea was to convert these into outline paths, plot them as lines per group, and then try to fill between successive outlines by category.

import pandas as pd
import matplotlib.pyplot as plt
# Example: grouping and building per-category outline series
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='-')

This produces category-wise outlines, but leaves the key requirement open: the filled area between stacked layers. The missing piece is a reliable way to refer to the prior layer when shading, without resorting to manual naming.

What’s really going on

Each row already describes a rectangle. Reconstructing polygons as lines and then trying to infer fill regions is redundant. When the desired output is a stacked, filled chart by category, bars map to the data model more directly: width equals max_x − min_x, height equals max_y − min_y, and the lower boundary is min_y. The x position is the bar’s center, so it becomes min_x + (max_x − min_x)/2.

There’s another subtlety in the source: duplicate columns. When a frame includes pairs like min_x and min_x2, or max_y and max_y2, pruning duplicates streamlines plotting.

Solution: render rectangles directly

Switching to bars replaces outline-and-fill logic with a single call that draws the correct geometry. Categories remain separate by group, and the rendering is filled by design.

import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
# Keep only unique columns if duplicates exist
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()

The mapping is exact: the bar’s center along x is min_x plus half the width, the height is the vertical span, and the baseline is min_y. The result is a filled, stacked chart per “PVR Group” without dealing with inter-line shading at all. Color choices can be explicit for a target design or left to defaults; either approach aligns with the goal.

Why this matters

When geometry is encoded as bounds, working with rectangle primitives avoids the overhead and complexity of outline-to-fill conversions. It also scales naturally with any number of categories and stays faithful to the underlying data. By leaning on bar rendering, you get stacking and fill for free, and preserve clear grouping semantics.

Takeaways

If your DataFrame already carries min/max boundaries, build the chart from those primitives instead of reverse-engineering fills from line paths. Group by the categorical field, compute center, width, height, and bottom directly from the bounds, and render with bars. If the frame includes duplicate columns, remove them before plotting to keep the pipeline clean. Set colors explicitly when you need tight visual control, or rely on defaults and expose color choices to users later. This keeps the code shorter, the intent clearer, and the visualization accurate to the data.

The article is based on a question from StackOverflow by rfulks and an answer by strawdog.