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

Skip to content

Commit 4c805f7

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 4c805f7

2 files changed

Lines changed: 60 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+
# Ensure the axes aspect and projection matrix are up to date so that
691+
# the tick-label positions computed below match what draw() will use.
692+
locator = self.axes.get_axes_locator()
693+
self.axes.apply_aspect(
694+
locator(self.axes, renderer) if locator else None)
695+
self.axes.M = self.axes.get_proj()
696+
self.axes.invM = np.linalg.inv(self.axes.M)
697+
698+
# The 2D positions of 3D ticks and labels are updated during draw()
699+
# based on the current projection. Refresh that state without
700+
# rendering so the bbox query sees the same geometry as the real draw.
701+
with renderer._draw_disabled():
702+
self.draw(renderer)
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

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,49 @@ 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_matches_after_draw():
2784+
# Regression test for GH#31277.
2785+
# Axis3D.get_tightbbox() must refresh projected tick positions so that the
2786+
# bbox it returns is the same whether or not draw() has already been called.
2787+
fig = plt.figure()
2788+
ax = fig.add_subplot(projection='3d')
2789+
2790+
axes = [ax.xaxis, ax.yaxis, ax.zaxis]
2791+
2792+
renderer = fig.canvas.get_renderer()
2793+
bboxes_before = {
2794+
axis.axis_name: axis.get_tightbbox(renderer).extents.copy()
2795+
for axis in axes
2796+
}
2797+
2798+
fig.canvas.draw()
2799+
renderer = fig.canvas.get_renderer()
2800+
for axis in axes:
2801+
np.testing.assert_allclose(
2802+
bboxes_before[axis.axis_name],
2803+
axis.get_tightbbox(renderer).extents,
2804+
atol=1,
2805+
err_msg=f"{axis.axis_name}: tightbbox before draw differs from after draw",
2806+
)
2807+
2808+
2809+
def test_constrained_layout_3d_no_overlap_with_subplot_title():
2810+
# Regression test for GH#31277.
2811+
# When using constrained_layout, 3D tick labels must not overlap the title
2812+
# of an adjacent 2D subplot.
2813+
fig = plt.figure(constrained_layout=True)
2814+
ax1 = fig.add_subplot(2, 1, 1, projection='3d')
2815+
ax1.set_title('3D Plot')
2816+
ax2 = fig.add_subplot(2, 1, 2)
2817+
ax2.set_title('2D Plot')
2818+
2819+
fig.canvas.draw()
2820+
renderer = fig.canvas.get_renderer()
2821+
2822+
assert ax1.get_tightbbox(renderer).y0 >= ax2.title.get_window_extent(renderer).y1, \
2823+
"3D subplot tick labels overlap the title of the 2D subplot below"
2824+
2825+
27832826
def test_ctrl_rotation_snaps_to_5deg():
27842827
fig = plt.figure()
27852828
ax = fig.add_subplot(projection='3d')

0 commit comments

Comments
 (0)