_secondary_axes.py 13 KB

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