Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

Commit6b6237c

Browse files
committed
ENH: add secondary x/y axis
1 parentb9b02f1 commit6b6237c

File tree

11 files changed

+977
-17
lines changed

11 files changed

+977
-17
lines changed

‎doc/api/axes_api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ Text and Annotations
184184
Axes.inset_axes
185185
Axes.indicate_inset
186186
Axes.indicate_inset_zoom
187+
Axes.secondary_xaxis
188+
Axes.secondary_yaxis
187189

188190

189191
Fields
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
"""
12+
13+
importmatplotlib.pyplotasplt
14+
importnumpyasnp
15+
frommatplotlib.transformsimportTransform
16+
frommatplotlib.tickerimport (
17+
AutoLocator,AutoMinorLocator)
18+
19+
fig,ax=plt.subplots(constrained_layout=True)
20+
x=np.arange(0,360,1)
21+
y=np.sin(2*x*np.pi/180)
22+
ax.plot(x,y)
23+
ax.set_xlabel('angle [degrees]')
24+
ax.set_ylabel('signal')
25+
ax.set_title('Sine wave')
26+
27+
secax=ax.secondary_xaxis('top',conversion=[np.pi/180])
28+
secax.set_xlabel('angle [rad]')
29+
plt.show()
30+
31+
###########################################################################
32+
# The conversion can be a linear slope and an offset as a 2-tuple. It can
33+
# also be more complicated. The strings "inverted", "power", and "linear"
34+
# are accepted as valid arguments for the ``conversion`` kwarg, and scaling
35+
# is set by the ``otherargs`` kwarg.
36+
#
37+
# .. note ::
38+
#
39+
# In this case, the xscale of the parent is logarithmic, so the child is
40+
# made logarithmic as well.
41+
42+
fig,ax=plt.subplots(constrained_layout=True)
43+
x=np.arange(0.02,1,0.02)
44+
np.random.seed(19680801)
45+
y=np.random.randn(len(x))**2
46+
ax.loglog(x,y)
47+
ax.set_xlabel('f [Hz]')
48+
ax.set_ylabel('PSD')
49+
ax.set_title('Random spectrum')
50+
51+
secax=ax.secondary_xaxis('top',conversion='inverted',otherargs=1)
52+
secax.set_xlabel('period [s]')
53+
secax.set_xscale('log')
54+
plt.show()
55+
56+
###########################################################################
57+
# Considerably more complicated, the user can define their own transform
58+
# to pass to ``conversion``. Here the conversion is arbitrary linearly
59+
# interpolated between two (sorted) arrays.
60+
61+
fig,ax=plt.subplots(constrained_layout=True)
62+
ax.plot(np.arange(1,11),np.arange(1,11))
63+
64+
65+
classLocalArbitraryInterp(Transform):
66+
"""
67+
Return interpolated from data. Note that both arrays
68+
have to be ascending for this to work in this example. (Could
69+
have more error checking to do more generally)
70+
"""
71+
72+
input_dims=1
73+
output_dims=1
74+
is_separable=True
75+
has_inverse=True
76+
77+
def__init__(self,xold,xnew):
78+
Transform.__init__(self)
79+
self._xold=xold
80+
self._xnew=xnew
81+
82+
deftransform_non_affine(self,values):
83+
q=np.interp(values,self._xold,self._xnew, )
84+
returnq
85+
86+
definverted(self):
87+
""" we are just our own inverse """
88+
returnLocalArbitraryInterp(self._xnew,self._xold)
89+
90+
# this is anarbitrary mapping defined by data. Only issue is that it
91+
# should be one-to-one and the vectors need to be ascending for the inverse
92+
# mapping to work.
93+
xold=np.arange(0,11,0.2)
94+
xnew=np.sort(10*np.exp(-xold/4))
95+
96+
ax.plot(xold[3:],xnew[3:])
97+
ax.set_xlabel('X [m]')
98+
99+
secax=ax.secondary_xaxis('top',conversion=LocalArbitraryInterp(xold,xnew))
100+
secax.xaxis.set_minor_locator(AutoMinorLocator())
101+
secax.set_xlabel('Exponential axes')
102+
103+
plt.show()

‎lib/matplotlib/_constrained_layout.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
181181
sup=fig._suptitle
182182
bbox=invTransFig(sup.get_window_extent(renderer=renderer))
183183
height=bbox.y1-bbox.y0
184-
sup._layoutbox.edit_height(height+h_pad)
184+
ifnp.isfinite(height):
185+
sup._layoutbox.edit_height(height+h_pad)
185186

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

‎lib/matplotlib/axes/_axes.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
safe_first_element)
3939
frommatplotlib.containerimportBarContainer,ErrorbarContainer,StemContainer
4040
frommatplotlib.axes._baseimport_AxesBase,_process_plot_format
41+
frommatplotlib.axes._secondary_axesimportSecondary_Axis
4142

4243
_log=logging.getLogger(__name__)
4344

@@ -639,6 +640,159 @@ def indicate_inset_zoom(self, inset_ax, **kwargs):
639640

640641
returnrectpatch,connects
641642

643+
defsecondary_xaxis(self,location,*,conversion=None,
644+
otherargs=None,**kwargs):
645+
"""
646+
Add a second x-axis to this axes.
647+
648+
For example if we want to have a second scale for the data plotted on
649+
the xaxis.
650+
651+
Warnings
652+
--------
653+
654+
This method is experimental as of 3.1, and the API may change.
655+
656+
Parameters
657+
----------
658+
location : string or scalar
659+
The position to put the secondary axis. Strings can be 'top' or
660+
'bottom', scalar can be a float indicating the relative position
661+
on the axes to put the new axes (0 being the bottom, and 1.0 being
662+
the top.)
663+
664+
conversion : scalar, two-tuple of scalars, string, or Transform
665+
If a scalar or a two-tuple of scalar, the secondary axis is
666+
converted via a linear conversion with slope given by the first
667+
and offset given by the second. i.e. ``conversion = [2, 1]``
668+
element for a parent axis between 0 and 1 gives a secondary axis
669+
between 1 and 3.
670+
671+
If a string, if can be one of "linear", "power", and "inverted".
672+
If "linear", the value of ``otherargs`` should be a float or
673+
two-tuple as above. If "inverted" the values in the secondary axis
674+
are inverted and multiplied by the value supplied by ``oterargs``.
675+
If "power", then the original values are transformed by
676+
``newx = otherargs[1] * oldx ** otherargs[0]``.
677+
678+
Finally, the user can supply a subclass of `.transforms.Transform`
679+
to arbitrarily transform between the parent axes and the
680+
secondary axes.
681+
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
682+
for an example of making such a transform.
683+
684+
685+
Other Parameters
686+
----------------
687+
**kwargs : `~matplotlib.axes.Axes` properties.
688+
Other miscellaneous axes parameters.
689+
690+
Returns
691+
-------
692+
ax : axes._secondary_axes.Secondary_Axis
693+
694+
Examples
695+
--------
696+
697+
Add a secondary axes that shows both wavelength for the main
698+
axes that shows wavenumber.
699+
700+
.. plot::
701+
702+
fig, ax = plt.subplots()
703+
ax.loglog(range(1, 360, 5), range(1, 360, 5))
704+
ax.set_xlabel('wavenumber [cpkm]')
705+
secax = ax.secondary_xaxis('top', conversion='inverted',
706+
otherargs=1.)
707+
secax.set_xlabel('wavelength [km]')
708+
709+
710+
"""
711+
if (locationin ['top','bottom']orisinstance(location,Number)):
712+
secondary_ax=Secondary_Axis(self,'x',location,
713+
conversion,otherargs=otherargs,
714+
**kwargs)
715+
self.add_child_axes(secondary_ax)
716+
returnsecondary_ax
717+
else:
718+
raiseValueError('secondary_xaxis location must be either '
719+
'"top" or "bottom"')
720+
721+
defsecondary_yaxis(self,location,*,conversion=None,
722+
otherargs=None,**kwargs):
723+
"""
724+
Add a second y-axis to this axes.
725+
726+
For example if we want to have a second scale for the data plotted on
727+
the xaxis.
728+
729+
Warnings
730+
--------
731+
732+
This method is experimental as of 3.1, and the API may change.
733+
734+
Parameters
735+
----------
736+
location : string or scalar
737+
The position to put the secondary axis. Strings can be 'left' or
738+
'right', scalar can be a float indicating the relative position
739+
on the axes to put the new axes (0 being the left, and 1.0 being
740+
the right.)
741+
742+
conversion : scalar, two-tuple of scalars, string, or Transform
743+
If a scalar or a two-tuple of scalar, the secondary axis is
744+
converted via a linear conversion with slope given by the first
745+
and offset given by the second. i.e. ``conversion = [2, 1]``
746+
element for a parent axis between 0 and 1 gives a secondary axis
747+
between 1 and 3.
748+
749+
If a string, if can be one of "linear", "power", and "inverted".
750+
If "linear", the value of ``otherargs`` should be a float or
751+
two-tuple as above. If "inverted" the values in the secondary axis
752+
are inverted and multiplied by the value supplied by ``oterargs``.
753+
If "power", then the original values are transformed by
754+
``newy = otherargs[1] * oldy ** otherargs[0]``.
755+
756+
Finally, the user can supply a subclass of `.transforms.Transform`
757+
to arbitrarily transform between the parent axes and the
758+
secondary axes.
759+
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
760+
for an example of making such a transform.
761+
762+
763+
Other Parameters
764+
----------------
765+
**kwargs : `~matplotlib.axes.Axes` properties.
766+
Other miscellaneous axes parameters.
767+
768+
Returns
769+
-------
770+
ax : axes._secondary_axes.Secondary_Axis
771+
772+
Examples
773+
--------
774+
775+
Add a secondary axes that converts from radians to degrees
776+
777+
.. plot::
778+
779+
fig, ax = plt.subplots()
780+
ax.plot(range(1, 360, 5), range(1, 360, 5))
781+
ax.set_ylabel('degrees')
782+
secax = ax.secondary_yaxis('right', conversion=[np.pi / 180])
783+
secax.set_ylabel('radians')
784+
785+
"""
786+
iflocationin ['left','right']orisinstance(location,Number):
787+
secondary_ax=Secondary_Axis(self,'y',location,
788+
conversion,otherargs=otherargs,
789+
**kwargs)
790+
self.add_child_axes(secondary_ax)
791+
returnsecondary_ax
792+
else:
793+
raiseValueError('secondary_yaxis location must be either '
794+
'"left" or "right"')
795+
642796
deftext(self,x,y,s,fontdict=None,withdash=False,**kwargs):
643797
"""
644798
Add text to the axes.

‎lib/matplotlib/axes/_base.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,7 +2511,16 @@ def _update_title_position(self, renderer):
25112511
y=1.0
25122512
# need to check all our twins too...
25132513
axs=self._twinned_axes.get_siblings(self)
2514-
2514+
# and all the children
2515+
foraxinself.child_axes:
2516+
ifaxisnotNone:
2517+
locator=ax.get_axes_locator()
2518+
iflocator:
2519+
pos=locator(self,renderer)
2520+
ax.apply_aspect(pos)
2521+
else:
2522+
ax.apply_aspect()
2523+
axs=axs+ [ax]
25152524
foraxinaxs:
25162525
try:
25172526
if (ax.xaxis.get_label_position()=='top'
@@ -2543,12 +2552,15 @@ def draw(self, renderer=None, inframe=False):
25432552

25442553
# prevent triggering call backs during the draw process
25452554
self._stale=True
2546-
locator=self.get_axes_locator()
2547-
iflocator:
2548-
pos=locator(self,renderer)
2549-
self.apply_aspect(pos)
2550-
else:
2551-
self.apply_aspect()
2555+
2556+
# loop over self and child axes...
2557+
foraxin [self]:
2558+
locator=ax.get_axes_locator()
2559+
iflocator:
2560+
pos=locator(self,renderer)
2561+
ax.apply_aspect(pos)
2562+
else:
2563+
ax.apply_aspect()
25522564

25532565
artists=self.get_children()
25542566
artists.remove(self.patch)
@@ -4199,7 +4211,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41994211
bb_xaxis=self.xaxis.get_tightbbox(renderer)
42004212
ifbb_xaxis:
42014213
bb.append(bb_xaxis)
4202-
42034214
self._update_title_position(renderer)
42044215
bb.append(self.get_window_extent(renderer))
42054216

@@ -4219,9 +4230,11 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
42194230
bbox_artists=self.get_default_bbox_extra_artists()
42204231

42214232
forainbbox_artists:
4222-
bbox=a.get_tightbbox(renderer)
4223-
ifbboxisnotNoneand (bbox.width!=0orbbox.height!=0):
4224-
bb.append(bbox)
4233+
bbox=a.get_tightbbox(renderer, )
4234+
if (bboxisnotNoneand
4235+
(bbox.width!=0orbbox.height!=0)and
4236+
np.isfinite(bbox.x0+bbox.x1+bbox.y0+bbox.y1)):
4237+
bb.append(bbox)
42254238

42264239
_bbox=mtransforms.Bbox.union(
42274240
[bforbinbbifb.width!=0orb.height!=0])

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp