Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4abd6e2

Browse files
committed
BUG: Skip removed colorbar axes in constrained layout (fixes #31330)
When cbar.ax.remove() is called directly (bypassing Colorbar.remove()), the parent axes' _colorbars list still holds a reference to the removed colorbar Axes. On the next draw(), constrained layout crashes in make_layout_margins() and reposition_axes() with: AttributeError: 'NoneType' object has no attribute 'transSubfigure' because get_pos_and_bbox() calls ax.get_figure(root=False) which returns None for a removed Axes. Fix: guard both _colorbars iteration loops against removed axes by checking get_figure(root=False) is None and continuing. Adds regression test.
1 parent e9637a6 commit 4abd6e2

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

lib/matplotlib/_constrained_layout.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
398398
# make margin for colorbars. These margins go in the
399399
# padding margin, versus the margin for Axes decorators.
400400
for cbax in ax._colorbars:
401+
# Skip colorbar axes that have been removed from the figure
402+
# (e.g. via ``cbar.ax.remove()`` instead of ``cbar.remove()``).
403+
if cbax.get_figure(root=False) is None:
404+
continue
401405
# note pad is a fraction of the parent width...
402406
pad = colorbar_get_pad(layoutgrids, cbax)
403407
# colorbars can be child of more than one subplot spec:
@@ -688,6 +692,8 @@ def reposition_axes(layoutgrids, fig, renderer, *,
688692
# one colorbar:
689693
offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0}
690694
for nn, cbax in enumerate(ax._colorbars[::-1]):
695+
if cbax.get_figure(root=False) is None:
696+
continue # colorbar axes was removed from the figure
691697
if ax == cbax._colorbar_info['parents'][0]:
692698
reposition_colorbar(layoutgrids, cbax, renderer,
693699
offset=offset, compress=compress)

lib/matplotlib/tests/test_constrainedlayout.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,3 +809,24 @@ def test_submerged_subfig():
809809
for ax in axs[1:]:
810810
assert np.allclose(ax.get_position().bounds[-1],
811811
axs[0].get_position().bounds[-1], atol=1e-6)
812+
813+
814+
def test_colorbar_remove_axes_no_crash():
815+
"""
816+
Regression test for GH #31330.
817+
818+
Directly removing the colorbar Axes (``cbar.ax.remove()``) should not
819+
crash when the figure is subsequently drawn with constrained layout.
820+
"""
821+
import numpy as np
822+
x = y = np.linspace(-1, 1, 20)
823+
X, Y = np.meshgrid(x, y)
824+
Z = X**2 - Y**2
825+
826+
# cbar.ax.remove() (direct axes removal) should not raise
827+
fig, ax = plt.subplots(layout='constrained')
828+
CS = ax.contourf(X, Y, Z, cmap='bone')
829+
cbar = fig.colorbar(CS)
830+
cbar.ax.remove()
831+
# Drawing must not crash with AttributeError on transSubfigure
832+
fig.draw_without_rendering()

0 commit comments

Comments
 (0)