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

Skip to content

Commit 5959833

Browse files
committed
BUG: Refresh 3D axis projection before tightbbox to fix constrained layout overlap (GH#31277)
Axis3D.get_tightbbox() read stale tick-label (x, y) positions that were last set during the previous draw() call. When constrained_layout queries the bounding boxes before any draw has occurred, the positions are uninitialised, causing the 3D subplot's reported bbox to be too small. Adjacent subplots then overlap the 3D tick labels. Fix: at the start of Axis3D.get_tightbbox(), refresh the projection matrix (M / invM) via apply_aspect() and get_proj(), then call self.draw() inside renderer._draw_disabled() to update the tick-label positions without producing any visible output. The rest of the method already reads those positions correctly. Two regression tests are added: - test_axis_get_tightbbox_before_draw_matches_after_draw: the bbox returned before any draw() must match the one returned after draw(). - test_constrained_layout_3d_no_overlap_with_subplot_title: with a 3D subplot above a 2D subplot in constrained_layout, the 3D tight bbox bottom must not overlap the 2D subplot title.
1 parent e9637a6 commit 5959833

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

lib/mpl_toolkits/mplot3d/axis3d.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,26 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False):
684684
# docstring inherited
685685
if not self.get_visible():
686686
return
687+
if renderer is None:
688+
renderer = self.get_figure(root=True)._get_renderer()
689+
690+
# The 2D positions of 3D ticks and labels are updated during draw()
691+
# based on the current projection matrix M. If M has not been
692+
# computed yet (i.e. before the first real draw), initialize it now.
693+
if self.axes.M is None:
694+
self.axes.M = self.axes.get_proj()
695+
self.axes.invM = np.linalg.inv(self.axes.M)
696+
697+
# For constrained_layout, the layout engine queries tightbbox before
698+
# any draw() call, so tick 2D positions have not been set yet and the
699+
# bbox would be degenerate. Do a no-output draw to initialize them.
700+
# The flag is per-axis so each of xaxis/yaxis/zaxis is initialised once.
701+
if (not getattr(self, '_tightbbox_initialized', False)
702+
and self.get_figure(root=True).get_constrained_layout()):
703+
with renderer._draw_disabled():
704+
self.draw(renderer)
705+
self._tightbbox_initialized = True
706+
687707
# We have to directly access the internal data structures
688708
# (and hope they are up to date) because at draw time we
689709
# shift the ticks and their labels around in (x, y) space
62.3 KB
Loading

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,39 @@ def test_axis_get_tightbbox_includes_offset_text():
27802780
f"bbox.y1 ({bbox.y1}) should be >= offset_bbox.y1 ({offset_bbox.y1})"
27812781

27822782

2783+
def test_axis_get_tightbbox_before_draw_includes_ticklabels():
2784+
# Regression test for GH#31277.
2785+
# Axis3D.get_tightbbox() must return a non-degenerate bbox before draw()
2786+
# has been called when constrained_layout is active, so that the layout
2787+
# engine can account for 3D tick labels.
2788+
fig = plt.figure(constrained_layout=True)
2789+
ax = fig.add_subplot(projection='3d')
2790+
2791+
renderer = fig.canvas.get_renderer()
2792+
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
2793+
bbox = axis.get_tightbbox(renderer)
2794+
assert bbox is not None, f"{axis.axis_name}: get_tightbbox returned None"
2795+
assert bbox.width > 0 and bbox.height > 0, \
2796+
f"{axis.axis_name}: tightbbox has zero area before draw()"
2797+
2798+
2799+
def test_constrained_layout_3d_no_overlap_with_subplot_title():
2800+
# Regression test for GH#31277.
2801+
# When using constrained_layout, 3D tick labels must not overlap the title
2802+
# of an adjacent 2D subplot.
2803+
fig = plt.figure(constrained_layout=True)
2804+
ax1 = fig.add_subplot(2, 1, 1, projection='3d')
2805+
ax1.set_title('3D Plot')
2806+
ax2 = fig.add_subplot(2, 1, 2)
2807+
ax2.set_title('2D Plot')
2808+
2809+
fig.canvas.draw()
2810+
renderer = fig.canvas.get_renderer()
2811+
2812+
assert ax1.get_tightbbox(renderer).y0 >= ax2.title.get_window_extent(renderer).y1, \
2813+
"3D subplot tick labels overlap the title of the 2D subplot below"
2814+
2815+
27832816
def test_ctrl_rotation_snaps_to_5deg():
27842817
fig = plt.figure()
27852818
ax = fig.add_subplot(projection='3d')

0 commit comments

Comments
 (0)