_secondary_axes.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import numpy as np
  2. import matplotlib.cbook as cbook
  3. import matplotlib.docstring as docstring
  4. import matplotlib.ticker as mticker
  5. import matplotlib.transforms as mtransforms
  6. from matplotlib.axes._base import _AxesBase
  7. def _make_secondary_locator(rect, parent):
  8. """
  9. Helper function to locate the secondary axes.
  10. A locator gets used in `Axes.set_aspect` to override the default
  11. locations... It is a function that takes an axes object and
  12. a renderer and tells `set_aspect` where it is to be placed.
  13. This locator make the transform be in axes-relative co-coordinates
  14. because that is how we specify the "location" of the secondary axes.
  15. Here *rect* is a rectangle [l, b, w, h] that specifies the
  16. location for the axes in the transform given by *trans* on the
  17. *parent*.
  18. """
  19. _rect = mtransforms.Bbox.from_bounds(*rect)
  20. def secondary_locator(ax, renderer):
  21. # delay evaluating transform until draw time because the
  22. # parent transform may have changed (i.e. if window reesized)
  23. bb = mtransforms.TransformedBbox(_rect, parent.transAxes)
  24. tr = parent.figure.transFigure.inverted()
  25. bb = mtransforms.TransformedBbox(bb, tr)
  26. return bb
  27. return secondary_locator
  28. class SecondaryAxis(_AxesBase):
  29. """
  30. General class to hold a Secondary_X/Yaxis.
  31. """
  32. def __init__(self, parent, orientation,
  33. location, functions, **kwargs):
  34. """
  35. See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
  36. While there is no need for this to be private, it should really be
  37. called by those higher level functions.
  38. """
  39. self._functions = functions
  40. self._parent = parent
  41. self._orientation = orientation
  42. self._ticks_set = False
  43. if self._orientation == 'x':
  44. super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
  45. self._axis = self.xaxis
  46. self._locstrings = ['top', 'bottom']
  47. self._otherstrings = ['left', 'right']
  48. elif self._orientation == 'y':
  49. super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs)
  50. self._axis = self.yaxis
  51. self._locstrings = ['right', 'left']
  52. self._otherstrings = ['top', 'bottom']
  53. self._parentscale = self._axis.get_scale()
  54. # this gets positioned w/o constrained_layout so exclude:
  55. self._layoutbox = None
  56. self._poslayoutbox = None
  57. self.set_location(location)
  58. self.set_functions(functions)
  59. # styling:
  60. if self._orientation == 'x':
  61. otheraxis = self.yaxis
  62. else:
  63. otheraxis = self.xaxis
  64. otheraxis.set_major_locator(mticker.NullLocator())
  65. otheraxis.set_ticks_position('none')
  66. for st in self._otherstrings:
  67. self.spines[st].set_visible(False)
  68. for st in self._locstrings:
  69. self.spines[st].set_visible(True)
  70. if self._pos < 0.5:
  71. # flip the location strings...
  72. self._locstrings = self._locstrings[::-1]
  73. self.set_alignment(self._locstrings[0])
  74. def set_alignment(self, align):
  75. """
  76. Set if axes spine and labels are drawn at top or bottom (or left/right)
  77. of the axes.
  78. Parameters
  79. ----------
  80. align : str
  81. either 'top' or 'bottom' for orientation='x' or
  82. 'left' or 'right' for orientation='y' axis.
  83. """
  84. if align in self._locstrings:
  85. if align == self._locstrings[1]:
  86. # need to change the orientation.
  87. self._locstrings = self._locstrings[::-1]
  88. elif align != self._locstrings[0]:
  89. raise ValueError('"{}" is not a valid axis orientation, '
  90. 'not changing the orientation;'
  91. 'choose "{}" or "{}""'.format(align,
  92. self._locstrings[0], self._locstrings[1]))
  93. self.spines[self._locstrings[0]].set_visible(True)
  94. self.spines[self._locstrings[1]].set_visible(False)
  95. self._axis.set_ticks_position(align)
  96. self._axis.set_label_position(align)
  97. def set_location(self, location):
  98. """
  99. Set the vertical or horizontal location of the axes in
  100. parent-normalized co-ordinates.
  101. Parameters
  102. ----------
  103. location : {'top', 'bottom', 'left', 'right'} or float
  104. The position to put the secondary axis. Strings can be 'top' or
  105. 'bottom' for orientation='x' and 'right' or 'left' for
  106. orientation='y'. A float indicates the relative position on the
  107. parent axes to put the new axes, 0.0 being the bottom (or left)
  108. and 1.0 being the top (or right).
  109. """
  110. # This puts the rectangle into figure-relative coordinates.
  111. if isinstance(location, str):
  112. if location in ['top', 'right']:
  113. self._pos = 1.
  114. elif location in ['bottom', 'left']:
  115. self._pos = 0.
  116. else:
  117. raise ValueError("location must be '{}', '{}', or a "
  118. "float, not '{}'".format(location,
  119. self._locstrings[0], self._locstrings[1]))
  120. else:
  121. self._pos = location
  122. self._loc = location
  123. if self._orientation == 'x':
  124. bounds = [0, self._pos, 1., 1e-10]
  125. else:
  126. bounds = [self._pos, 0, 1e-10, 1]
  127. secondary_locator = _make_secondary_locator(bounds, self._parent)
  128. # this locator lets the axes move in the parent axes coordinates.
  129. # so it never needs to know where the parent is explicitly in
  130. # figure co-ordinates.
  131. # it gets called in `ax.apply_aspect() (of all places)
  132. self.set_axes_locator(secondary_locator)
  133. def apply_aspect(self, position=None):
  134. # docstring inherited.
  135. self._set_lims()
  136. super().apply_aspect(position)
  137. @cbook._make_keyword_only("3.2", "minor")
  138. def set_ticks(self, ticks, minor=False):
  139. """
  140. Set the x ticks with list of *ticks*
  141. Parameters
  142. ----------
  143. ticks : list
  144. List of x-axis tick locations.
  145. minor : bool, optional
  146. If ``False`` sets major ticks, if ``True`` sets minor ticks.
  147. Default is ``False``.
  148. """
  149. ret = self._axis.set_ticks(ticks, minor=minor)
  150. self.stale = True
  151. self._ticks_set = True
  152. return ret
  153. def set_functions(self, functions):
  154. """
  155. Set how the secondary axis converts limits from the parent axes.
  156. Parameters
  157. ----------
  158. functions : 2-tuple of func, or `Transform` with an inverse.
  159. Transform between the parent axis values and the secondary axis
  160. values.
  161. If supplied as a 2-tuple of functions, the first function is
  162. the forward transform function and the second is the inverse
  163. transform.
  164. If a transform is supplied, then the transform must have an
  165. inverse.
  166. """
  167. if self._orientation == 'x':
  168. set_scale = self.set_xscale
  169. parent_scale = self._parent.get_xscale()
  170. else:
  171. set_scale = self.set_yscale
  172. parent_scale = self._parent.get_yscale()
  173. # we need to use a modified scale so the scale can receive the
  174. # transform. Only types supported are linear and log10 for now.
  175. # Probably possible to add other transforms as a todo...
  176. if parent_scale == 'log':
  177. defscale = 'functionlog'
  178. else:
  179. defscale = 'function'
  180. if (isinstance(functions, tuple) and len(functions) == 2 and
  181. callable(functions[0]) and callable(functions[1])):
  182. # make an arbitrary convert from a two-tuple of functions
  183. # forward and inverse.
  184. self._functions = functions
  185. elif functions is None:
  186. self._functions = (lambda x: x, lambda x: x)
  187. else:
  188. raise ValueError('functions argument of secondary axes '
  189. 'must be a two-tuple of callable functions '
  190. 'with the first function being the transform '
  191. 'and the second being the inverse')
  192. # need to invert the roles here for the ticks to line up.
  193. set_scale(defscale, functions=self._functions[::-1])
  194. def draw(self, renderer=None, inframe=False):
  195. """
  196. Draw the secondary axes.
  197. Consults the parent axes for its limits and converts them
  198. using the converter specified by
  199. `~.axes._secondary_axes.set_functions` (or *functions*
  200. parameter when axes initialized.)
  201. """
  202. self._set_lims()
  203. # this sets the scale in case the parent has set its scale.
  204. self._set_scale()
  205. super().draw(renderer=renderer, inframe=inframe)
  206. def _set_scale(self):
  207. """
  208. Check if parent has set its scale
  209. """
  210. if self._orientation == 'x':
  211. pscale = self._parent.xaxis.get_scale()
  212. set_scale = self.set_xscale
  213. if self._orientation == 'y':
  214. pscale = self._parent.yaxis.get_scale()
  215. set_scale = self.set_yscale
  216. if pscale == self._parentscale:
  217. return
  218. else:
  219. self._parentscale = pscale
  220. if pscale == 'log':
  221. defscale = 'functionlog'
  222. else:
  223. defscale = 'function'
  224. if self._ticks_set:
  225. ticks = self._axis.get_ticklocs()
  226. # need to invert the roles here for the ticks to line up.
  227. set_scale(defscale, functions=self._functions[::-1])
  228. # OK, set_scale sets the locators, but if we've called
  229. # axsecond.set_ticks, we want to keep those.
  230. if self._ticks_set:
  231. self._axis.set_major_locator(mticker.FixedLocator(ticks))
  232. def _set_lims(self):
  233. """
  234. Set the limits based on parent limits and the convert method
  235. between the parent and this secondary axes.
  236. """
  237. if self._orientation == 'x':
  238. lims = self._parent.get_xlim()
  239. set_lim = self.set_xlim
  240. if self._orientation == 'y':
  241. lims = self._parent.get_ylim()
  242. set_lim = self.set_ylim
  243. order = lims[0] < lims[1]
  244. lims = self._functions[0](np.array(lims))
  245. neworder = lims[0] < lims[1]
  246. if neworder != order:
  247. # Flip because the transform will take care of the flipping.
  248. lims = lims[::-1]
  249. set_lim(lims)
  250. def set_aspect(self, *args, **kwargs):
  251. """
  252. Secondary axes cannot set the aspect ratio, so calling this just
  253. sets a warning.
  254. """
  255. cbook._warn_external("Secondary axes can't set the aspect ratio")
  256. def set_xlabel(self, xlabel, fontdict=None, labelpad=None, **kwargs):
  257. """
  258. Set the label for the x-axis.
  259. Parameters
  260. ----------
  261. xlabel : str
  262. The label text.
  263. labelpad : scalar, optional, default: None
  264. Spacing in points between the label and the x-axis.
  265. Other Parameters
  266. ----------------
  267. **kwargs : `.Text` properties
  268. `.Text` properties control the appearance of the label.
  269. See also
  270. --------
  271. text : for information on how override and the optional args work
  272. """
  273. if labelpad is not None:
  274. self.xaxis.labelpad = labelpad
  275. return self.xaxis.set_label_text(xlabel, fontdict, **kwargs)
  276. def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs):
  277. """
  278. Set the label for the x-axis.
  279. Parameters
  280. ----------
  281. ylabel : str
  282. The label text.
  283. labelpad : scalar, optional, default: None
  284. Spacing in points between the label and the x-axis.
  285. Other Parameters
  286. ----------------
  287. **kwargs : `.Text` properties
  288. `.Text` properties control the appearance of the label.
  289. See also
  290. --------
  291. text : for information on how override and the optional args work
  292. """
  293. if labelpad is not None:
  294. self.yaxis.labelpad = labelpad
  295. return self.yaxis.set_label_text(ylabel, fontdict, **kwargs)
  296. def set_color(self, color):
  297. """
  298. Change the color of the secondary axes and all decorators.
  299. Parameters
  300. ----------
  301. color : Matplotlib color
  302. """
  303. if self._orientation == 'x':
  304. self.tick_params(axis='x', colors=color)
  305. self.spines['bottom'].set_color(color)
  306. self.spines['top'].set_color(color)
  307. self.xaxis.label.set_color(color)
  308. else:
  309. self.tick_params(axis='y', colors=color)
  310. self.spines['left'].set_color(color)
  311. self.spines['right'].set_color(color)
  312. self.yaxis.label.set_color(color)
  313. _secax_docstring = '''
  314. Warnings
  315. --------
  316. This method is experimental as of 3.1, and the API may change.
  317. Parameters
  318. ----------
  319. location : {'top', 'bottom', 'left', 'right'} or float
  320. The position to put the secondary axis. Strings can be 'top' or
  321. 'bottom' for orientation='x' and 'right' or 'left' for
  322. orientation='y'. A float indicates the relative position on the
  323. parent axes to put the new axes, 0.0 being the bottom (or left)
  324. and 1.0 being the top (or right).
  325. functions : 2-tuple of func, or Transform with an inverse
  326. If a 2-tuple of functions, the user specifies the transform
  327. function and its inverse. i.e.
  328. `functions=(lambda x: 2 / x, lambda x: 2 / x)` would be an
  329. reciprocal transform with a factor of 2.
  330. The user can also directly supply a subclass of
  331. `.transforms.Transform` so long as it has an inverse.
  332. See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
  333. for examples of making these conversions.
  334. Other Parameters
  335. ----------------
  336. **kwargs : `~matplotlib.axes.Axes` properties.
  337. Other miscellaneous axes parameters.
  338. Returns
  339. -------
  340. ax : axes._secondary_axes.SecondaryAxis
  341. '''
  342. docstring.interpd.update(_secax_docstring=_secax_docstring)