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

1import numpy as np 

2 

3from matplotlib import _api, _docstring 

4import matplotlib.ticker as mticker 

5from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator 

6from matplotlib.axis import Axis 

7 

8 

9class SecondaryAxis(_AxesBase): 

10 """ 

11 General class to hold a Secondary_X/Yaxis. 

12 """ 

13 

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 """ 

20 

21 self._functions = functions 

22 self._parent = parent 

23 self._orientation = orientation 

24 self._ticks_set = False 

25 

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: 

38 

39 self.set_location(location) 

40 self.set_functions(functions) 

41 

42 # styling: 

43 if self._orientation == 'x': 

44 otheraxis = self.yaxis 

45 else: 

46 otheraxis = self.xaxis 

47 

48 otheraxis.set_major_locator(mticker.NullLocator()) 

49 otheraxis.set_ticks_position('none') 

50 

51 self.spines[self._otherstrings].set_visible(False) 

52 self.spines[self._locstrings].set_visible(True) 

53 

54 if self._pos < 0.5: 

55 # flip the location strings... 

56 self._locstrings = self._locstrings[::-1] 

57 self.set_alignment(self._locstrings[0]) 

58 

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. 

63 

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) 

77 

78 def set_location(self, location): 

79 """ 

80 Set the vertical or horizontal location of the axes in 

81 parent-normalized coordinates. 

82 

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 """ 

92 

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 

106 

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] 

113 

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)) 

120 

121 def apply_aspect(self, position=None): 

122 # docstring inherited. 

123 self._set_lims() 

124 super().apply_aspect(position) 

125 

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 

132 

133 def set_functions(self, functions): 

134 """ 

135 Set how the secondary axis converts limits from the parent axes. 

136 

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. 

142 

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. 

146 

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() 

163 

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. 

169 

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) 

179 

180 def _set_scale(self): 

181 """ 

182 Check if parent has set its scale 

183 """ 

184 

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 

193 

194 if pscale == 'log': 

195 defscale = 'functionlog' 

196 else: 

197 defscale = 'function' 

198 

199 if self._ticks_set: 

200 ticks = self._axis.get_ticklocs() 

201 

202 # need to invert the roles here for the ticks to line up. 

203 set_scale(defscale, functions=self._functions[::-1]) 

204 

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)) 

209 

210 # If the parent scale doesn't change, we can skip this next time. 

211 self._parentscale = pscale 

212 

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) 

231 

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") 

238 

239 def set_color(self, color): 

240 """ 

241 Change the color of the secondary axes and all decorators. 

242 

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) 

257 

258 

259_secax_docstring = ''' 

260Warnings 

261-------- 

262This method is experimental as of 3.1, and the API may change. 

263 

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). 

272 

273functions : 2-tuple of func, or Transform with an inverse 

274 

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. 

280 

281 The user can also directly supply a subclass of 

282 `.transforms.Transform` so long as it has an inverse. 

283 

284 See :doc:`/gallery/subplots_axes_and_figures/secondary_axis` 

285 for examples of making these conversions. 

286 

287Returns 

288------- 

289ax : axes._secondary_axes.SecondaryAxis 

290 

291Other Parameters 

292---------------- 

293**kwargs : `~matplotlib.axes.Axes` properties. 

294 Other miscellaneous axes parameters. 

295''' 

296_docstring.interpd.update(_secax_docstring=_secax_docstring)