From d9bd094ab9d9a4e31446458f59a6c1f16552fbd7 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 16 Dec 2024 16:08:49 +0100 Subject: [PATCH 1/7] feat: axes class and kwargs for twinx and twiny --- lib/matplotlib/axes/_base.py | 18 ++++++++++++++---- lib/matplotlib/axes/_base.pyi | 6 +++--- lib/matplotlib/tests/test_axes.py | 22 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 108cda04865f..ed1d6d779545 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4596,7 +4596,7 @@ def _make_twin_axes(self, *args, **kwargs): self._twinned_axes.join(self, twin) return twin - def twinx(self): + def twinx(self, **kwargs): """ Create a twin Axes sharing the xaxis. @@ -4606,6 +4606,11 @@ def twinx(self): Axes. To ensure that the tick marks of both y-axes align, see `~matplotlib.ticker.LinearLocator`. + Parameters + ---------- + kwargs : dict + The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. + Returns ------- Axes @@ -4616,7 +4621,7 @@ def twinx(self): For those who are 'picking' artists while using twinx, pick events are only called for the artists in the top-most Axes. """ - ax2 = self._make_twin_axes(sharex=self) + ax2 = self._make_twin_axes(sharex=self, axes_class=type(self), **kwargs) ax2.yaxis.tick_right() ax2.yaxis.set_label_position('right') ax2.yaxis.set_offset_position('right') @@ -4627,7 +4632,7 @@ def twinx(self): ax2.xaxis.units = self.xaxis.units return ax2 - def twiny(self): + def twiny(self, **kwargs): """ Create a twin Axes sharing the yaxis. @@ -4637,6 +4642,11 @@ def twiny(self): To ensure that the tick marks of both x-axes align, see `~matplotlib.ticker.LinearLocator`. + Parameters + ---------- + kwargs : dict + The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. + Returns ------- Axes @@ -4647,7 +4657,7 @@ def twiny(self): For those who are 'picking' artists while using twiny, pick events are only called for the artists in the top-most Axes. """ - ax2 = self._make_twin_axes(sharey=self) + ax2 = self._make_twin_axes(sharey=self, axes_class=type(self), **kwargs) ax2.xaxis.tick_top() ax2.xaxis.set_label_position('top') ax2.set_autoscaley_on(self.get_autoscaley_on()) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index b4926f113564..0b487de49917 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -4,7 +4,6 @@ import datetime from collections.abc import Callable, Iterable, Iterator, Sequence from matplotlib import cbook from matplotlib.artist import Artist -from matplotlib.axes import Axes from matplotlib.axis import XAxis, YAxis, Tick from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent from matplotlib.cbook import CallbackRegistry @@ -29,6 +28,7 @@ import numpy as np from numpy.typing import ArrayLike from typing import Any, Literal, TypeVar, overload from matplotlib.typing import ColorType +from typing_extensions import Self _T = TypeVar("_T", bound=Artist) @@ -385,8 +385,8 @@ class _AxesBase(martist.Artist): bbox_extra_artists: Sequence[Artist] | None = ..., for_layout_only: bool = ... ) -> Bbox | None: ... - def twinx(self) -> Axes: ... - def twiny(self) -> Axes: ... + def twinx(self, **kwargs) -> Self: ... + def twiny(self, **kwargs) -> Self: ... def get_shared_x_axes(self) -> cbook.GrouperView: ... def get_shared_y_axes(self) -> cbook.GrouperView: ... def label_outer(self, remove_inner_ticks: bool = ...) -> None: ... diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 38857e846c55..df9765f7077e 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7532,6 +7532,28 @@ def test_twinx_knows_limits(): assert_array_equal(xtwin.viewLim.intervalx, ax2.viewLim.intervalx) +class SubclassAxes(Axes): + def __init__(self, *args, foo, **kwargs): + super().__init__(*args, **kwargs) + self.foo = foo + + +@pytest.mark.parametrize(("axes_class", "kw0", "kw1"), [ + (Axes, {}, {}), + (SubclassAxes, {"foo": 0}, {"foo": 1}), +]) +def test_twinx_subclass(axes_class, kw0, kw1): + fig = plt.figure() + classed_ax = fig.add_subplot(axes_class=axes_class, **kw0) + for k, v in kw0.items(): + assert getattr(classed_ax, k) == v + + twin = classed_ax.twinx(**kw1) + assert type(twin) is axes_class + for k, v in kw1.items(): + assert getattr(twin, k) == v + + def test_zero_linewidth(): # Check that setting a zero linewidth doesn't error plt.plot([0, 1], [0, 1], ls='--', lw=0) From f83b93d000fdc7a89c5eeed734c58d559964c325 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Tue, 17 Dec 2024 22:14:25 +0100 Subject: [PATCH 2/7] fix: projection or polar --- lib/matplotlib/axes/_base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ed1d6d779545..75bc27aafa02 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4621,7 +4621,9 @@ def twinx(self, **kwargs): For those who are 'picking' artists while using twinx, pick events are only called for the artists in the top-most Axes. """ - ax2 = self._make_twin_axes(sharex=self, axes_class=type(self), **kwargs) + if not {"projection", "polar", "axes_class"}.intersection(kwargs): + kwargs["axes_class"] = type(self) + ax2 = self._make_twin_axes(sharex=self, **kwargs) ax2.yaxis.tick_right() ax2.yaxis.set_label_position('right') ax2.yaxis.set_offset_position('right') @@ -4657,7 +4659,9 @@ def twiny(self, **kwargs): For those who are 'picking' artists while using twiny, pick events are only called for the artists in the top-most Axes. """ - ax2 = self._make_twin_axes(sharey=self, axes_class=type(self), **kwargs) + if not {"projection", "polar", "axes_class"}.intersection(kwargs): + kwargs["axes_class"] = type(self) + ax2 = self._make_twin_axes(sharey=self, **kwargs) ax2.xaxis.tick_top() ax2.xaxis.set_label_position('top') ax2.set_autoscaley_on(self.get_autoscaley_on()) From 230fd0ac2b5cd1f4f4d06d6fb4e15b325289ace1 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 16 Dec 2024 16:08:49 +0100 Subject: [PATCH 3/7] feat: axes class and kwargs for twinx and twiny --- 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 75bc27aafa02..6871fb1d5f0f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4623,7 +4623,7 @@ def twinx(self, **kwargs): """ if not {"projection", "polar", "axes_class"}.intersection(kwargs): kwargs["axes_class"] = type(self) - ax2 = self._make_twin_axes(sharex=self, **kwargs) + ax2 = self._make_twin_axes(sharex=self, axes_class=type(self), **kwargs) ax2.yaxis.tick_right() ax2.yaxis.set_label_position('right') ax2.yaxis.set_offset_position('right') @@ -4661,7 +4661,7 @@ def twiny(self, **kwargs): """ if not {"projection", "polar", "axes_class"}.intersection(kwargs): kwargs["axes_class"] = type(self) - ax2 = self._make_twin_axes(sharey=self, **kwargs) + ax2 = self._make_twin_axes(sharey=self, axes_class=type(self), **kwargs) ax2.xaxis.tick_top() ax2.xaxis.set_label_position('top') ax2.set_autoscaley_on(self.get_autoscaley_on()) From 9a12c2c545c38b5cffc2b75ed76a8c1812137cf1 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sat, 21 Dec 2024 00:12:23 +0100 Subject: [PATCH 4/7] fix(comment): https://github.com/matplotlib/matplotlib/pull/29325#issuecomment-2555976517 --- lib/matplotlib/axes/_base.py | 26 ++++++++++++++++++-------- lib/matplotlib/axes/_base.pyi | 6 +++--- lib/matplotlib/tests/test_axes.py | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6871fb1d5f0f..f83cbff66556 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4596,7 +4596,7 @@ def _make_twin_axes(self, *args, **kwargs): self._twinned_axes.join(self, twin) return twin - def twinx(self, **kwargs): + def twinx(self, axes_class=None, **kwargs): """ Create a twin Axes sharing the xaxis. @@ -4608,6 +4608,11 @@ def twinx(self, **kwargs): Parameters ---------- + axes_class : subclass type of `~.axes.Axes`, optional + The `.axes.Axes` subclass that is instantiated. This parameter + is incompatible with *projection* and *polar*. See + :ref:`axisartist_users-guide-index` for examples. + kwargs : dict The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. @@ -4621,9 +4626,9 @@ def twinx(self, **kwargs): For those who are 'picking' artists while using twinx, pick events are only called for the artists in the top-most Axes. """ - if not {"projection", "polar", "axes_class"}.intersection(kwargs): - kwargs["axes_class"] = type(self) - ax2 = self._make_twin_axes(sharex=self, axes_class=type(self), **kwargs) + if axes_class: + kwargs["axes_class"] = axes_class + ax2 = self._make_twin_axes(sharex=self, **kwargs) ax2.yaxis.tick_right() ax2.yaxis.set_label_position('right') ax2.yaxis.set_offset_position('right') @@ -4634,7 +4639,7 @@ def twinx(self, **kwargs): ax2.xaxis.units = self.xaxis.units return ax2 - def twiny(self, **kwargs): + def twiny(self, axes_class=None, **kwargs): """ Create a twin Axes sharing the yaxis. @@ -4646,6 +4651,11 @@ def twiny(self, **kwargs): Parameters ---------- + axes_class : subclass type of `~.axes.Axes`, optional + The `.axes.Axes` subclass that is instantiated. This parameter + is incompatible with *projection* and *polar*. See + :ref:`axisartist_users-guide-index` for examples. + kwargs : dict The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. @@ -4659,9 +4669,9 @@ def twiny(self, **kwargs): For those who are 'picking' artists while using twiny, pick events are only called for the artists in the top-most Axes. """ - if not {"projection", "polar", "axes_class"}.intersection(kwargs): - kwargs["axes_class"] = type(self) - ax2 = self._make_twin_axes(sharey=self, axes_class=type(self), **kwargs) + if axes_class: + kwargs["axes_class"] = axes_class + ax2 = self._make_twin_axes(sharey=self, **kwargs) ax2.xaxis.tick_top() ax2.xaxis.set_label_position('top') ax2.set_autoscaley_on(self.get_autoscaley_on()) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 0b487de49917..31208ce23d1e 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -4,6 +4,7 @@ import datetime from collections.abc import Callable, Iterable, Iterator, Sequence from matplotlib import cbook from matplotlib.artist import Artist +from matplotlib.axes import Axes from matplotlib.axis import XAxis, YAxis, Tick from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent from matplotlib.cbook import CallbackRegistry @@ -28,7 +29,6 @@ import numpy as np from numpy.typing import ArrayLike from typing import Any, Literal, TypeVar, overload from matplotlib.typing import ColorType -from typing_extensions import Self _T = TypeVar("_T", bound=Artist) @@ -385,8 +385,8 @@ class _AxesBase(martist.Artist): bbox_extra_artists: Sequence[Artist] | None = ..., for_layout_only: bool = ... ) -> Bbox | None: ... - def twinx(self, **kwargs) -> Self: ... - def twiny(self, **kwargs) -> Self: ... + def twinx(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ... + def twiny(self, axes_class: Axes | None = ..., **kwargs) -> Axes: ... def get_shared_x_axes(self) -> cbook.GrouperView: ... def get_shared_y_axes(self) -> cbook.GrouperView: ... def label_outer(self, remove_inner_ticks: bool = ...) -> None: ... diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index df9765f7077e..bfe08082f577 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7548,7 +7548,7 @@ def test_twinx_subclass(axes_class, kw0, kw1): for k, v in kw0.items(): assert getattr(classed_ax, k) == v - twin = classed_ax.twinx(**kw1) + twin = classed_ax.twinx(axes_class=axes_class, **kw1) assert type(twin) is axes_class for k, v in kw1.items(): assert getattr(twin, k) == v From 769b4d3c8a2bab6a660e439b80d2bdee5e1dc47a Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sat, 21 Dec 2024 11:28:27 +0100 Subject: [PATCH 5/7] fix(codecov): coverage --- lib/matplotlib/tests/test_axes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index bfe08082f577..649a7991d546 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7538,17 +7538,18 @@ def __init__(self, *args, foo, **kwargs): self.foo = foo +@pytest.mark.parametrize("twinning", ["twinx", "twiny"]) @pytest.mark.parametrize(("axes_class", "kw0", "kw1"), [ (Axes, {}, {}), (SubclassAxes, {"foo": 0}, {"foo": 1}), ]) -def test_twinx_subclass(axes_class, kw0, kw1): +def test_twinning_subclass(twinning, axes_class, kw0, kw1): fig = plt.figure() classed_ax = fig.add_subplot(axes_class=axes_class, **kw0) for k, v in kw0.items(): assert getattr(classed_ax, k) == v - twin = classed_ax.twinx(axes_class=axes_class, **kw1) + twin = getattr(classed_ax, twinning)(axes_class=axes_class, **kw1) assert type(twin) is axes_class for k, v in kw1.items(): assert getattr(twin, k) == v From 18589f32bf19b7783e59c15a2965357a0dc3ff83 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sat, 21 Dec 2024 23:48:59 +0100 Subject: [PATCH 6/7] Apply suggestions from code review - https://github.com/matplotlib/matplotlib/pull/29325/files#r1894693106 - https://github.com/matplotlib/matplotlib/pull/29325/files#r1894693188 - https://github.com/matplotlib/matplotlib/pull/29325/files#r1894696478 Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axes/_base.py | 4 ++++ lib/matplotlib/tests/test_axes.py | 36 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f83cbff66556..650aab6bea0c 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4613,6 +4613,8 @@ def twinx(self, axes_class=None, **kwargs): is incompatible with *projection* and *polar*. See :ref:`axisartist_users-guide-index` for examples. + By default, `~.axes.Axes` is used. + kwargs : dict The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. @@ -4656,6 +4658,8 @@ def twiny(self, axes_class=None, **kwargs): is incompatible with *projection* and *polar*. See :ref:`axisartist_users-guide-index` for examples. + By default, `~.axes.Axes` is used. + kwargs : dict The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 649a7991d546..ecc37c83b55d 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7538,21 +7538,27 @@ def __init__(self, *args, foo, **kwargs): self.foo = foo -@pytest.mark.parametrize("twinning", ["twinx", "twiny"]) -@pytest.mark.parametrize(("axes_class", "kw0", "kw1"), [ - (Axes, {}, {}), - (SubclassAxes, {"foo": 0}, {"foo": 1}), -]) -def test_twinning_subclass(twinning, axes_class, kw0, kw1): - fig = plt.figure() - classed_ax = fig.add_subplot(axes_class=axes_class, **kw0) - for k, v in kw0.items(): - assert getattr(classed_ax, k) == v - - twin = getattr(classed_ax, twinning)(axes_class=axes_class, **kw1) - assert type(twin) is axes_class - for k, v in kw1.items(): - assert getattr(twin, k) == v +def test_twinning_with_axes_class(): + """Check that twinx/y(axes_class=...) gives the appropriate class.""" + _, ax = plt.subplots() + twinx = ax.twinx(axes_class=SubclassAxes, foo=1) + assert isinstance(twinx, SubclassAxes) + assert twinx.foo == 1 + twiny = ax.twiny(axes_class=SubclassAxes, foo=2) + assert isinstance(twiny, SubclassAxes) + assert twiny.foo == 2 + + +def test_twinning_default_axes_class(): + """ + Check that the default class for twinx/y() is Axes, + even if the original is an Axes subclass. + """ + _, ax = plt.subplots(subplot_kw=dict(axes_class=SubclassAxes, foo=1)) + twinx = ax.twinx() + assert type(twinx) is Axes + twiny = ax.twiny() + assert type(twiny) is Axes def test_zero_linewidth(): From bb8e3742e6fe140b53916fa851ad53be561bdf59 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Wed, 8 Jan 2025 11:31:36 +0100 Subject: [PATCH 7/7] Apply suggestions from code review - https://github.com/matplotlib/matplotlib/pull/29325#discussion_r1903091205 - https://github.com/matplotlib/matplotlib/pull/29325#discussion_r1903107092 - https://github.com/matplotlib/matplotlib/pull/29325#discussion_r1906984234 Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- lib/matplotlib/axes/_base.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 650aab6bea0c..8313d26df190 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4615,8 +4615,12 @@ def twinx(self, axes_class=None, **kwargs): By default, `~.axes.Axes` is used. + .. versionadded:: 3.11 + kwargs : dict - The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. + The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`. + + .. versionadded:: 3.11 Returns ------- @@ -4660,8 +4664,12 @@ def twiny(self, axes_class=None, **kwargs): By default, `~.axes.Axes` is used. + .. versionadded:: 3.11 + kwargs : dict - The keyword arguments passed to ``add_subplot()`` or ``add_axes()``. + The keyword arguments passed to `.Figure.add_subplot` or `.Figure.add_axes`. + + .. versionadded:: 3.11 Returns -------