Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6cfe2ca

Browse files
committed
ENH: add secondary x/y axis
1 parent 922dea2 commit 6cfe2ca

File tree

14 files changed

+860
-22
lines changed

14 files changed

+860
-22
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ per-file-ignores =
228228
examples/subplots_axes_and_figures/axes_zoom_effect.py: E402
229229
examples/subplots_axes_and_figures/demo_constrained_layout.py: E402
230230
examples/subplots_axes_and_figures/demo_tight_layout.py: E402
231+
examples/subplots_axes_and_figures/secondary_axis.py: E402
231232
examples/subplots_axes_and_figures/two_scales.py: E402
232233
examples/subplots_axes_and_figures/zoom_inset_axes.py: E402
233234
examples/tests/backend_driver_sgskip.py: E402, E501

doc/api/axes_api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ Text and Annotations
188188
Axes.inset_axes
189189
Axes.indicate_inset
190190
Axes.indicate_inset_zoom
191+
Axes.secondary_xaxis
192+
Axes.secondary_yaxis
191193

192194

193195
Fields
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
:orphan:
2+
3+
Secondary x/y Axis support
4+
--------------------------
5+
6+
A new method provides the ability to add a second axis to an existing
7+
axes via `.Axes.secondary_xaxis` and `.Axes.secondary_yaxis`. See
8+
:doc:`/gallery/subplots_axes_and_figures/secondary_axis` for examples.
9+
10+
.. plot::
11+
12+
import matplotlib.pyplot as plt
13+
14+
fig, ax = plt.subplots(figsize=(5, 3))
15+
ax.plot(range(360))
16+
ax.secondary_xaxis('top', functions=(np.deg2rad, np.rad2deg))
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""
2+
==============
3+
Secondary Axis
4+
==============
5+
6+
Sometimes we want as secondary axis on a plot, for instance to convert
7+
radians to degrees on the same plot. We can do this by making a child
8+
axes with only one axis visible via `.Axes.axes.secondary_xaxis` and
9+
`.Axes.axes.secondary_yaxis`.
10+
11+
If we want to label the top of the axes:
12+
"""
13+
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
import datetime
17+
import matplotlib.dates as mdates
18+
from matplotlib.transforms import Transform
19+
from matplotlib.ticker import (
20+
AutoLocator, AutoMinorLocator)
21+
22+
fig, ax = plt.subplots(constrained_layout=True)
23+
x = np.arange(0, 360, 1)
24+
y = np.sin(2 * x * np.pi / 180)
25+
ax.plot(x, y)
26+
ax.set_xlabel('angle [degrees]')
27+
ax.set_ylabel('signal')
28+
ax.set_title('Sine wave')
29+
secax = ax.secondary_xaxis('top')
30+
plt.show()
31+
32+
###########################################################################
33+
# However, its often useful to label the secondary axis with something
34+
# other than the labels in the main axis. In that case we need to provide
35+
# both a forward and an inverse conversion function in a tuple
36+
# to the ``functions`` kwarg:
37+
38+
fig, ax = plt.subplots(constrained_layout=True)
39+
x = np.arange(0, 360, 1)
40+
y = np.sin(2 * x * np.pi / 180)
41+
ax.plot(x, y)
42+
ax.set_xlabel('angle [degrees]')
43+
ax.set_ylabel('signal')
44+
ax.set_title('Sine wave')
45+
46+
47+
def deg2rad(x):
48+
return x * np.pi / 180
49+
50+
51+
def rad2deg(x):
52+
return x * 180 / np.pi
53+
54+
secax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg))
55+
secax.set_xlabel('angle [rad]')
56+
plt.show()
57+
58+
###########################################################################
59+
# Here is the case of converting from wavenumber to wavelength in a
60+
# log-log scale.
61+
#
62+
# .. note ::
63+
#
64+
# In this case, the xscale of the parent is logarithmic, so the child is
65+
# made logarithmic as well.
66+
67+
fig, ax = plt.subplots(constrained_layout=True)
68+
x = np.arange(0.02, 1, 0.02)
69+
np.random.seed(19680801)
70+
y = np.random.randn(len(x)) ** 2
71+
ax.loglog(x, y)
72+
ax.set_xlabel('f [Hz]')
73+
ax.set_ylabel('PSD')
74+
ax.set_title('Random spectrum')
75+
76+
77+
def forward(x):
78+
return 1 / x
79+
80+
81+
def inverse(x):
82+
return 1 / x
83+
84+
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
85+
secax.set_xlabel('period [s]')
86+
plt.show()
87+
88+
###########################################################################
89+
# Sometime we want to relate the axes in a transform that is ad-hoc from
90+
# the data, and is derived empirically. In that case we can set the
91+
# forward and inverse transforms functions to be linear interpolations from the
92+
# one data set to the other.
93+
94+
fig, ax = plt.subplots(constrained_layout=True)
95+
xdata = np.arange(1, 11, 0.4)
96+
ydata = np.random.randn(len(xdata))
97+
ax.plot(xdata, ydata, label='Plotted data')
98+
99+
xold = np.arange(0, 11, 0.2)
100+
# fake data set relating x co-ordinate to another data-derived co-ordinate.
101+
xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3)
102+
103+
ax.plot(xold[3:], xnew[3:], label='Transform data')
104+
ax.set_xlabel('X [m]')
105+
ax.legend()
106+
107+
108+
def forward(x):
109+
return np.interp(x, xold, xnew)
110+
111+
112+
def inverse(x):
113+
return np.interp(x, xnew, xold)
114+
115+
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
116+
secax.xaxis.set_minor_locator(AutoMinorLocator())
117+
secax.set_xlabel('$X_{other}$')
118+
119+
plt.show()
120+
121+
###########################################################################
122+
# A final example translates np.datetime64 to yearday on the x axis and
123+
# from Celsius to Farenheit on the y axis:
124+
125+
126+
dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6)
127+
for k in range(240)]
128+
temperature = np.random.randn(len(dates))
129+
fig, ax = plt.subplots(constrained_layout=True)
130+
131+
ax.plot(dates, temperature)
132+
ax.set_ylabel(r'$T\ [^oC]$')
133+
plt.xticks(rotation=70)
134+
135+
136+
def date2yday(x):
137+
"""
138+
x is in matplotlib datenums, so they are floats.
139+
"""
140+
y = x - mdates.date2num(datetime.datetime(2018, 1, 1))
141+
return y
142+
143+
144+
def yday2date(x):
145+
"""
146+
return a matplotlib datenum (x is days since start of year)
147+
"""
148+
y = x + mdates.date2num(datetime.datetime(2018, 1, 1))
149+
return y
150+
151+
secaxx = ax.secondary_xaxis('top', functions=(date2yday, yday2date))
152+
secaxx.set_xlabel('yday [2018]')
153+
154+
155+
def CtoF(x):
156+
return x * 1.8 + 32
157+
158+
159+
def FtoC(x):
160+
return (x - 32) / 1.8
161+
162+
secaxy = ax.secondary_yaxis('right', functions=(CtoF, FtoC))
163+
secaxy.set_ylabel(r'$T\ [^oF]$')
164+
165+
plt.show()
166+
167+
#############################################################################
168+
#
169+
# ------------
170+
#
171+
# References
172+
# """"""""""
173+
#
174+
# The use of the following functions and methods is shown in this example:
175+
176+
import matplotlib
177+
178+
matplotlib.axes.Axes.secondary_xaxis
179+
matplotlib.axes.Axes.secondary_yaxis

lib/matplotlib/_constrained_layout.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
180180
sup = fig._suptitle
181181
bbox = invTransFig(sup.get_window_extent(renderer=renderer))
182182
height = bbox.y1 - bbox.y0
183-
sup._layoutbox.edit_height(height+h_pad)
183+
if np.isfinite(height):
184+
sup._layoutbox.edit_height(height+h_pad)
184185

185186
# OK, the above lines up ax._poslayoutbox with ax._layoutbox
186187
# now we need to
@@ -266,10 +267,14 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad):
266267
"""
267268
fig = ax.figure
268269
invTransFig = fig.transFigure.inverted().transform_bbox
269-
270270
pos = ax.get_position(original=True)
271271
tightbbox = ax.get_tightbbox(renderer=renderer)
272272
bbox = invTransFig(tightbbox)
273+
# this can go wrong:
274+
if not np.isfinite(bbox.y0 + bbox.x0 + bbox.y1 + bbox.x1):
275+
# just abort, this is likely a bad set of co-ordinates that
276+
# is transitory...
277+
return
273278
# use stored h_pad if it exists
274279
h_padt = ax._poslayoutbox.h_pad
275280
if h_padt is None:
@@ -287,6 +292,8 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad):
287292
_log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad))
288293
_log.debug('right %f', (bbox.x1 - pos.x1 + w_pad))
289294
_log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt))
295+
_log.debug('bbox.y0 %f', bbox.y0)
296+
_log.debug('pos.y0 %f', pos.y0)
290297
# Sometimes its possible for the solver to collapse
291298
# rather than expand axes, so they all have zero height
292299
# or width. This stops that... It *should* have been

lib/matplotlib/axes/_axes.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import matplotlib.tri as mtri
3535
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
3636
from matplotlib.axes._base import _AxesBase, _process_plot_format
37+
from matplotlib.axes._secondary_axes import SecondaryAxis
3738

3839
_log = logging.getLogger(__name__)
3940

@@ -599,6 +600,80 @@ def indicate_inset_zoom(self, inset_ax, **kwargs):
599600

600601
return rectpatch, connects
601602

603+
@docstring.dedent_interpd
604+
def secondary_xaxis(self, location, *, functions=None, **kwargs):
605+
"""
606+
Add a second x-axis to this axes.
607+
608+
For example if we want to have a second scale for the data plotted on
609+
the xaxis.
610+
611+
%(_secax_docstring)s
612+
613+
Examples
614+
--------
615+
616+
Add a secondary axes that shows both wavelength for the main
617+
axes that shows wavenumber.
618+
619+
.. plot::
620+
621+
fig, ax = plt.subplots()
622+
ax.loglog(range(1, 360, 5), range(1, 360, 5))
623+
ax.set_xlabel('frequency [Hz]')
624+
625+
626+
def invert(x):
627+
return 1 / x
628+
629+
secax = ax.secondary_xaxis('top', functions=(invert, invert))
630+
secax.set_xlabel('Period [s]')
631+
plt.show()
632+
633+
634+
"""
635+
if (location in ['top', 'bottom'] or isinstance(location, Number)):
636+
secondary_ax = SecondaryAxis(self, 'x', location, functions,
637+
**kwargs)
638+
self.add_child_axes(secondary_ax)
639+
return secondary_ax
640+
else:
641+
raise ValueError('secondary_xaxis location must be either '
642+
'a float or "top"/"bottom"')
643+
644+
def secondary_yaxis(self, location, *, functions=None, **kwargs):
645+
"""
646+
Add a second y-axis to this axes.
647+
648+
For example if we want to have a second scale for the data plotted on
649+
the yaxis.
650+
651+
%(_secax_docstring)s
652+
653+
Examples
654+
--------
655+
656+
Add a secondary axes that converts from radians to degrees
657+
658+
.. plot::
659+
660+
fig, ax = plt.subplots()
661+
ax.plot(range(1, 360, 5), range(1, 360, 5))
662+
ax.set_ylabel('degrees')
663+
secax = ax.secondary_yaxis('right', functions=(np.deg2rad,
664+
np.rad2deg))
665+
secax.set_ylabel('radians')
666+
667+
"""
668+
if location in ['left', 'right'] or isinstance(location, Number):
669+
secondary_ax = SecondaryAxis(self, 'y', location,
670+
functions, **kwargs)
671+
self.add_child_axes(secondary_ax)
672+
return secondary_ax
673+
else:
674+
raise ValueError('secondary_yaxis location must be either '
675+
'a float or "left"/"right"')
676+
602677
def text(self, x, y, s, fontdict=None, withdash=False, **kwargs):
603678
"""
604679
Add text to the axes.

lib/matplotlib/axes/_base.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,8 +2500,17 @@ def _update_title_position(self, renderer):
25002500
title.set_position((x, 1.0))
25012501
# need to check all our twins too...
25022502
axs = self._twinned_axes.get_siblings(self)
2503-
2504-
top = 0 # the top of all the axes twinned with this axes...
2503+
# and all the children
2504+
for ax in self.child_axes:
2505+
if ax is not None:
2506+
locator = ax.get_axes_locator()
2507+
if locator:
2508+
pos = locator(self, renderer)
2509+
ax.apply_aspect(pos)
2510+
else:
2511+
ax.apply_aspect()
2512+
axs = axs + [ax]
2513+
top = 0
25052514
for ax in axs:
25062515
try:
25072516
if (ax.xaxis.get_label_position() == 'top'
@@ -2544,6 +2553,8 @@ def draw(self, renderer=None, inframe=False):
25442553

25452554
# prevent triggering call backs during the draw process
25462555
self._stale = True
2556+
2557+
# loop over self and child axes...
25472558
locator = self.get_axes_locator()
25482559
if locator:
25492560
pos = locator(self, renderer)
@@ -4315,6 +4326,9 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
43154326
if bb_yaxis:
43164327
bb.append(bb_yaxis)
43174328

4329+
self._update_title_position(renderer)
4330+
bb.append(self.get_window_extent(renderer))
4331+
43184332
self._update_title_position(renderer)
43194333
if self.title.get_visible():
43204334
bb.append(self.title.get_window_extent(renderer))

0 commit comments

Comments
 (0)