From d9ef7af11bd65934de92a7cbbfdb8a7716c13b80 Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Mon, 23 Mar 2026 00:03:21 +0000 Subject: [PATCH 1/7] Fix #21409: Make twin axes inherit parent position 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(). --- lib/matplotlib/axes/_base.py | 3 +++ lib/matplotlib/tests/test_axes.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f89c231815dc..1a3d142742ac 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4657,6 +4657,9 @@ def _make_twin_axes(self, *args, **kwargs): twin.set_zorder(self.zorder) self._twinned_axes.join(self, twin) + + twin.set_position(self.get_position()) + return twin def twinx(self, axes_class=None, **kwargs): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 74d48a89d0c0..849b5d3dc27f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -477,6 +477,16 @@ def test_twin_inherit_autoscale_setting(): assert not ax_y_off.get_autoscaley_on() +@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) + + def test_inverted_cla(): # GitHub PR #5450. Setting autoscale should reset # axes to be non-inverted. From 88d4ec56449927e8acbc8925ca84f7bd12225952 Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Tue, 24 Mar 2026 17:23:39 +0000 Subject: [PATCH 2/7] Refine twin position fix to preserve layout participation --- lib/matplotlib/axes/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 1a3d142742ac..b5be77f2bd4b 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4657,8 +4657,8 @@ def _make_twin_axes(self, *args, **kwargs): twin.set_zorder(self.zorder) self._twinned_axes.join(self, twin) - - twin.set_position(self.get_position()) + if not self.get_in_layout(): + twin.set_position(self.get_position()) return twin From a5435b5a09a951b5d644090f502c7caa9cf92d03 Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Wed, 25 Mar 2026 00:26:59 +0000 Subject: [PATCH 3/7] Address review: preserve original/active positions and fix test + lint --- lib/matplotlib/axes/_base.py | 4 +++- lib/matplotlib/tests/test_axes.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b5be77f2bd4b..68fd00a46c7e 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4657,8 +4657,10 @@ def _make_twin_axes(self, *args, **kwargs): twin.set_zorder(self.zorder) self._twinned_axes.join(self, twin) + if not self.get_in_layout(): - twin.set_position(self.get_position()) + twin._set_position(self.get_position(original=True), which="original") + twin._set_position(self.get_position(original=False), which="active") return twin diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 849b5d3dc27f..4d57405f66d7 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -484,7 +484,7 @@ def test_twin_respects_position_after_set_position(twin): 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) + assert_allclose(ax.get_position().bounds, ax2.get_position().bounds) def test_inverted_cla(): From ce749b465cb8b787b49e1f8a9b4031e51f89f318 Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Wed, 25 Mar 2026 20:01:11 +0000 Subject: [PATCH 4/7] Clarify manual-position twin sync in code comment --- lib/matplotlib/axes/_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 68fd00a46c7e..a09e69b378d9 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4658,6 +4658,15 @@ def _make_twin_axes(self, *args, **kwargs): self._twinned_axes.join(self, twin) + # If the parent Axes has been manually positioned (set_position() sets + # in_layout=False), the SubplotSpec-based add_subplot(...) path ignores + # that manual position when creating a twin. In that case, explicitly + # copy both the original and active positions to the twin so they start + # aligned. + # + # For layout-managed Axes (in_layout=True), we keep the existing + # SubplotSpec-driven behavior, so layout engines such as tight_layout + # and constrained_layout continue to control positioning. if not self.get_in_layout(): twin._set_position(self.get_position(original=True), which="original") twin._set_position(self.get_position(original=False), which="active") From 5876d06ffed133aecae705d0fff4dae1f54251ef Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Fri, 27 Mar 2026 22:33:55 +0000 Subject: [PATCH 5/7] Add regression test for twinx top spine position --- lib/matplotlib/tests/test_axes.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 4d57405f66d7..588e5edec7a7 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -484,7 +484,21 @@ def test_twin_respects_position_after_set_position(twin): ax.set_position([0.2, 0.2, 0.5, 0.5]) ax2 = getattr(ax, f"twin{twin}")() - assert_allclose(ax.get_position().bounds, ax2.get_position().bounds) + assert_allclose(ax.get_position(original=True).bounds, + ax2.get_position(original=True).bounds) + + assert_allclose(ax.get_position(original=False).bounds, + ax2.get_position(original=False).bounds) + + +@pytest.mark.parametrize("twin", ("x", "y")) +def test_twin_keeps_layout_participation_for_layout_managed_axes(twin): + fig, ax = plt.subplots() + + ax2 = getattr(ax, f"twin{twin}")() + + assert ax.get_in_layout() + assert ax2.get_in_layout() def test_inverted_cla(): From e8df9e59bd26a9f6c2d068b91430926b72358f2e Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Sun, 29 Mar 2026 18:35:52 +0100 Subject: [PATCH 6/7] Add test to ensure twin axes remain aligned after tight_layout --- lib/matplotlib/tests/test_axes.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 588e5edec7a7..5af6a9000dca 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -501,6 +501,16 @@ def test_twin_keeps_layout_participation_for_layout_managed_axes(twin): assert ax2.get_in_layout() +@pytest.mark.parametrize("twin", ("x", "y")) +def test_twin_stays_aligned_after_tight_layout(twin): + fig,ax = plt.subplots() + ax2 = getattr(ax, f"twin{twin}")() + + fig.tight_layout() + + assert_allclose(ax.get_position().bounds, ax2.get_position().bounds) + + def test_inverted_cla(): # GitHub PR #5450. Setting autoscale should reset # axes to be non-inverted. From c2a343cfdd9a481b978aecfe8138b710aed6daf5 Mon Sep 17 00:00:00 2001 From: Mafalda Botelho Date: Mon, 30 Mar 2026 12:16:01 +0100 Subject: [PATCH 7/7] Add tests for twin axes alignment with manual positioning_layout --- lib/matplotlib/tests/test_axes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 5af6a9000dca..75ab359a27eb 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -502,11 +502,13 @@ def test_twin_keeps_layout_participation_for_layout_managed_axes(twin): @pytest.mark.parametrize("twin", ("x", "y")) -def test_twin_stays_aligned_after_tight_layout(twin): - fig,ax = plt.subplots() +def test_twin_stays_aligned_after_constrained_layout(twin): + fig, ax = plt.subplots(constrained_layout=True) + + ax.set_position([0.2, 0.2, 0.5, 0.5]) ax2 = getattr(ax, f"twin{twin}")() - fig.tight_layout() + fig.canvas.draw() assert_allclose(ax.get_position().bounds, ax2.get_position().bounds)