Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit df9e346

Browse filesBrowse files
authored
Merge pull request #11859 from jklymak/enh-secondary-axes
ENH: add secondary x/y axis
2 parents 6b68d86 + 85c1b99 commit df9e346
Copy full SHA for df9e346

File tree

Expand file treeCollapse file tree

14 files changed

+845
-24
lines changed
Filter options
Expand file treeCollapse file tree

14 files changed

+845
-24
lines changed

‎.flake8

Copy file name to clipboardExpand all lines: .flake8
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ per-file-ignores =
224224
examples/subplots_axes_and_figures/axes_zoom_effect.py: E402
225225
examples/subplots_axes_and_figures/demo_constrained_layout.py: E402
226226
examples/subplots_axes_and_figures/demo_tight_layout.py: E402
227+
examples/subplots_axes_and_figures/secondary_axis.py: E402
227228
examples/subplots_axes_and_figures/two_scales.py: E402
228229
examples/subplots_axes_and_figures/zoom_inset_axes.py: E402
229230
examples/tests/backend_driver_sgskip.py: E402, E501

‎doc/api/axes_api.rst

Copy file name to clipboardExpand all lines: doc/api/axes_api.rst
+2Lines 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
+16Lines 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))
+164Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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`. This secondary axis can have a different scale
10+
than the main axis by providing both a forward and an inverse conversion
11+
function in a tuple to the ``functions`` kwarg:
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+
30+
31+
def deg2rad(x):
32+
return x * np.pi / 180
33+
34+
35+
def rad2deg(x):
36+
return x * 180 / np.pi
37+
38+
secax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg))
39+
secax.set_xlabel('angle [rad]')
40+
plt.show()
41+
42+
###########################################################################
43+
# Here is the case of converting from wavenumber to wavelength in a
44+
# log-log scale.
45+
#
46+
# .. note ::
47+
#
48+
# In this case, the xscale of the parent is logarithmic, so the child is
49+
# made logarithmic as well.
50+
51+
fig, ax = plt.subplots(constrained_layout=True)
52+
x = np.arange(0.02, 1, 0.02)
53+
np.random.seed(19680801)
54+
y = np.random.randn(len(x)) ** 2
55+
ax.loglog(x, y)
56+
ax.set_xlabel('f [Hz]')
57+
ax.set_ylabel('PSD')
58+
ax.set_title('Random spectrum')
59+
60+
61+
def forward(x):
62+
return 1 / x
63+
64+
65+
def inverse(x):
66+
return 1 / x
67+
68+
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
69+
secax.set_xlabel('period [s]')
70+
plt.show()
71+
72+
###########################################################################
73+
# Sometime we want to relate the axes in a transform that is ad-hoc from
74+
# the data, and is derived empirically. In that case we can set the
75+
# forward and inverse transforms functions to be linear interpolations from the
76+
# one data set to the other.
77+
78+
fig, ax = plt.subplots(constrained_layout=True)
79+
xdata = np.arange(1, 11, 0.4)
80+
ydata = np.random.randn(len(xdata))
81+
ax.plot(xdata, ydata, label='Plotted data')
82+
83+
xold = np.arange(0, 11, 0.2)
84+
# fake data set relating x co-ordinate to another data-derived co-ordinate.
85+
# xnew must be monotonic, so we sort...
86+
xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3)
87+
88+
ax.plot(xold[3:], xnew[3:], label='Transform data')
89+
ax.set_xlabel('X [m]')
90+
ax.legend()
91+
92+
93+
def forward(x):
94+
return np.interp(x, xold, xnew)
95+
96+
97+
def inverse(x):
98+
return np.interp(x, xnew, xold)
99+
100+
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
101+
secax.xaxis.set_minor_locator(AutoMinorLocator())
102+
secax.set_xlabel('$X_{other}$')
103+
104+
plt.show()
105+
106+
###########################################################################
107+
# A final example translates np.datetime64 to yearday on the x axis and
108+
# from Celsius to Farenheit on the y axis:
109+
110+
111+
dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6)
112+
for k in range(240)]
113+
temperature = np.random.randn(len(dates))
114+
fig, ax = plt.subplots(constrained_layout=True)
115+
116+
ax.plot(dates, temperature)
117+
ax.set_ylabel(r'$T\ [^oC]$')
118+
plt.xticks(rotation=70)
119+
120+
121+
def date2yday(x):
122+
"""
123+
x is in matplotlib datenums, so they are floats.
124+
"""
125+
y = x - mdates.date2num(datetime.datetime(2018, 1, 1))
126+
return y
127+
128+
129+
def yday2date(x):
130+
"""
131+
return a matplotlib datenum (x is days since start of year)
132+
"""
133+
y = x + mdates.date2num(datetime.datetime(2018, 1, 1))
134+
return y
135+
136+
secaxx = ax.secondary_xaxis('top', functions=(date2yday, yday2date))
137+
secaxx.set_xlabel('yday [2018]')
138+
139+
140+
def CtoF(x):
141+
return x * 1.8 + 32
142+
143+
144+
def FtoC(x):
145+
return (x - 32) / 1.8
146+
147+
secaxy = ax.secondary_yaxis('right', functions=(CtoF, FtoC))
148+
secaxy.set_ylabel(r'$T\ [^oF]$')
149+
150+
plt.show()
151+
152+
#############################################################################
153+
#
154+
# ------------
155+
#
156+
# References
157+
# """"""""""
158+
#
159+
# The use of the following functions and methods is shown in this example:
160+
161+
import matplotlib
162+
163+
matplotlib.axes.Axes.secondary_xaxis
164+
matplotlib.axes.Axes.secondary_yaxis

‎lib/matplotlib/_constrained_layout.py

Copy file name to clipboardExpand all lines: lib/matplotlib/_constrained_layout.py
+9-2Lines 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.width) and np.isfinite(bbox.height)):
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

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+74Lines changed: 74 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,79 @@ 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+
The main axis shows frequency, and the secondary axis shows period.
617+
618+
.. plot::
619+
620+
fig, ax = plt.subplots()
621+
ax.loglog(range(1, 360, 5), range(1, 360, 5))
622+
ax.set_xlabel('frequency [Hz]')
623+
624+
625+
def invert(x):
626+
return 1 / x
627+
628+
secax = ax.secondary_xaxis('top', functions=(invert, invert))
629+
secax.set_xlabel('Period [s]')
630+
plt.show()
631+
632+
633+
"""
634+
if (location in ['top', 'bottom'] or isinstance(location, Number)):
635+
secondary_ax = SecondaryAxis(self, 'x', location, functions,
636+
**kwargs)
637+
self.add_child_axes(secondary_ax)
638+
return secondary_ax
639+
else:
640+
raise ValueError('secondary_xaxis location must be either '
641+
'a float or "top"/"bottom"')
642+
643+
def secondary_yaxis(self, location, *, functions=None, **kwargs):
644+
"""
645+
Add a second y-axis to this axes.
646+
647+
For example if we want to have a second scale for the data plotted on
648+
the yaxis.
649+
650+
%(_secax_docstring)s
651+
652+
Examples
653+
--------
654+
655+
Add a secondary axes that converts from radians to degrees
656+
657+
.. plot::
658+
659+
fig, ax = plt.subplots()
660+
ax.plot(range(1, 360, 5), range(1, 360, 5))
661+
ax.set_ylabel('degrees')
662+
secax = ax.secondary_yaxis('right', functions=(np.deg2rad,
663+
np.rad2deg))
664+
secax.set_ylabel('radians')
665+
666+
"""
667+
if location in ['left', 'right'] or isinstance(location, Number):
668+
secondary_ax = SecondaryAxis(self, 'y', location,
669+
functions, **kwargs)
670+
self.add_child_axes(secondary_ax)
671+
return secondary_ax
672+
else:
673+
raise ValueError('secondary_yaxis location must be either '
674+
'a float or "left"/"right"')
675+
602676
def text(self, x, y, s, fontdict=None, withdash=False, **kwargs):
603677
"""
604678
Add text to the axes.

‎lib/matplotlib/axes/_base.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.py
+16-2Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.