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 1e7f060

Browse filesBrowse files
committed
ENH: add secondary x/y axis
1 parent 136d986 commit 1e7f060
Copy full SHA for 1e7f060

File tree

Expand file treeCollapse file tree

14 files changed

+861
-22
lines changed
Filter options
Expand file treeCollapse file tree

14 files changed

+861
-22
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
@@ -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

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))
+179Lines 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

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
+75Lines 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

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.