Skip to content

Fix #21409: Make twin axes inherit parent position#31353

Open
maf2310 wants to merge 4 commits intomatplotlib:mainfrom
maf2310:fix-twin-position
Open

Fix #21409: Make twin axes inherit parent position#31353
maf2310 wants to merge 4 commits intomatplotlib:mainfrom
maf2310:fix-twin-position

Conversation

@maf2310
Copy link

@maf2310 maf2310 commented Mar 23, 2026

Closes #21409

PR summary

When set_position() is called before twinx() or twiny(), the twin
axes are created using the original subplot position instead of the
current position of the parent axes.

This causes the twin axes to be misaligned with the parent.

This change ensures that the twin axes inherit the current position of
the parent at creation time by calling:

twin.set_position(self.get_position())

This is a minimal fix that avoids changing the subplot or layout model
and instead enforces consistency at twin creation time.

A regression test was added for both twinx() and twiny(). It checks
that the parent and twin axes have matching bbox.bounds after calling
set_position() before creating the twin.

Minimum example

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.set_position([0.2, 0.2, 0.5, 0.5])
ax2 = ax.twinx()
plt.show()

AI Disclosure

AI assistance was used to help understand parts of the existing
Matplotlib codebase during investigation. The final patch and tests were
implemented and validated manually.

PR checklist

When set_position() is called before twinx() or twiny(),
the twin axes are created using the original subplot
position instead of the current position of the parent.

Set the twin position from the parent in _make_twin_axes
so that twins start aligned with the parent axes.

Add a regression test covering both twinx() and twiny().
@github-actions
Copy link

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks. We also ask that you please finish addressing any review comments on this PR and wait for it to be merged (or closed) before opening a new one, as it can be a valuable learning experience to go through the review process.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide.
Please let us know if (and how) you use AI, it will help us give you better feedback on your PR.

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@scottshambaugh scottshambaugh added the status: autoclose candidate PRs that are not yet ready for review and may be automatically closed in two weeks label Mar 23, 2026
@github-actions
Copy link

⏰ This pull request might be automatically closed in two weeks from now.

Thank you for your contribution to Matplotlib and for the effort you have put into this PR. This pull request does not yet meet the quality and clarity standards needed for an effective review. Project maintainers have limited time for code reviews, and our goal is to prioritize well-prepared contributions to keep Matplotlib maintainable.

Matplotlib maintainers cannot provide one-to-one guidance on this PR. However, if you ask focused, well-researched questions, a community member may be willing to help. 💬

To increase the chance of a productive review:

As the author, you are responsible for driving this PR, which entails doing necessary background research as well as presenting its context and your thought process. If you are a new contributor, or do not know how to fulfill these requirements, we recommend that you familiarize yourself with Matplotlib's development conventions or engage with the community via our Discourse or one of our meetings before submitting code.

If you substantially improve this PR within two weeks, leave a comment and a team member may remove the status: autoclose candidate label and the PR stays open. Cosmetic changes or incomplete fixes will not be sufficient. Maintainers will assess improvements on their own schedule. Please do not ping (@) maintainers.

@scottshambaugh
Copy link
Contributor

scottshambaugh commented Mar 23, 2026

Per your comment in #21409, please test this with tight_layout as well. Thanks!

@maf2310
Copy link
Author

maf2310 commented Mar 23, 2026

Thanks! I tested this with tight_layout().

Without tight_layout(), the original issue is fixed: when set_position() is
called before twinx() / twiny(), the twin axes inherit the parent position
and stay aligned.

With tight_layout(), both axes are still aligned with each other, but the
manual position set via set_position() is overridden by the layout engine.
Their positions remain equal, but they are moved together to the layout-managed
position.

So this minimal fix addresses the original issue at twin creation time, but it
does not preserve a manual set_position() across a later tight_layout()
call. Please let me know whether that behavior is acceptable for this issue, or
whether the expected behavior is to preserve the manual position even after
tight_layout().

@scottshambaugh
Copy link
Contributor

Could you please share some screenshots that show what's going on?

@maf2310
Copy link
Author

maf2310 commented Mar 23, 2026

Yes, of course

  • Before the fix: The twin axes are misaligned.
  • After the fix: The twin axes are correctly aligned, inheriting the position of the parent.
  • With tight_layout(): The axes remain aligned, but the manual set_position() is overridden by the layout engine. The positions are moved, but stay aligned.

Here are the screenshots showing the behavior before and after the fix, as well as with tight_layout().

before_fix
after_fix
tight_layout

Please let me know if this behavior is acceptable or if we need to explore further.

@sanrishi
Copy link

Thanks for the patch and test! One concern: twin.set_position(...) calls set_in_layout(False), which opts all twins out of layout even for normal subplots. That’s a behavior change beyond the bug.

Could we either gate this to only run when the parent is manually positioned (not self.get_in_layout()), or use _set_position(...) so layout participation doesn’t change? That would keep the fix focused on the reported issue without altering layout behavior.

@maf2310
Copy link
Author

maf2310 commented Mar 24, 2026

Thanks, that’s a very helpful point — I hadn’t considered the impact on
set_in_layout.

You're right that using twin.set_position(...) unintentionally opts the
twin out of layout, which goes beyond the original issue.

I’ll update the patch to avoid that behavior change. My plan is to either:

  • switch to _set_position(...) so layout participation is preserved, or
  • gate the change to only apply when the parent is manually positioned
    (not self.get_in_layout()).

I’ll test both approaches (including with tight_layout)

@maf2310
Copy link
Author

maf2310 commented Mar 24, 2026

I’ve updated the patch based on your suggestion.

The fix is now limited to the manual-positioning case
(not self.get_in_layout()), and it uses _set_position(...)
instead of set_position(...) so it doesn’t affect layout participation.

I reran the relevant tests (including the original reproduction case and
tight_layout()), and the parent and twin axes remain aligned while
preserving the existing layout behavior.

Comment on lines +4660 to +4661
if not self.get_in_layout():
twin.set_position(self.get_position())

Choose a reason for hiding this comment

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

Thanks for the update the not self.get_in_layout() guard looks right and keeps the fix scoped to manual positioning. One small concern: twin.set_position(self.get_position()) sets both original and active positions. If the parent only changed the active position (set_position(..., which="active")), this would also overwrite the twin’s original.

Should we consider _set_position(..., which="active") or explicitly using get_position(original=True) depending on intended semantics?

Comment on lines +480 to +487
@pytest.mark.parametrize("twin", ("x", "y"))
def test_twin_respects_position_after_set_position(twin):
fig, ax = plt.subplots()

ax.set_position([0.2, 0.2, 0.5, 0.5])
ax2 = getattr(ax, f"twin{twin}")()

assert_allclose(ax.bbox.bounds, ax2.bbox.bounds)

Choose a reason for hiding this comment

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

Also, the new test compares ax.bbox.bounds without forcing a draw; that can be backend‑sensitive. Would you consider comparing ax.get_position().bounds instead, or calling fig.canvas.draw() before comparing bboxes?

@sanrishi
Copy link

sanrishi commented Mar 24, 2026

@maf2310 Linting check is failing consider fixing it also!

@maf2310
Copy link
Author

maf2310 commented Mar 25, 2026

Thanks for the detailed feedback!

I updated the patch so that it now:

  • only applies to the manual-positioning case (not self.get_in_layout())
  • uses _set_position(...) instead of set_position(...)
  • preserves both original and active positions
  • updates the test to use get_position().bounds instead of bbox.bounds

I reran the relevant local tests and the original reproduction case.

The remaining CI failures appear to be in unrelated areas:

  • interactive backend tests (test_webagg, test_qt_missing, etc.)
  • documentation build (docs-python3, missing requirements)

There are no failures in test_axes.py or in the new regression test added by this PR.

Copy link

@sanrishi sanrishi left a comment

Choose a reason for hiding this comment

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

Looks good! The _set_position + get_position().bounds updates are exactly what I was hoping for. This should resolve the issue now!

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Overall, I suspect the need for distinguishing in-layout is an indicator of a deeper architectual issue, that we need to work around here. Therefore, while this fixes the concrete issue, I'm not convinced that it's the right solution.

@maf2310
Copy link
Author

maf2310 commented Mar 25, 2026

Thanks, that makes sense.

I see your point about this possibly being a deeper issue.

For now I tried to keep the fix focused on the specific case from the
issue, where a manually positioned axes isn’t respected when creating a
twin.

Happy to adjust if you think there’s a better approach.

@timhoffm
Copy link
Member

timhoffm commented Mar 26, 2026

I don't know whether there is a better approach. It might be that we are forced to this "workaround" because of the current architecture of the library (layout takes responsibility of position management, and we have to do this manually in the absence of layout - could be a valid argument). But I would like this to be explicitly investigated and discussed. In particular, I'd like to have @jklymak's feedback here as he is our layout expert and the author of #22817.

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

Labels

status: autoclose candidate PRs that are not yet ready for review and may be automatically closed in two weeks topic: axes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: twinx and twiny ignores previous set_position

4 participants