BUG: Skip removed colorbar axes in constrained layout (fixes #31330)#31549
BUG: Skip removed colorbar axes in constrained layout (fixes #31330)#31549tinezivic wants to merge 1 commit intomatplotlib:mainfrom
Conversation
…ib#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.
8d454d1 to
4abd6e2
Compare
|
@jklymak Thanks — agreed that This PR is not trying to change that API or make Since (Also: force-pushed to rebase onto current |
|
IMO the problem is with colorbar and it should not be the layout engine's job to work around it. I have proposed an alternative solution at #31555. |
|
Closing this in favour of #31555 by @rcomer, which fixes the root cause rather than working around it in the layout engine. My approach skipped dead colorbar axes during layout — that prevents the crash, but the stale reference in Thanks for the quick alternative @rcomer! |
PR summary
Fixes #31330.
When the user calls
cbar.ax.remove()directly (instead of the documentedcbar.remove()), the colorbar Axes is detached from the figure but the parent axes'_colorbarslist still holds a reference to it. On the next draw with constrained layout,make_layout_margins()andreposition_axes()iterate overax._colorbarsand callget_pos_and_bbox(cbax, renderer), which internally callscbax.get_figure(root=False)— returningNonefor the removed Axes — and then crashes:Root cause:
Artist.remove()sets_parent_figure = None, soget_figure(root=False)returnsNoneafter removal. The constrained layout code does not guard against this.Fix: Add a
get_figure(root=False) is Noneguard in both colorbar iteration loops (make_layout_marginsandreposition_axes) to skip colorbar axes that have been removed from the figure.Why not remove from
_colorbarsinAxes.remove()?The current
Colorbar.remove()API already handles that cleanup correctly. The crash happens only when users bypass it by callingcbar.ax.remove()directly. A null guard in the layout engine is the appropriate defense-in-depth fix and avoids changing cleanup semantics.AI Disclosure
I used GitHub Copilot as a pair-programming aid to navigate the codebase and assist with writing the fix and test.
PR checklist