Skip to content

FIX: Include axis labels in Axes3D.get_tightbbox() to prevent clipping#31572

Open
buddy0452004 wants to merge 1 commit intomatplotlib:mainfrom
buddy0452004:fix-3d-zlabel-tightbbox
Open

FIX: Include axis labels in Axes3D.get_tightbbox() to prevent clipping#31572
buddy0452004 wants to merge 1 commit intomatplotlib:mainfrom
buddy0452004:fix-3d-zlabel-tightbbox

Conversation

@buddy0452004
Copy link
Copy Markdown
Contributor

@buddy0452004 buddy0452004 commented Apr 25, 2026

Fixes #28117

Previously, axis labels on 3D axes were excluded entirely from the tightbbox when for_layout_only=True. This caused savefig(bbox_inches='tight') to clip these labels in the saved output since bbox_inches='tight' uses get_tightbbox() internally. The inline Jupyter backend (%matplotlib inline) also triggers this path, which is the original report in #28117.

Root cause: In axis3d.py, the label was guarded by not for_layout_only:

if (self.label.get_visible() and not for_layout_only and
        self.label.get_text()):
    other.append(self.label.get_window_extent(renderer))

Fix: Harmonize with 2D axis behavior (axis.py lines 1368–1380) always include the label bbox, but collapse it in the irrelevant direction when for_layout_only=True.

Minimum reproducible example:

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(4, 3))
ax = fig.add_subplot(1, 1, 1, projection='3d')
ax.plot([0, 1], [0, 1], [0, 1])
ax.set_zlabel("Z axis label")
ax.set_xlabel("X axis label")
fig.savefig("out.png", bbox_inches='tight')

Before fix: zlabel and xlabel are clipped in the saved image. After fix: all labels are fully visible.

AI Disclosure

None.

PR checklist

@github-actions github-actions Bot added topic: mplot3d Documentation: examples files in galleries/examples labels Apr 25, 2026
@buddy0452004 buddy0452004 force-pushed the fix-3d-zlabel-tightbbox branch from c0bf5d5 to 464330c Compare April 25, 2026 14:36
@github-actions github-actions Bot removed the Documentation: examples files in galleries/examples label Apr 25, 2026
@buddy0452004
Copy link
Copy Markdown
Contributor Author

The image comparison failure in test_axes3d_primary_views is expected the fix correctly includes axis labels in the tightbbox, which slightly changes the layout when tight_layout() is used with 3D axes that have labels. I will update the baseline image.

Comment thread lib/mpl_toolkits/mplot3d/axis3d.py Outdated
elif self.axis_name == "y":
if bb.height > 0:
bb.y0 = (bb.y0 + bb.y1) / 2 - 0.5
bb.y1 = bb.y0 + 1.0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right idea, but needs generalising. My understanding of the 2D case is that, for the layout engine, we do not want to make extra space in the direction parallel to the axis because if the label is very long you eventually just run out of space on the figure. For 2D the parallel direction is fixed for x and y so the code can explicitly mention width and height. For 3D each of x, y, and z can be in any direction.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 26, 2026

A note on PR priority: usually we prioritise the first PR opened against an issue. #31571 links the same issue and was opened first but only addresses the case when for_layout_only=False. If this one additionally fixes the for_layout_only=True case, then we will want it regardless of whether #31571 gets merged first.

@buddy0452004 buddy0452004 force-pushed the fix-3d-zlabel-tightbbox branch from 464330c to 48ee5b8 Compare April 27, 2026 08:44
@buddy0452004
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. Updated the fix to use self.label.get_rotation() instead of axis_name the bbox is now collapsed based on the actual label orientation (vertical vs horizontal) rather than hardcoding by axis name. This correctly handles all view angles since 3D labels can rotate freely depending on the viewpoint.

@buddy0452004
Copy link
Copy Markdown
Contributor Author

The only remaining failure is the test_axes3d_primary_views image comparison. The layout change is correct labels are now properly included in tightbbox. I'm unable to regenerate the baseline locally due to version differences. Could someone help regenerate it, or is there a CI command to do so?

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 27, 2026

Hi @buddy0452004 I'm unclear what you mean about version differences. The new image gets saved out automatically when you run the tests locally. The failure message will display the location of the result image.

@buddy0452004
Copy link
Copy Markdown
Contributor Author

Hi @rcomer, thanks for the guidance! I've now added the baseline image generated locally using the result image from the test failure. CI is running please let me know if anything else needs to be addressed.

@buddy0452004
Copy link
Copy Markdown
Contributor Author

buddy0452004 commented Apr 27, 2026

Hi @rcomer, I generated the baseline image from the local test result, but it was produced with FreeType 2.13.3 while CI uses 2.6.1, causing image differences. Is there a way to generate a CI-compatible baseline, or can the baseline be regenerated by CI itself?

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 27, 2026

Hmmm, I don't know what to advise about your local test run. You can get the CI-generated image from the Artifacts section at the bottom of he Github Actions summary page
https://github.com/matplotlib/matplotlib/actions/runs/25006548524?pr=31572

However, regardless of the text differences, I don't think tight-layout should be adding quite that much white space. I also can't see anything in your logic that would explain why it adds so much.

@buddy0452004 buddy0452004 force-pushed the fix-3d-zlabel-tightbbox branch from 9e821ba to 1f40cca Compare April 28, 2026 19:41
@buddy0452004
Copy link
Copy Markdown
Contributor Author

Updated the fix to collapse the label bbox based on get_rotation() angle instead of hardcoding by axis name, as suggested. This correctly handles all view angles since 3D labels can rotate freely. Will update the baseline image once CI generates the new result artifact.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 28, 2026

Updated the fix to collapse the label bbox based on get_rotation() angle instead of hardcoding by axis name, as suggested. This correctly handles all view angles since 3D labels can rotate freely. Will update the baseline image once CI generates the new result artifact.

You already had something based on get_rotation() yesterday. Did an LLM write this comment?

@buddy0452004
Copy link
Copy Markdown
Contributor Author

Haha no I was just frustrated trying to sort out the baseline situation and wrote that poorly The rotation based approach was already there from before I just explained it badly in the comment

Previously, axis labels on 3D axes were excluded from the tightbbox
when for_layout_only=True, causing savefig(bbox_inches='tight') and
the inline Jupyter backend to clip them in the output.

Fix: harmonize with 2D axis behavior by always including the label
bbox but collapsing it in the irrelevant direction when for_layout_only
is True. Create a new Bbox object instead of mutating in place to avoid
corrupting the layout engine.

Updated baseline image for test_axes3d_primary_views.
@buddy0452004 buddy0452004 force-pushed the fix-3d-zlabel-tightbbox branch from 030d227 to 2ad5d7e Compare April 29, 2026 09:58
@buddy0452004
Copy link
Copy Markdown
Contributor Author

Sorry for the messy commit history I was going back and forth trying to figure out why the baseline looked so broken. Turns out the real bug was that I was mutating the Bbox object in place, which was corrupting the layout engine and making the axes render tiny. Switched to creating a new Bbox instead and that fixed it. Baseline updated, all 37 CI checks passing now.

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]: The zlabel on 3D axes will be cut when using '%matplotlib inline' in Jupyter

2 participants