Coverage for /usr/lib/python3/dist-packages/matplotlib/axes/_secondary_axes.py: 17%
120 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1import numpy as np
3from matplotlib import _api, _docstring
4import matplotlib.ticker as mticker
5from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator
6from matplotlib.axis import Axis
9class SecondaryAxis(_AxesBase):
10 """
11 General class to hold a Secondary_X/Yaxis.
12 """
14 def __init__(self, parent, orientation, location, functions, **kwargs):
15 """
16 See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
17 While there is no need for this to be private, it should really be
18 called by those higher level functions.
19 """
21 self._functions = functions
22 self._parent = parent
23 self._orientation = orientation
24 self._ticks_set = False
26 if self._orientation == 'x':
27 super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
28 self._axis = self.xaxis
29 self._locstrings = ['top', 'bottom']
30 self._otherstrings = ['left', 'right']
31 elif self._orientation == 'y':
32 super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs)
33 self._axis = self.yaxis
34 self._locstrings = ['right', 'left']
35 self._otherstrings = ['top', 'bottom']
36 self._parentscale = None
37 # this gets positioned w/o constrained_layout so exclude:
39 self.set_location(location)
40 self.set_functions(functions)
42 # styling:
43 if self._orientation == 'x':
44 otheraxis = self.yaxis
45 else:
46 otheraxis = self.xaxis
48 otheraxis.set_major_locator(mticker.NullLocator())
49 otheraxis.set_ticks_position('none')
51 self.spines[self._otherstrings].set_visible(False)
52 self.spines[self._locstrings].set_visible(True)
54 if self._pos < 0.5:
55 # flip the location strings...
56 self._locstrings = self._locstrings[::-1]
57 self.set_alignment(self._locstrings[0])
59 def set_alignment(self, align):
60 """
61 Set if axes spine and labels are drawn at top or bottom (or left/right)
62 of the axes.
64 Parameters
65 ----------
66 align : str
67 either 'top' or 'bottom' for orientation='x' or
68 'left' or 'right' for orientation='y' axis.
69 """
70 _api.check_in_list(self._locstrings, align=align)
71 if align == self._locstrings[1]: # Need to change the orientation.
72 self._locstrings = self._locstrings[::-1]
73 self.spines[self._locstrings[0]].set_visible(True)
74 self.spines[self._locstrings[1]].set_visible(False)
75 self._axis.set_ticks_position(align)
76 self._axis.set_label_position(align)
78 def set_location(self, location):
79 """
80 Set the vertical or horizontal location of the axes in
81 parent-normalized coordinates.
83 Parameters
84 ----------
85 location : {'top', 'bottom', 'left', 'right'} or float
86 The position to put the secondary axis. Strings can be 'top' or
87 'bottom' for orientation='x' and 'right' or 'left' for
88 orientation='y'. A float indicates the relative position on the
89 parent axes to put the new axes, 0.0 being the bottom (or left)
90 and 1.0 being the top (or right).
91 """
93 # This puts the rectangle into figure-relative coordinates.
94 if isinstance(location, str):
95 if location in ['top', 'right']:
96 self._pos = 1.
97 elif location in ['bottom', 'left']:
98 self._pos = 0.
99 else:
100 raise ValueError(
101 f"location must be {self._locstrings[0]!r}, "
102 f"{self._locstrings[1]!r}, or a float, not {location!r}")
103 else:
104 self._pos = location
105 self._loc = location
107 if self._orientation == 'x':
108 # An x-secondary axes is like an inset axes from x = 0 to x = 1 and
109 # from y = pos to y = pos + eps, in the parent's transAxes coords.
110 bounds = [0, self._pos, 1., 1e-10]
111 else:
112 bounds = [self._pos, 0, 1e-10, 1]
114 # this locator lets the axes move in the parent axes coordinates.
115 # so it never needs to know where the parent is explicitly in
116 # figure coordinates.
117 # it gets called in ax.apply_aspect() (of all places)
118 self.set_axes_locator(
119 _TransformedBoundsLocator(bounds, self._parent.transAxes))
121 def apply_aspect(self, position=None):
122 # docstring inherited.
123 self._set_lims()
124 super().apply_aspect(position)
126 @_docstring.copy(Axis.set_ticks)
127 def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs):
128 ret = self._axis.set_ticks(ticks, labels, minor=minor, **kwargs)
129 self.stale = True
130 self._ticks_set = True
131 return ret
133 def set_functions(self, functions):
134 """
135 Set how the secondary axis converts limits from the parent axes.
137 Parameters
138 ----------
139 functions : 2-tuple of func, or `Transform` with an inverse.
140 Transform between the parent axis values and the secondary axis
141 values.
143 If supplied as a 2-tuple of functions, the first function is
144 the forward transform function and the second is the inverse
145 transform.
147 If a transform is supplied, then the transform must have an
148 inverse.
149 """
150 if (isinstance(functions, tuple) and len(functions) == 2 and
151 callable(functions[0]) and callable(functions[1])):
152 # make an arbitrary convert from a two-tuple of functions
153 # forward and inverse.
154 self._functions = functions
155 elif functions is None:
156 self._functions = (lambda x: x, lambda x: x)
157 else:
158 raise ValueError('functions argument of secondary axes '
159 'must be a two-tuple of callable functions '
160 'with the first function being the transform '
161 'and the second being the inverse')
162 self._set_scale()
164 # Should be changed to draw(self, renderer) once the deprecation of
165 # renderer=None and of inframe expires.
166 def draw(self, *args, **kwargs):
167 """
168 Draw the secondary axes.
170 Consults the parent axes for its limits and converts them
171 using the converter specified by
172 `~.axes._secondary_axes.set_functions` (or *functions*
173 parameter when axes initialized.)
174 """
175 self._set_lims()
176 # this sets the scale in case the parent has set its scale.
177 self._set_scale()
178 super().draw(*args, **kwargs)
180 def _set_scale(self):
181 """
182 Check if parent has set its scale
183 """
185 if self._orientation == 'x':
186 pscale = self._parent.xaxis.get_scale()
187 set_scale = self.set_xscale
188 if self._orientation == 'y':
189 pscale = self._parent.yaxis.get_scale()
190 set_scale = self.set_yscale
191 if pscale == self._parentscale:
192 return
194 if pscale == 'log':
195 defscale = 'functionlog'
196 else:
197 defscale = 'function'
199 if self._ticks_set:
200 ticks = self._axis.get_ticklocs()
202 # need to invert the roles here for the ticks to line up.
203 set_scale(defscale, functions=self._functions[::-1])
205 # OK, set_scale sets the locators, but if we've called
206 # axsecond.set_ticks, we want to keep those.
207 if self._ticks_set:
208 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
213 def _set_lims(self):
214 """
215 Set the limits based on parent limits and the convert method
216 between the parent and this secondary axes.
217 """
218 if self._orientation == 'x':
219 lims = self._parent.get_xlim()
220 set_lim = self.set_xlim
221 if self._orientation == 'y':
222 lims = self._parent.get_ylim()
223 set_lim = self.set_ylim
224 order = lims[0] < lims[1]
225 lims = self._functions[0](np.array(lims))
226 neworder = lims[0] < lims[1]
227 if neworder != order:
228 # Flip because the transform will take care of the flipping.
229 lims = lims[::-1]
230 set_lim(lims)
232 def set_aspect(self, *args, **kwargs):
233 """
234 Secondary axes cannot set the aspect ratio, so calling this just
235 sets a warning.
236 """
237 _api.warn_external("Secondary axes can't set the aspect ratio")
239 def set_color(self, color):
240 """
241 Change the color of the secondary axes and all decorators.
243 Parameters
244 ----------
245 color : color
246 """
247 if self._orientation == 'x':
248 self.tick_params(axis='x', colors=color)
249 self.spines.bottom.set_color(color)
250 self.spines.top.set_color(color)
251 self.xaxis.label.set_color(color)
252 else:
253 self.tick_params(axis='y', colors=color)
254 self.spines.left.set_color(color)
255 self.spines.right.set_color(color)
256 self.yaxis.label.set_color(color)
259_secax_docstring = '''
260Warnings
261--------
262This method is experimental as of 3.1, and the API may change.
264Parameters
265----------
266location : {'top', 'bottom', 'left', 'right'} or float
267 The position to put the secondary axis. Strings can be 'top' or
268 'bottom' for orientation='x' and 'right' or 'left' for
269 orientation='y'. A float indicates the relative position on the
270 parent axes to put the new axes, 0.0 being the bottom (or left)
271 and 1.0 being the top (or right).
273functions : 2-tuple of func, or Transform with an inverse
275 If a 2-tuple of functions, the user specifies the transform
276 function and its inverse. i.e.
277 ``functions=(lambda x: 2 / x, lambda x: 2 / x)`` would be an
278 reciprocal transform with a factor of 2. Both functions must accept
279 numpy arrays as input.
281 The user can also directly supply a subclass of
282 `.transforms.Transform` so long as it has an inverse.
284 See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
285 for examples of making these conversions.
287Returns
288-------
289ax : axes._secondary_axes.SecondaryAxis
291Other Parameters
292----------------
293**kwargs : `~matplotlib.axes.Axes` properties.
294 Other miscellaneous axes parameters.
295'''
296_docstring.interpd.update(_secax_docstring=_secax_docstring)