Skip to content

BUG: Refresh 3D axis projection before tightbbox to fix constrained layout overlap (GH#31277)#31548

Open
tinezivic wants to merge 2 commits into
matplotlib:mainfrom
tinezivic:fix/3d-tightbbox-constrained-layout-31277
Open

BUG: Refresh 3D axis projection before tightbbox to fix constrained layout overlap (GH#31277)#31548
tinezivic wants to merge 2 commits into
matplotlib:mainfrom
tinezivic:fix/3d-tightbbox-constrained-layout-31277

Conversation

@tinezivic
Copy link
Copy Markdown
Contributor

@tinezivic tinezivic commented Apr 22, 2026

Closes #31277

constrained_layout calls Axis3D.get_tightbbox() before any draw(), so tick-label 2D positions are not initialized yet and the returned bbox is too small. That leads to overlap between the 3D axis tick labels and the title of the subplot below.

The fix adds a one-time no-output draw() inside get_tightbbox(), guarded by _tightbbox_initialized, and only when constrained_layout is active. If needed, axes.M/axes.invM are initialized first.

This keeps behavior unchanged for non-constrained layouts.

Before / After:

Before (bug) After (fix)
before_fix_31277 after_fix_31277

Minimal reproducer:

fig = plt.figure(constrained_layout=True)
ax1 = fig.add_subplot(2, 1, 1, projection="3d")
ax1.set_title("3D Plot")
ax2 = fig.add_subplot(2, 1, 2)
ax2.set_title("2D Plot")
plt.show()

test_stem3d baseline is updated because it uses constrained_layout=True, so this fix changes the rendered output (tick labels are no longer clipped).

I used GitHub Copilot to help explore the code path; all changes were reviewed and tested manually.

@tinezivic tinezivic force-pushed the fix/3d-tightbbox-constrained-layout-31277 branch from 4c805f7 to b8ee493 Compare April 22, 2026 22:36
@scottshambaugh
Copy link
Copy Markdown
Contributor

scottshambaugh commented Apr 23, 2026

The testing failures are real and require the baseline images to be regenerated, but I think this is a pretty clean fix that solves things correctly.

A note: your PR description and follow-on comment appear AI-generated. Please do not do this, except for direct translation if English is not your first language. There are things in it which do not match the code (apply_aspect is not called), and does it not raise confidence that you understand the changes you are making.

@tinezivic tinezivic force-pushed the fix/3d-tightbbox-constrained-layout-31277 branch from b8ee493 to 5959833 Compare April 23, 2026 05:22
@tinezivic
Copy link
Copy Markdown
Contributor Author

Thanks — that's fair feedback.

You're right that the description and follow-up comment were not precise and contained a leftover reference to apply_aspect(), which is not in the code. I've rewritten the description to match the actual implementation and removed the self-review comment.

I've also narrowed the fix: draw() is now only called when constrained_layout is active, guarded by a per-axis flag _tightbbox_initialized so it runs at most once per axis. This avoids the side effect that was causing test_axes3d_primary_views to fail.

test_stem3d uses constrained_layout=True, so its baseline changes as expected — 3D tick labels are now fully visible instead of being clipped. I've updated that baseline accordingly.

@scottshambaugh
Copy link
Copy Markdown
Contributor

scottshambaugh commented Apr 23, 2026

Does it make more sense to put all this new logic inside the constrained layout check?

We should be avoiding code that can have side effects, what was the issue with test_axes3d_primary_views? If this was changing things because it was called twice, that makes me think we have something changing state that shouldn't be.

Also, please comment on the extent of AI usage to write your comments here.

@tinezivic tinezivic force-pushed the fix/3d-tightbbox-constrained-layout-31277 branch from 5959833 to b55e1e1 Compare April 23, 2026 22:47
@tinezivic
Copy link
Copy Markdown
Contributor Author

tinezivic commented Apr 23, 2026

In the fix the "if" block is now in the constrained layout check as you pointed out, it makes more sense as it is needed only for draw() call.

test_axes3d_primary_views issue is failing locally in my virtual environment even without my code so it is not regression from my code.
(edit: the root cause of test_axes3d_primary_views failing locally: it is caused by vcs-versioning==1.1.1, which introduced a UserWarning that matplotlib's test suite treats as an error. Downgrading to vcs-versioning==1.1.0 or removing vcs-versioning fixes it. CI passes because vcs-versioning is not part of matplotlib's dependencies.)

$ git checkout fix/3d-tightbbox-constrained-layout-31277
$ python -m pytest lib/mpl_toolkits/mplot3d/tests/test_axes3d.py::test_axes3d_primary_views --tb=line -q

F [100%]
=================================== FAILURES ===================================
E UserWarning: No GlobalOverrides context is active. Auto-creating one with SETUPTOOLS_SCM prefix for backwards compatibility.
FAILED test_axes3d_primary_views[png] - UserWarning: No GlobalOverrides context is active...
1 failed in 0.86s

_tightbbox_initialized flag ensures that block runs only once per axis, so draw() is not called on every get_tightbbox() call.

AI (Copilot) was also used for my comments. I have read and understood the problem that it solves.
In future I will write comments myself.

…ayout 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(), if constrained_layout is
active and this is the first tightbbox call for this axis, compute the
projection matrix (M / invM) and call self.draw() inside
renderer._draw_disabled() to update the tick-label positions without
producing any visible output. A per-axis flag (_tightbbox_initialized)
ensures the dry-run happens only once per layout pass.

Two regression tests are added:
- test_axis_get_tightbbox_before_draw_includes_ticklabels: the bboxes
  returned before any draw() must be non-degenerate for all three axes.
- 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.

Baseline image update (stem3d.png):
test_stem3d uses constrained_layout=True with a 2x3 grid of 3D subplots,
which puts it directly on the fixed code path. Before this fix the layout
engine received degenerate bboxes for all six 3D axes, so it did not
reserve space for the tick labels; several labels were clipped at the
subplot edges. After the fix the layout engine reserves the correct space,
the subplots are slightly smaller, and all tick labels are fully visible.
The baseline image has been regenerated to reflect the corrected layout.
@tinezivic tinezivic force-pushed the fix/3d-tightbbox-constrained-layout-31277 branch from b55e1e1 to 3cb927a Compare April 27, 2026 19:35
@tinezivic
Copy link
Copy Markdown
Contributor Author

tinezivic commented May 2, 2026

I just wanted to comment those 3 CI failings are not coused by this PR.

Thank you for your review and if there are any issues that should be addressed, I am happy to re-evaluate and fix them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: 3D ax1's tick labels and ax2's title overlap in a constrained layout

3 participants