2025, Oct 25 13:00

How to stop y tick label overlap in stacked Matplotlib subplots that share the x-axis

Learn how to prevent overlapping y tick labels in shared x-axis Matplotlib subplots by nudging labels with ScaledTranslation. Keep layout tight and data intact.

When stacking Matplotlib subplots that share the same x-axis, the lowest y tick label of the top axes and the highest y tick label of the bottom axes can draw on top of each other. The overlap ruins readability, and padding with extra hspace or altering data limits is not always desirable. The goal is to keep both tick labels and gently nudge them apart.

Reproducing the overlap

The following minimal example builds two subplots with a shared x-axis and zero vertical spacing. The top axes spans −1 to 1, while the bottom one spans −0.01 to 0.01. The y tick labels at the touching boundary collide.

import numpy as np
import matplotlib.pyplot as plt

fig_box, ax_grid = plt.subplots(nrows=2, sharex='col', gridspec_kw={'hspace': 0, 'wspace': 0})
ax_grid[1].set_xlim([0, 10])

ax_grid[0].set_ylim(-1.0, 1.0)
ax_grid[1].set_ylim(-0.01, 0.01)

x_vals = np.linspace(0, 10, 100)

ax_grid[0].plot(x_vals, np.sin(x_vals), color="red")
ax_grid[1].plot(x_vals, 0.01 * np.sin(x_vals), color="blue")

plt.show()

What actually overlaps and why

The conflict is not about the tick positions themselves; it’s about the tick labels—the rendered text at the boundary between the two axes. Because the subplots share x and have no vertical padding, the lowest label of the upper axes and the highest label of the lower axes occupy the same space. Changing hspace or expanding y-limits would avoid the collision, but that also changes the layout or the visible range, which may not be acceptable.

Shift the tick labels, not the ticks

A precise fix is to move just the two problematic labels by a small offset in display units. ScaledTranslation provides a clean way to apply a vertical shift measured in points, keeping the data and layout intact.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import ScaledTranslation

fig_win, ax_set = plt.subplots(nrows=2, sharex='col', gridspec_kw={'hspace': 0, 'wspace': 0})
ax_set[1].set_xlim([0, 10])

ax_set[0].set_ylim(-1.0, 1.0)
ax_set[1].set_ylim(-0.01, 0.01)

x_seq = np.linspace(0, 10, 100)

ax_set[0].plot(x_seq, np.sin(x_seq), color="red")
ax_set[1].plot(x_seq, 0.01 * np.sin(x_seq), color="blue")

# 5 pt vertical offset; 5 pt = 5/72 inch
shift = ScaledTranslation(0, 5/72, fig_win.dpi_scale_trans)

# Move the lowest label on the top axes slightly up
lbl = ax_set[0].yaxis.get_majorticklabels()[0]
lbl.set_transform(lbl.get_transform() + shift)

# Move the highest label on the bottom axes slightly down
lbl = ax_set[1].yaxis.get_majorticklabels()[-1]
lbl.set_transform(lbl.get_transform() - shift)

plt.show()

Why this approach is useful

This technique preserves the data ranges and the tight layout while restoring legibility. It lets you keep both tick labels without moving one set of ticks to the right or inflating subplot spacing. That can be important when the figure needs to stay compact or when a second y axis is planned for one of the panels, where relocating a tick label column would get in the way.

Takeaways

If two y tick labels collide between vertically stacked subplots that share the x-axis, nudge only the labels that clash. A tiny point-based offset via ScaledTranslation is enough to separate them, avoids changing axis limits or hspace, and keeps both labels visible. Apply the upward shift to the lowest label on the upper axes and a downward shift to the highest label on the lower axes, and the plot remains clean and readable.

The article is based on a question from StackOverflow by Toffomat and an answer by simon.