Skip to content

Fix Axes3D.get_tightbbox to include axis labels when not for_layout_only#31571

Open
Scolliq wants to merge 1 commit into
matplotlib:mainfrom
Scolliq:fix/axes3d-tightbbox-labels
Open

Fix Axes3D.get_tightbbox to include axis labels when not for_layout_only#31571
Scolliq wants to merge 1 commit into
matplotlib:mainfrom
Scolliq:fix/axes3d-tightbbox-labels

Conversation

@Scolliq
Copy link
Copy Markdown

@Scolliq Scolliq commented Apr 25, 2026

PR summary

Fixes a bug where axis labels (xlabel, ylabel, zlabel) on 3D axes are clipped when saving a figure with bbox_inches='tight'.

Root cause: Axes3D.get_tightbbox unconditionally called martist._get_tightbbox_for_layout_only for every 3D axis, which always passes for_layout_only=True internally and therefore explicitly excludes axis labels from the returned bounding box. This meant the tight-save path never saw the labels, so they were cropped.

Fix: Branch on the for_layout_only argument:

  • True (layout engine): keep using _get_tightbbox_for_layout_only so layout calculations are unaffected.
  • False (savefig tight path): call axis.get_tightbbox(renderer, for_layout_only=False) directly so labels are included.

Minimum reproducer:

import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z Label')
ax.scatter([1, 2, 3], [1, 2, 3], [1, 2, 3])
fig.savefig('out.png', bbox_inches='tight')  # labels were clipped before this fix

Output with this branch (bbox_inches='tight') — all labels fully visible:

axes3d_fixed

Closes #28117.

AI Disclosure

I used Claude Code (claude-sonnet-4-6) to help locate the relevant code path (Axes3D.get_tightbbox_get_tightbbox_for_layout_only) and to draft the fix. I reviewed the logic myself — tracing how for_layout_only propagates through get_tightbbox and confirming that the layout engine path should remain unchanged — before committing.

PR checklist

  • "closes [Bug]: The zlabel on 3D axes will be cut when using '%matplotlib inline' in Jupyter #28117" is in the body of the PR description to link the related issue
  • new and changed code is tested (test_axes3d_tightbbox_includes_axis_labels added)
  • [N/A] Plotting related features are demonstrated in an example (this is a bug fix, not a new feature)
  • [N/A] New Features and API Changes are noted with a directive and release note (bug fix only)
  • [N/A] Documentation complies with general and docstring guidelines (no new public API added)

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 25, 2026

Hello @Scolliq, I see you opened two PRs at the same time. We ask that new contributors not open a second PR until the first is closed
https://matplotlib.org/devdocs/devel/contribute.html#first-contributions

Please could you decide which of your PRs you would like to move forward with and close the other for now.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 25, 2026

Since your PRs look AI driven, please also familiarise yourself with our policy on that
https://matplotlib.org/devdocs/devel/contribute.html#use-of-generative-ai

@Scolliq
Copy link
Copy Markdown
Author

Scolliq commented Apr 25, 2026

Apologies did not know about the one per person for the initial. The other has been closed.

@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 26, 2026

Please update the PR summary using our template. Please also post the image that you get from running the example code from the issue with your branch.

Comment thread doc/axes3d_fixed.png Outdated
Comment thread lib/mpl_toolkits/mplot3d/axes3d.py Outdated
# Include axis labels when computing the full tight
# bbox (e.g. savefig(bbox_inches='tight')).
axis_bb = axis.get_tightbbox(
renderer, for_layout_only=False)
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’m not sure I understand the need for the if-loop here. If we are confident that axis.get_tightbbox takes the for_layout_only keyword, can’t we just pass for_layout_only=for_layout_only in both cases? If we are not confident that axis.get_tightbbox takes the for_layout_only keyword, then we should not pass it in the second case. Note that for_layout_only=False is the default for Matplotlib artists where it is implemented.

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.

Sorry, I actually think this should go the other way. _get_tightbbox_for_layout_only would originally have been used in case axis.get_tightbbox does not take for_layout_only. Obviously the axises on the Axes3D class do take it, but we guard against the possibility that a user or downstream package makes a subclass with axises that don't take it. So the if-loop was correct, but for_layout_only=False should not be passed in the else branch.

Comment thread lib/mpl_toolkits/mplot3d/tests/test_axes3d.py Outdated
@Scolliq
Copy link
Copy Markdown
Author

Scolliq commented Apr 26, 2026

Thanks for the review, that's a much cleaner approach, I overcomplicated it with the if/else when just passing \ through directly does the same thing in one line. Will fix

…_only

Axes3D.get_tightbbox was calling _get_tightbbox_for_layout_only for
every 3D axis unconditionally, which always excludes axis labels.
Fix by passing for_layout_only through to axis.get_tightbbox directly,
so the tight-save path (for_layout_only=False) includes labels while
the layout engine path (for_layout_only=True) is unchanged.

Fixes matplotlib#28117.
@rcomer
Copy link
Copy Markdown
Member

rcomer commented May 15, 2026

Hi @Scolliq are you still interested in working on this?

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