Crowded Matplotlib figures usually fail at the spacing layer rather than the data layer. Long tick labels, subplot titles, axis labels, legends, and colorbars all compete for the same canvas area, so a plot that works as one panel can become cramped when it moves into a report-sized grid.
Matplotlib figures can use layout engines that reserve space for Axes decorations before the figure is drawn. For new or editable figures, layout=“constrained” is the first choice because it handles subplots, colorbars, and nested layouts better than the older tight_layout() adjustment.
Use one layout engine per figure. Calling tight_layout() after enabling constrained layout disables constrained layout, and manual subplots_adjust() values can fight the layout engine when both try to move the same Axes.
Related: How to create subplots in Matplotlib
Related: How to set figure size in Matplotlib
Related: How to format tick labels in Matplotlib
fig, axs = plt.subplots(2, 2, figsize=(8.6, 5.4), layout="constrained")
Set layout=“constrained” before adding Axes content so Matplotlib can reserve room for labels, titles, legends, and colorbars during the draw. For a simple older figure that cannot be changed at creation time, use fig.tight_layout(pad=1.2) before saving instead of mixing both layout systems.
fig.suptitle("Support ticket backlog with readable labels") ax.set_xlabel("Month reviewed") ax.set_ylabel("Tickets pending triage") fig.colorbar(points, ax=axs, label="SLA risk score")
Layout engines measure normal Matplotlib artists. Absolute text positions, manually placed Axes, and annotations outside the figure area may still require a larger figsize or explicit margins.
from pathlib import Path import numpy as np import matplotlib.pyplot as plt output = Path("layout-fix-overlap.png") months = np.arange(1, 7) month_labels = [ "January", "February", "March", "April", "May", "June", ] regions = { "North support queue": [42, 38, 47, 51, 49, 55], "South support queue": [31, 35, 34, 39, 44, 46], "East support queue": [28, 33, 37, 36, 41, 43], "West support queue": [36, 32, 40, 45, 47, 50], } fig, axs = plt.subplots(2, 2, figsize=(8.6, 5.4), layout="constrained") fig.suptitle("Support ticket backlog with readable labels", fontsize=15) last_points = None for index, (ax, (region, tickets)) in enumerate(zip(axs.flat, regions.items())): risk_score = np.linspace(35 + index * 6, 78 + index * 4, len(months)) last_points = ax.scatter( months, tickets, c=risk_score, cmap="viridis", vmin=30, vmax=95, s=72, edgecolor="black", linewidth=0.4, ) ax.plot(months, tickets, color="0.30", linewidth=1.4) ax.set_title(region) ax.set_xlabel("Month reviewed") ax.set_ylabel("Tickets pending triage") ax.set_xticks(months, month_labels, rotation=35, ha="right") ax.grid(True, alpha=0.25) fig.colorbar(last_points, ax=axs, label="SLA risk score") fig.savefig(output, dpi=160) engine = fig.get_layout_engine() print(f"layout engine: {engine.__class__.__name__}") print(f"saved: {output.name}")
$ python layout_fix_overlap.py layout engine: ConstrainedLayoutEngine saved: layout-fix-overlap.png
