2025, Dec 10 03:00

Consistent y-scale for vertical profiles in Matplotlib: resize subplot heights, align bottoms, lock y-limits

Keep a consistent y-scale across Matplotlib subplots with different domains by resizing axes with set_position and locking y-limits for cleaner comparisons.

When you compare vertical profiles from multiple simulations, you often want a consistent y-scale so visual differences aren’t caused by rescaling. The catch appears when the domains differ: one run goes up to z=1.4, another to 1.1, a third to 1.0. With a shared y-axis, the shorter profiles leave unused space above their top values, and the panels look unevenly padded. The goal is to keep the same scale across plots while letting each axis stop at its true maximum so the figures become progressively shorter from left to right.

Reproducing the problem

The following snippet sets identical y-scaling and reveals the blank area above the shorter domains.

import matplotlib.pyplot as plt
import numpy as np

canvas, cols = plt.subplots(1, 3, sharey=True)

z_a = np.linspace(0, 1.4, 20)
z_b = np.linspace(0, 1.1, 20)
z_c = np.linspace(0, 1.0, 20)

cols[0].plot(np.exp(-z_a), z_a, c='k')
cols[1].plot(np.exp(-z_b), z_b, c='k')
cols[2].plot(np.exp(-z_c), z_c, c='k')

plt.show()

What’s actually happening

With a shared y-axis you get consistent limits, which preserves scale but forces every subplot to reserve vertical space up to the global maximum. That’s why the second and third panels show empty space above their highest data points. Trying to switch to gridspec or subplots_mosaic won’t help by itself, because they still keep the axes the same relative size unless you explicitly manipulate the layout.

The fix: scale subplot heights to the data range

A practical way forward is to compute the ratio of each profile’s maximum z to the global maximum and then resize each axes’ height by that ratio while keeping their bottoms aligned. This preserves the y-scale and trims each panel at the right place. To eliminate a subtle scaling mismatch at tick levels, set the y-limits explicitly when you resize.

import matplotlib.pyplot as plt
import numpy as np

z_a = np.linspace(0, 1.4, 20)
z_b = np.linspace(0, 1.1, 20)
z_c = np.linspace(0, 1.0, 20)

fig, axarr = plt.subplots(1, 3)

axarr[0].plot(np.exp(-z_a), z_a, c='k')
axarr[1].plot(np.exp(-z_b), z_b, c='k')
axarr[2].plot(np.exp(-z_c), z_c, c='k')

pos_cache = [ax.get_position() for ax in axarr]

max_z_each = [np.max(z_a), np.max(z_b), np.max(z_c)]
max_z_all = max(max_z_each)
height_scale = [m / max_z_all for m in max_z_each]

for axis, bbox, s in zip(axarr, pos_cache, height_scale):
    new_h = (bbox.y1 - bbox.y0) * s
    bottom = bbox.y0
    axis.set_position([bbox.x0, bottom, bbox.width, new_h])
    axis.set_ylim(0, max_z_all * s)

plt.show()

If you want a bit more horizontal spacing between the columns, shrink the width in the set_position call, for example by using 0.8 * bbox.width. Be aware that calling plt.subplots_adjust or plt.tight_layout afterwards will force all axes back to the same height. An alternative direction is to work with inset_axes, which can be adapted for a similar effect.

Why this matters

Equal y-scale without wasted space makes side-by-side comparisons more honest and easier to read. Readers won’t be distracted by large patches of empty area, and identical tick spacing across panels keeps the visual baseline consistent. This is especially important when differences between simulations are subtle and you need the viewer’s attention on shape and magnitude rather than layout artifacts.

Takeaways

When multiple panels share the same physical scale but the underlying domains differ, trim the height of each axes according to its own maximum while leaving the bottoms aligned. Do the resizing via set_position and lock the y-limits to avoid even slight tick misalignment. Adjust spacing by modifying the axes width directly if needed, and avoid global layout functions that would reset your carefully tuned heights. The result is a compact, accurate, and comparable set of plots that tells the story without visual clutter.