Skip to content

Commit c1b51aa

Browse files
ZachDaChampionQuLogic
authored andcommitted
Allow passing a transformation to secondary_[xy]axis
Add transform argument to secondary axes Update _secax_docstring Move new params to end of functions Add input check to secondary axes Add tests Add examples Move transform type checks and improve docs Add type stubs Update _secax_docstring Move new params to end of functions Add input check to secondary axes Move transform type checks and improve docs Fix rebase error Fix stub for SecondaryAxis.__init__ Clarify example Add default param to secax constructor Fix stub for secax constructor Simplify imports Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> Remove redundancy in docs Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> Fix typo
1 parent 88d64e5 commit c1b51aa

File tree

7 files changed

+103
-21
lines changed

7 files changed

+103
-21
lines changed

galleries/examples/subplots_axes_and_figures/secondary_axis.py

+19
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ def rad2deg(x):
4040
secax.set_xlabel('angle [rad]')
4141
plt.show()
4242

43+
# %%
44+
# By default, the secondary axis is drawn in the Axes coordinate space.
45+
# We can also provide a custom transform to place it in a different
46+
# coordinate space. Here we put the axis at Y = 0 in data coordinates.
47+
48+
fig, ax = plt.subplots(layout='constrained')
49+
x = np.arange(0, 10)
50+
np.random.seed(19680801)
51+
y = np.random.randn(len(x))
52+
ax.plot(x, y)
53+
ax.set_xlabel('X')
54+
ax.set_ylabel('Y')
55+
ax.set_title('Random data')
56+
57+
# Pass ax.transData as a transform to place the axis relative to our data
58+
secax = ax.secondary_xaxis(0, transform=ax.transData)
59+
secax.set_xlabel('Axis at Y = 0')
60+
plt.show()
61+
4362
# %%
4463
# Here is the case of converting from wavenumber to wavelength in a
4564
# log-log scale.

lib/matplotlib/axes/_axes.py

+38-14
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def indicate_inset_zoom(self, inset_ax, **kwargs):
551551
return self.indicate_inset(rect, inset_ax, **kwargs)
552552

553553
@_docstring.dedent_interpd
554-
def secondary_xaxis(self, location, *, functions=None, **kwargs):
554+
def secondary_xaxis(self, location, *, functions=None, transform=None, **kwargs):
555555
"""
556556
Add a second x-axis to this `~.axes.Axes`.
557557
@@ -582,18 +582,30 @@ def invert(x):
582582
secax = ax.secondary_xaxis('top', functions=(invert, invert))
583583
secax.set_xlabel('Period [s]')
584584
plt.show()
585+
586+
To add a secondary axis relative to your data, you can pass a transform
587+
to the new axis.
588+
589+
.. plot::
590+
591+
fig, ax = plt.subplots()
592+
ax.plot(range(0, 5), range(-1, 4))
593+
594+
# Pass 'ax.transData' as a transform to place the axis
595+
# relative to your data at y=0
596+
secax = ax.secondary_xaxis(0, transform=ax.transData)
585597
"""
586-
if location in ['top', 'bottom'] or isinstance(location, Real):
587-
secondary_ax = SecondaryAxis(self, 'x', location, functions,
588-
**kwargs)
589-
self.add_child_axes(secondary_ax)
590-
return secondary_ax
591-
else:
598+
if not (location in ['top', 'bottom'] or isinstance(location, Real)):
592599
raise ValueError('secondary_xaxis location must be either '
593600
'a float or "top"/"bottom"')
594601

602+
secondary_ax = SecondaryAxis(self, 'x', location, functions,
603+
transform, **kwargs)
604+
self.add_child_axes(secondary_ax)
605+
return secondary_ax
606+
595607
@_docstring.dedent_interpd
596-
def secondary_yaxis(self, location, *, functions=None, **kwargs):
608+
def secondary_yaxis(self, location, *, functions=None, transform=None, **kwargs):
597609
"""
598610
Add a second y-axis to this `~.axes.Axes`.
599611
@@ -614,16 +626,28 @@ def secondary_yaxis(self, location, *, functions=None, **kwargs):
614626
secax = ax.secondary_yaxis('right', functions=(np.deg2rad,
615627
np.rad2deg))
616628
secax.set_ylabel('radians')
629+
630+
To add a secondary axis relative to your data, you can pass a transform
631+
to the new axis.
632+
633+
.. plot::
634+
635+
fig, ax = plt.subplots()
636+
ax.plot(range(0, 5), range(-1, 4))
637+
638+
# Pass 'ax.transData' as a transform to place the axis
639+
# relative to your data at x=3
640+
secax = ax.secondary_yaxis(3, transform=ax.transData)
617641
"""
618-
if location in ['left', 'right'] or isinstance(location, Real):
619-
secondary_ax = SecondaryAxis(self, 'y', location,
620-
functions, **kwargs)
621-
self.add_child_axes(secondary_ax)
622-
return secondary_ax
623-
else:
642+
if not (location in ['left', 'right'] or isinstance(location, Real)):
624643
raise ValueError('secondary_yaxis location must be either '
625644
'a float or "left"/"right"')
626645

646+
secondary_ax = SecondaryAxis(self, 'y', location, functions,
647+
transform, **kwargs)
648+
self.add_child_axes(secondary_ax)
649+
return secondary_ax
650+
627651
@_docstring.dedent_interpd
628652
def text(self, x, y, s, fontdict=None, **kwargs):
629653
"""

lib/matplotlib/axes/_axes.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Axes(_AxesBase):
9494
]
9595
| Transform
9696
| None = ...,
97+
transform: Transform | None = ...,
9798
**kwargs
9899
) -> SecondaryAxis: ...
99100
def secondary_yaxis(
@@ -105,6 +106,7 @@ class Axes(_AxesBase):
105106
]
106107
| Transform
107108
| None = ...,
109+
transform: Transform | None = ...,
108110
**kwargs
109111
) -> SecondaryAxis: ...
110112
def text(

lib/matplotlib/axes/_secondary_axes.py

+37-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import numpy as np
44

5-
from matplotlib import _api, _docstring
5+
from matplotlib import _api, _docstring, transforms
66
import matplotlib.ticker as mticker
77
from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator
88
from matplotlib.axis import Axis
@@ -14,7 +14,8 @@ class SecondaryAxis(_AxesBase):
1414
General class to hold a Secondary_X/Yaxis.
1515
"""
1616

17-
def __init__(self, parent, orientation, location, functions, **kwargs):
17+
def __init__(self, parent, orientation, location, functions, transform=None,
18+
**kwargs):
1819
"""
1920
See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
2021
While there is no need for this to be private, it should really be
@@ -39,7 +40,7 @@ def __init__(self, parent, orientation, location, functions, **kwargs):
3940
self._parentscale = None
4041
# this gets positioned w/o constrained_layout so exclude:
4142

42-
self.set_location(location)
43+
self.set_location(location, transform)
4344
self.set_functions(functions)
4445

4546
# styling:
@@ -74,7 +75,7 @@ def set_alignment(self, align):
7475
self._axis.set_ticks_position(align)
7576
self._axis.set_label_position(align)
7677

77-
def set_location(self, location):
78+
def set_location(self, location, transform=None):
7879
"""
7980
Set the vertical or horizontal location of the axes in
8081
parent-normalized coordinates.
@@ -87,8 +88,17 @@ def set_location(self, location):
8788
orientation='y'. A float indicates the relative position on the
8889
parent Axes to put the new Axes, 0.0 being the bottom (or left)
8990
and 1.0 being the top (or right).
91+
92+
transform : `.Transform`, optional
93+
Transform for the location to use. Defaults to
94+
the parent's ``transAxes``, so locations are normally relative to
95+
the parent axes.
96+
97+
.. versionadded:: 3.9
9098
"""
9199

100+
_api.check_isinstance((transforms.Transform, None), transform=transform)
101+
92102
# This puts the rectangle into figure-relative coordinates.
93103
if isinstance(location, str):
94104
_api.check_in_list(self._locstrings, location=location)
@@ -106,15 +116,28 @@ def set_location(self, location):
106116
# An x-secondary axes is like an inset axes from x = 0 to x = 1 and
107117
# from y = pos to y = pos + eps, in the parent's transAxes coords.
108118
bounds = [0, self._pos, 1., 1e-10]
119+
120+
# If a transformation is provided, use its y component rather than
121+
# the parent's transAxes. This can be used to place axes in the data
122+
# coords, for instance.
123+
if transform is not None:
124+
transform = transforms.blended_transform_factory(
125+
self._parent.transAxes, transform)
109126
else: # 'y'
110127
bounds = [self._pos, 0, 1e-10, 1]
128+
if transform is not None:
129+
transform = transforms.blended_transform_factory(
130+
transform, self._parent.transAxes) # Use provided x axis
131+
132+
# If no transform is provided, use the parent's transAxes
133+
if transform is None:
134+
transform = self._parent.transAxes
111135

112136
# this locator lets the axes move in the parent axes coordinates.
113137
# so it never needs to know where the parent is explicitly in
114138
# figure coordinates.
115139
# it gets called in ax.apply_aspect() (of all places)
116-
self.set_axes_locator(
117-
_TransformedBoundsLocator(bounds, self._parent.transAxes))
140+
self.set_axes_locator(_TransformedBoundsLocator(bounds, transform))
118141

119142
def apply_aspect(self, position=None):
120143
# docstring inherited.
@@ -278,6 +301,14 @@ def set_color(self, color):
278301
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
279302
for examples of making these conversions.
280303
304+
transform : `.Transform`, optional
305+
If specified, *location* will be
306+
placed relative to this transform (in the direction of the axis)
307+
rather than the parent's axis. i.e. a secondary x-axis will
308+
use the provided y transform and the x transform of the parent.
309+
310+
.. versionadded:: 3.9
311+
281312
Returns
282313
-------
283314
ax : axes._secondary_axes.SecondaryAxis

lib/matplotlib/axes/_secondary_axes.pyi

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ class SecondaryAxis(_AxesBase):
1818
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
1919
]
2020
| Transform,
21+
transform: Transform | None = ...,
2122
**kwargs
2223
) -> None: ...
2324
def set_alignment(
2425
self, align: Literal["top", "bottom", "right", "left"]
2526
) -> None: ...
2627
def set_location(
27-
self, location: Literal["top", "bottom", "right", "left"] | float
28+
self,
29+
location: Literal["top", "bottom", "right", "left"] | float,
30+
transform: Transform | None = ...
2831
) -> None: ...
2932
def set_ticks(
3033
self,

lib/matplotlib/tests/test_axes.py

+3
Original file line numberDiff line numberDiff line change
@@ -7528,6 +7528,7 @@ def invert(x):
75287528
secax(0.6, functions=(lambda x: x**2, lambda x: x**(1/2)))
75297529
secax(0.8)
75307530
secax("top" if nn == 0 else "right", functions=_Translation(2))
7531+
secax(6.25, transform=ax.transData)
75317532

75327533

75337534
def test_secondary_fail():
@@ -7539,6 +7540,8 @@ def test_secondary_fail():
75397540
ax.secondary_xaxis('right')
75407541
with pytest.raises(ValueError):
75417542
ax.secondary_yaxis('bottom')
7543+
with pytest.raises(TypeError):
7544+
ax.secondary_xaxis(0.2, transform='error')
75427545

75437546

75447547
def test_secondary_resize():

0 commit comments

Comments
 (0)