Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import numpy as np 

2 

3import matplotlib.cbook as cbook 

4import matplotlib.docstring as docstring 

5import matplotlib.ticker as mticker 

6import matplotlib.transforms as mtransforms 

7from matplotlib.axes._base import _AxesBase 

8 

9 

10def _make_secondary_locator(rect, parent): 

11 """ 

12 Helper function to locate the secondary axes. 

13 

14 A locator gets used in `Axes.set_aspect` to override the default 

15 locations... It is a function that takes an axes object and 

16 a renderer and tells `set_aspect` where it is to be placed. 

17 

18 This locator make the transform be in axes-relative co-coordinates 

19 because that is how we specify the "location" of the secondary axes. 

20 

21 Here *rect* is a rectangle [l, b, w, h] that specifies the 

22 location for the axes in the transform given by *trans* on the 

23 *parent*. 

24 """ 

25 _rect = mtransforms.Bbox.from_bounds(*rect) 

26 def secondary_locator(ax, renderer): 

27 # delay evaluating transform until draw time because the 

28 # parent transform may have changed (i.e. if window reesized) 

29 bb = mtransforms.TransformedBbox(_rect, parent.transAxes) 

30 tr = parent.figure.transFigure.inverted() 

31 bb = mtransforms.TransformedBbox(bb, tr) 

32 return bb 

33 

34 return secondary_locator 

35 

36 

37class SecondaryAxis(_AxesBase): 

38 """ 

39 General class to hold a Secondary_X/Yaxis. 

40 """ 

41 

42 def __init__(self, parent, orientation, 

43 location, functions, **kwargs): 

44 """ 

45 See `.secondary_xaxis` and `.secondary_yaxis` for the doc string. 

46 While there is no need for this to be private, it should really be 

47 called by those higher level functions. 

48 """ 

49 

50 self._functions = functions 

51 self._parent = parent 

52 self._orientation = orientation 

53 self._ticks_set = False 

54 

55 if self._orientation == 'x': 

56 super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs) 

57 self._axis = self.xaxis 

58 self._locstrings = ['top', 'bottom'] 

59 self._otherstrings = ['left', 'right'] 

60 elif self._orientation == 'y': 

61 super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs) 

62 self._axis = self.yaxis 

63 self._locstrings = ['right', 'left'] 

64 self._otherstrings = ['top', 'bottom'] 

65 self._parentscale = self._axis.get_scale() 

66 # this gets positioned w/o constrained_layout so exclude: 

67 self._layoutbox = None 

68 self._poslayoutbox = None 

69 

70 self.set_location(location) 

71 self.set_functions(functions) 

72 

73 # styling: 

74 if self._orientation == 'x': 

75 otheraxis = self.yaxis 

76 else: 

77 otheraxis = self.xaxis 

78 

79 otheraxis.set_major_locator(mticker.NullLocator()) 

80 otheraxis.set_ticks_position('none') 

81 

82 for st in self._otherstrings: 

83 self.spines[st].set_visible(False) 

84 for st in self._locstrings: 

85 self.spines[st].set_visible(True) 

86 

87 if self._pos < 0.5: 

88 # flip the location strings... 

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

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

91 

92 def set_alignment(self, align): 

93 """ 

94 Set if axes spine and labels are drawn at top or bottom (or left/right) 

95 of the axes. 

96 

97 Parameters 

98 ---------- 

99 align : str 

100 either 'top' or 'bottom' for orientation='x' or 

101 'left' or 'right' for orientation='y' axis. 

102 """ 

103 if align in self._locstrings: 

104 if align == self._locstrings[1]: 

105 # need to change the orientation. 

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

107 elif align != self._locstrings[0]: 

108 raise ValueError('"{}" is not a valid axis orientation, ' 

109 'not changing the orientation;' 

110 'choose "{}" or "{}""'.format(align, 

111 self._locstrings[0], self._locstrings[1])) 

112 self.spines[self._locstrings[0]].set_visible(True) 

113 self.spines[self._locstrings[1]].set_visible(False) 

114 self._axis.set_ticks_position(align) 

115 self._axis.set_label_position(align) 

116 

117 def set_location(self, location): 

118 """ 

119 Set the vertical or horizontal location of the axes in 

120 parent-normalized co-ordinates. 

121 

122 Parameters 

123 ---------- 

124 location : {'top', 'bottom', 'left', 'right'} or float 

125 The position to put the secondary axis. Strings can be 'top' or 

126 'bottom' for orientation='x' and 'right' or 'left' for 

127 orientation='y'. A float indicates the relative position on the 

128 parent axes to put the new axes, 0.0 being the bottom (or left) 

129 and 1.0 being the top (or right). 

130 """ 

131 

132 # This puts the rectangle into figure-relative coordinates. 

133 if isinstance(location, str): 

134 if location in ['top', 'right']: 

135 self._pos = 1. 

136 elif location in ['bottom', 'left']: 

137 self._pos = 0. 

138 else: 

139 raise ValueError("location must be '{}', '{}', or a " 

140 "float, not '{}'".format(location, 

141 self._locstrings[0], self._locstrings[1])) 

142 else: 

143 self._pos = location 

144 self._loc = location 

145 

146 if self._orientation == 'x': 

147 bounds = [0, self._pos, 1., 1e-10] 

148 else: 

149 bounds = [self._pos, 0, 1e-10, 1] 

150 

151 secondary_locator = _make_secondary_locator(bounds, self._parent) 

152 

153 # this locator lets the axes move in the parent axes coordinates. 

154 # so it never needs to know where the parent is explicitly in 

155 # figure co-ordinates. 

156 # it gets called in `ax.apply_aspect() (of all places) 

157 self.set_axes_locator(secondary_locator) 

158 

159 def apply_aspect(self, position=None): 

160 # docstring inherited. 

161 self._set_lims() 

162 super().apply_aspect(position) 

163 

164 @cbook._make_keyword_only("3.2", "minor") 

165 def set_ticks(self, ticks, minor=False): 

166 """ 

167 Set the x ticks with list of *ticks* 

168 

169 Parameters 

170 ---------- 

171 ticks : list 

172 List of x-axis tick locations. 

173 minor : bool, optional 

174 If ``False`` sets major ticks, if ``True`` sets minor ticks. 

175 Default is ``False``. 

176 """ 

177 ret = self._axis.set_ticks(ticks, minor=minor) 

178 self.stale = True 

179 self._ticks_set = True 

180 return ret 

181 

182 def set_functions(self, functions): 

183 """ 

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

185 

186 Parameters 

187 ---------- 

188 functions : 2-tuple of func, or `Transform` with an inverse. 

189 Transform between the parent axis values and the secondary axis 

190 values. 

191 

192 If supplied as a 2-tuple of functions, the first function is 

193 the forward transform function and the second is the inverse 

194 transform. 

195 

196 If a transform is supplied, then the transform must have an 

197 inverse. 

198 """ 

199 

200 if self._orientation == 'x': 

201 set_scale = self.set_xscale 

202 parent_scale = self._parent.get_xscale() 

203 else: 

204 set_scale = self.set_yscale 

205 parent_scale = self._parent.get_yscale() 

206 # we need to use a modified scale so the scale can receive the 

207 # transform. Only types supported are linear and log10 for now. 

208 # Probably possible to add other transforms as a todo... 

209 if parent_scale == 'log': 

210 defscale = 'functionlog' 

211 else: 

212 defscale = 'function' 

213 

214 if (isinstance(functions, tuple) and len(functions) == 2 and 

215 callable(functions[0]) and callable(functions[1])): 

216 # make an arbitrary convert from a two-tuple of functions 

217 # forward and inverse. 

218 self._functions = functions 

219 elif functions is None: 

220 self._functions = (lambda x: x, lambda x: x) 

221 else: 

222 raise ValueError('functions argument of secondary axes ' 

223 'must be a two-tuple of callable functions ' 

224 'with the first function being the transform ' 

225 'and the second being the inverse') 

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

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

228 

229 def draw(self, renderer=None, inframe=False): 

230 """ 

231 Draw the secondary axes. 

232 

233 Consults the parent axes for its limits and converts them 

234 using the converter specified by 

235 `~.axes._secondary_axes.set_functions` (or *functions* 

236 parameter when axes initialized.) 

237 """ 

238 self._set_lims() 

239 # this sets the scale in case the parent has set its scale. 

240 self._set_scale() 

241 super().draw(renderer=renderer, inframe=inframe) 

242 

243 def _set_scale(self): 

244 """ 

245 Check if parent has set its scale 

246 """ 

247 

248 if self._orientation == 'x': 

249 pscale = self._parent.xaxis.get_scale() 

250 set_scale = self.set_xscale 

251 if self._orientation == 'y': 

252 pscale = self._parent.yaxis.get_scale() 

253 set_scale = self.set_yscale 

254 if pscale == self._parentscale: 

255 return 

256 else: 

257 self._parentscale = pscale 

258 

259 if pscale == 'log': 

260 defscale = 'functionlog' 

261 else: 

262 defscale = 'function' 

263 

264 if self._ticks_set: 

265 ticks = self._axis.get_ticklocs() 

266 

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

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

269 

270 # OK, set_scale sets the locators, but if we've called 

271 # axsecond.set_ticks, we want to keep those. 

272 if self._ticks_set: 

273 self._axis.set_major_locator(mticker.FixedLocator(ticks)) 

274 

275 def _set_lims(self): 

276 """ 

277 Set the limits based on parent limits and the convert method 

278 between the parent and this secondary axes. 

279 """ 

280 if self._orientation == 'x': 

281 lims = self._parent.get_xlim() 

282 set_lim = self.set_xlim 

283 if self._orientation == 'y': 

284 lims = self._parent.get_ylim() 

285 set_lim = self.set_ylim 

286 order = lims[0] < lims[1] 

287 lims = self._functions[0](np.array(lims)) 

288 neworder = lims[0] < lims[1] 

289 if neworder != order: 

290 # Flip because the transform will take care of the flipping. 

291 lims = lims[::-1] 

292 set_lim(lims) 

293 

294 def set_aspect(self, *args, **kwargs): 

295 """ 

296 Secondary axes cannot set the aspect ratio, so calling this just 

297 sets a warning. 

298 """ 

299 cbook._warn_external("Secondary axes can't set the aspect ratio") 

300 

301 def set_xlabel(self, xlabel, fontdict=None, labelpad=None, **kwargs): 

302 """ 

303 Set the label for the x-axis. 

304 

305 Parameters 

306 ---------- 

307 xlabel : str 

308 The label text. 

309 

310 labelpad : scalar, optional, default: None 

311 Spacing in points between the label and the x-axis. 

312 

313 Other Parameters 

314 ---------------- 

315 **kwargs : `.Text` properties 

316 `.Text` properties control the appearance of the label. 

317 

318 See also 

319 -------- 

320 text : for information on how override and the optional args work 

321 """ 

322 if labelpad is not None: 

323 self.xaxis.labelpad = labelpad 

324 return self.xaxis.set_label_text(xlabel, fontdict, **kwargs) 

325 

326 def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs): 

327 """ 

328 Set the label for the x-axis. 

329 

330 Parameters 

331 ---------- 

332 ylabel : str 

333 The label text. 

334 

335 labelpad : scalar, optional, default: None 

336 Spacing in points between the label and the x-axis. 

337 

338 Other Parameters 

339 ---------------- 

340 **kwargs : `.Text` properties 

341 `.Text` properties control the appearance of the label. 

342 

343 See also 

344 -------- 

345 text : for information on how override and the optional args work 

346 """ 

347 if labelpad is not None: 

348 self.yaxis.labelpad = labelpad 

349 return self.yaxis.set_label_text(ylabel, fontdict, **kwargs) 

350 

351 def set_color(self, color): 

352 """ 

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

354 

355 Parameters 

356 ---------- 

357 color : Matplotlib color 

358 """ 

359 if self._orientation == 'x': 

360 self.tick_params(axis='x', colors=color) 

361 self.spines['bottom'].set_color(color) 

362 self.spines['top'].set_color(color) 

363 self.xaxis.label.set_color(color) 

364 else: 

365 self.tick_params(axis='y', colors=color) 

366 self.spines['left'].set_color(color) 

367 self.spines['right'].set_color(color) 

368 self.yaxis.label.set_color(color) 

369 

370 

371_secax_docstring = ''' 

372Warnings 

373-------- 

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

375 

376Parameters 

377---------- 

378location : {'top', 'bottom', 'left', 'right'} or float 

379 The position to put the secondary axis. Strings can be 'top' or 

380 'bottom' for orientation='x' and 'right' or 'left' for 

381 orientation='y'. A float indicates the relative position on the 

382 parent axes to put the new axes, 0.0 being the bottom (or left) 

383 and 1.0 being the top (or right). 

384 

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

386 

387 If a 2-tuple of functions, the user specifies the transform 

388 function and its inverse. i.e. 

389 `functions=(lambda x: 2 / x, lambda x: 2 / x)` would be an 

390 reciprocal transform with a factor of 2. 

391 

392 The user can also directly supply a subclass of 

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

394 

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

396 for examples of making these conversions. 

397 

398 

399Other Parameters 

400---------------- 

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

402 Other miscellaneous axes parameters. 

403 

404Returns 

405------- 

406ax : axes._secondary_axes.SecondaryAxis 

407''' 

408docstring.interpd.update(_secax_docstring=_secax_docstring)