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

Skip to content

Commit b55e1e1

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 b55e1e1

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

lib/mpl_toolkits/mplot3d/axis3d.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,23 @@ 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+
# For constrained_layout, the layout engine queries tightbbox before
691+
# any draw() call, so tick 2D positions have not been set yet and the
692+
# bbox would be degenerate. Do a no-output draw to initialize them.
693+
# M must be computed first if it hasn't been set yet.
694+
# The flag is per-axis so each of xaxis/yaxis/zaxis is initialised once.
695+
if (not getattr(self, '_tightbbox_initialized', False)
696+
and self.get_figure(root=True).get_constrained_layout()):
697+
if self.axes.M is None:
698+
self.axes.M = self.axes.get_proj()
699+
self.axes.invM = np.linalg.inv(self.axes.M)
700+
with renderer._draw_disabled():
701+
self.draw(renderer)
702+
self._tightbbox_initialized = True
703+
687704
# We have to directly access the internal data structures
688705
# (and hope they are up to date) because at draw time we
689706
# 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)