axis_artist.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  1. """
  2. The :mod:`.axis_artist` module implements custom artists to draw axis elements
  3. (axis lines and labels, tick lines and labels, grid lines).
  4. Axis lines and labels and tick lines and labels are managed by the `AxisArtist`
  5. class; grid lines are managed by the `GridlinesCollection` class.
  6. There is one `AxisArtist` per Axis; it can be accessed through
  7. the ``axis`` dictionary of the parent Axes (which should be a
  8. `mpl_toolkits.axislines.Axes`), e.g. ``ax.axis["bottom"]``.
  9. Children of the AxisArtist are accessed as attributes: ``.line`` and ``.label``
  10. for the axis line and label, ``.major_ticks``, ``.major_ticklabels``,
  11. ``.minor_ticks``, ``.minor_ticklabels`` for the tick lines and labels (e.g.
  12. ``ax.axis["bottom"].line``).
  13. Children properties (colors, fonts, line widths, etc.) can be set using
  14. setters, e.g. ::
  15. # Make the major ticks of the bottom axis red.
  16. ax.axis["bottom"].major_ticks.set_color("red")
  17. However, things like the locations of ticks, and their ticklabels need to be
  18. changed from the side of the grid_helper.
  19. axis_direction
  20. --------------
  21. `AxisArtist`, `AxisLabel`, `TickLabels` have an *axis_direction* attribute,
  22. which adjusts the location, angle, etc. The *axis_direction* must be one of
  23. "left", "right", "bottom", "top", and follows the Matplotlib convention for
  24. rectangular axis.
  25. For example, for the *bottom* axis (the left and right is relative to the
  26. direction of the increasing coordinate),
  27. * ticklabels and axislabel are on the right
  28. * ticklabels and axislabel have text angle of 0
  29. * ticklabels are baseline, center-aligned
  30. * axislabel is top, center-aligned
  31. The text angles are actually relative to (90 + angle of the direction to the
  32. ticklabel), which gives 0 for bottom axis.
  33. =================== ====== ======== ====== ========
  34. Property left bottom right top
  35. =================== ====== ======== ====== ========
  36. ticklabel location left right right left
  37. axislabel location left right right left
  38. ticklabel angle 90 0 -90 180
  39. axislabel angle 180 0 0 180
  40. ticklabel va center baseline center baseline
  41. axislabel va center top center bottom
  42. ticklabel ha right center right center
  43. axislabel ha right center right center
  44. =================== ====== ======== ====== ========
  45. Ticks are by default direct opposite side of the ticklabels. To make ticks to
  46. the same side of the ticklabels, ::
  47. ax.axis["bottom"].major_ticks.set_tick_out(True)
  48. The following attributes can be customized (use the ``set_xxx`` methods):
  49. * `Ticks`: ticksize, tick_out
  50. * `TickLabels`: pad
  51. * `AxisLabel`: pad
  52. """
  53. # FIXME :
  54. # angles are given in data coordinate - need to convert it to canvas coordinate
  55. from operator import methodcaller
  56. import numpy as np
  57. import matplotlib as mpl
  58. from matplotlib import _api, cbook
  59. import matplotlib.artist as martist
  60. import matplotlib.colors as mcolors
  61. import matplotlib.text as mtext
  62. from matplotlib.collections import LineCollection
  63. from matplotlib.lines import Line2D
  64. from matplotlib.patches import PathPatch
  65. from matplotlib.path import Path
  66. from matplotlib.transforms import (
  67. Affine2D, Bbox, IdentityTransform, ScaledTranslation)
  68. from .axisline_style import AxislineStyle
  69. class AttributeCopier:
  70. def get_ref_artist(self):
  71. """
  72. Return the underlying artist that actually defines some properties
  73. (e.g., color) of this artist.
  74. """
  75. raise RuntimeError("get_ref_artist must overridden")
  76. def get_attribute_from_ref_artist(self, attr_name):
  77. getter = methodcaller("get_" + attr_name)
  78. prop = getter(super())
  79. return getter(self.get_ref_artist()) if prop == "auto" else prop
  80. class Ticks(AttributeCopier, Line2D):
  81. """
  82. Ticks are derived from `.Line2D`, and note that ticks themselves
  83. are markers. Thus, you should use set_mec, set_mew, etc.
  84. To change the tick size (length), you need to use
  85. `set_ticksize`. To change the direction of the ticks (ticks are
  86. in opposite direction of ticklabels by default), use
  87. ``set_tick_out(False)``
  88. """
  89. def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs):
  90. self._ticksize = ticksize
  91. self.locs_angles_labels = []
  92. self.set_tick_out(tick_out)
  93. self._axis = axis
  94. if self._axis is not None:
  95. if "color" not in kwargs:
  96. kwargs["color"] = "auto"
  97. if "mew" not in kwargs and "markeredgewidth" not in kwargs:
  98. kwargs["markeredgewidth"] = "auto"
  99. Line2D.__init__(self, [0.], [0.], **kwargs)
  100. self.set_snap(True)
  101. def get_ref_artist(self):
  102. # docstring inherited
  103. return self._axis.majorTicks[0].tick1line
  104. def set_color(self, color):
  105. # docstring inherited
  106. # Unlike the base Line2D.set_color, this also supports "auto".
  107. if not cbook._str_equal(color, "auto"):
  108. mcolors._check_color_like(color=color)
  109. self._color = color
  110. self.stale = True
  111. def get_color(self):
  112. return self.get_attribute_from_ref_artist("color")
  113. def get_markeredgecolor(self):
  114. return self.get_attribute_from_ref_artist("markeredgecolor")
  115. def get_markeredgewidth(self):
  116. return self.get_attribute_from_ref_artist("markeredgewidth")
  117. def set_tick_out(self, b):
  118. """Set whether ticks are drawn inside or outside the axes."""
  119. self._tick_out = b
  120. def get_tick_out(self):
  121. """Return whether ticks are drawn inside or outside the axes."""
  122. return self._tick_out
  123. def set_ticksize(self, ticksize):
  124. """Set length of the ticks in points."""
  125. self._ticksize = ticksize
  126. def get_ticksize(self):
  127. """Return length of the ticks in points."""
  128. return self._ticksize
  129. def set_locs_angles(self, locs_angles):
  130. self.locs_angles = locs_angles
  131. _tickvert_path = Path([[0., 0.], [1., 0.]])
  132. def draw(self, renderer):
  133. if not self.get_visible():
  134. return
  135. gc = renderer.new_gc()
  136. gc.set_foreground(self.get_markeredgecolor())
  137. gc.set_linewidth(self.get_markeredgewidth())
  138. gc.set_alpha(self._alpha)
  139. path_trans = self.get_transform()
  140. marker_transform = (Affine2D()
  141. .scale(renderer.points_to_pixels(self._ticksize)))
  142. if self.get_tick_out():
  143. marker_transform.rotate_deg(180)
  144. for loc, angle in self.locs_angles:
  145. locs = path_trans.transform_non_affine(np.array([loc]))
  146. if self.axes and not self.axes.viewLim.contains(*locs[0]):
  147. continue
  148. renderer.draw_markers(
  149. gc, self._tickvert_path,
  150. marker_transform + Affine2D().rotate_deg(angle),
  151. Path(locs), path_trans.get_affine())
  152. gc.restore()
  153. class LabelBase(mtext.Text):
  154. """
  155. A base class for `.AxisLabel` and `.TickLabels`. The position and
  156. angle of the text are calculated by the offset_ref_angle,
  157. text_ref_angle, and offset_radius attributes.
  158. """
  159. def __init__(self, *args, **kwargs):
  160. self.locs_angles_labels = []
  161. self._ref_angle = 0
  162. self._offset_radius = 0.
  163. super().__init__(*args, **kwargs)
  164. self.set_rotation_mode("anchor")
  165. self._text_follow_ref_angle = True
  166. @property
  167. def _text_ref_angle(self):
  168. if self._text_follow_ref_angle:
  169. return self._ref_angle + 90
  170. else:
  171. return 0
  172. @property
  173. def _offset_ref_angle(self):
  174. return self._ref_angle
  175. _get_opposite_direction = {"left": "right",
  176. "right": "left",
  177. "top": "bottom",
  178. "bottom": "top"}.__getitem__
  179. def draw(self, renderer):
  180. if not self.get_visible():
  181. return
  182. # save original and adjust some properties
  183. tr = self.get_transform()
  184. angle_orig = self.get_rotation()
  185. theta = np.deg2rad(self._offset_ref_angle)
  186. dd = self._offset_radius
  187. dx, dy = dd * np.cos(theta), dd * np.sin(theta)
  188. self.set_transform(tr + Affine2D().translate(dx, dy))
  189. self.set_rotation(self._text_ref_angle + angle_orig)
  190. super().draw(renderer)
  191. # restore original properties
  192. self.set_transform(tr)
  193. self.set_rotation(angle_orig)
  194. def get_window_extent(self, renderer=None):
  195. if renderer is None:
  196. renderer = self.figure._get_renderer()
  197. # save original and adjust some properties
  198. tr = self.get_transform()
  199. angle_orig = self.get_rotation()
  200. theta = np.deg2rad(self._offset_ref_angle)
  201. dd = self._offset_radius
  202. dx, dy = dd * np.cos(theta), dd * np.sin(theta)
  203. self.set_transform(tr + Affine2D().translate(dx, dy))
  204. self.set_rotation(self._text_ref_angle + angle_orig)
  205. bbox = super().get_window_extent(renderer).frozen()
  206. # restore original properties
  207. self.set_transform(tr)
  208. self.set_rotation(angle_orig)
  209. return bbox
  210. class AxisLabel(AttributeCopier, LabelBase):
  211. """
  212. Axis label. Derived from `.Text`. The position of the text is updated
  213. in the fly, so changing text position has no effect. Otherwise, the
  214. properties can be changed as a normal `.Text`.
  215. To change the pad between tick labels and axis label, use `set_pad`.
  216. """
  217. def __init__(self, *args, axis_direction="bottom", axis=None, **kwargs):
  218. self._axis = axis
  219. self._pad = 5
  220. self._external_pad = 0 # in pixels
  221. LabelBase.__init__(self, *args, **kwargs)
  222. self.set_axis_direction(axis_direction)
  223. def set_pad(self, pad):
  224. """
  225. Set the internal pad in points.
  226. The actual pad will be the sum of the internal pad and the
  227. external pad (the latter is set automatically by the `.AxisArtist`).
  228. Parameters
  229. ----------
  230. pad : float
  231. The internal pad in points.
  232. """
  233. self._pad = pad
  234. def get_pad(self):
  235. """
  236. Return the internal pad in points.
  237. See `.set_pad` for more details.
  238. """
  239. return self._pad
  240. def get_ref_artist(self):
  241. # docstring inherited
  242. return self._axis.get_label()
  243. def get_text(self):
  244. # docstring inherited
  245. t = super().get_text()
  246. if t == "__from_axes__":
  247. return self._axis.get_label().get_text()
  248. return self._text
  249. _default_alignments = dict(left=("bottom", "center"),
  250. right=("top", "center"),
  251. bottom=("top", "center"),
  252. top=("bottom", "center"))
  253. def set_default_alignment(self, d):
  254. """
  255. Set the default alignment. See `set_axis_direction` for details.
  256. Parameters
  257. ----------
  258. d : {"left", "bottom", "right", "top"}
  259. """
  260. va, ha = _api.check_getitem(self._default_alignments, d=d)
  261. self.set_va(va)
  262. self.set_ha(ha)
  263. _default_angles = dict(left=180,
  264. right=0,
  265. bottom=0,
  266. top=180)
  267. def set_default_angle(self, d):
  268. """
  269. Set the default angle. See `set_axis_direction` for details.
  270. Parameters
  271. ----------
  272. d : {"left", "bottom", "right", "top"}
  273. """
  274. self.set_rotation(_api.check_getitem(self._default_angles, d=d))
  275. def set_axis_direction(self, d):
  276. """
  277. Adjust the text angle and text alignment of axis label
  278. according to the matplotlib convention.
  279. ===================== ========== ========= ========== ==========
  280. Property left bottom right top
  281. ===================== ========== ========= ========== ==========
  282. axislabel angle 180 0 0 180
  283. axislabel va center top center bottom
  284. axislabel ha right center right center
  285. ===================== ========== ========= ========== ==========
  286. Note that the text angles are actually relative to (90 + angle
  287. of the direction to the ticklabel), which gives 0 for bottom
  288. axis.
  289. Parameters
  290. ----------
  291. d : {"left", "bottom", "right", "top"}
  292. """
  293. self.set_default_alignment(d)
  294. self.set_default_angle(d)
  295. def get_color(self):
  296. return self.get_attribute_from_ref_artist("color")
  297. def draw(self, renderer):
  298. if not self.get_visible():
  299. return
  300. self._offset_radius = \
  301. self._external_pad + renderer.points_to_pixels(self.get_pad())
  302. super().draw(renderer)
  303. def get_window_extent(self, renderer=None):
  304. if renderer is None:
  305. renderer = self.figure._get_renderer()
  306. if not self.get_visible():
  307. return
  308. r = self._external_pad + renderer.points_to_pixels(self.get_pad())
  309. self._offset_radius = r
  310. bb = super().get_window_extent(renderer)
  311. return bb
  312. class TickLabels(AxisLabel): # mtext.Text
  313. """
  314. Tick labels. While derived from `.Text`, this single artist draws all
  315. ticklabels. As in `.AxisLabel`, the position of the text is updated
  316. in the fly, so changing text position has no effect. Otherwise,
  317. the properties can be changed as a normal `.Text`. Unlike the
  318. ticklabels of the mainline Matplotlib, properties of a single
  319. ticklabel alone cannot be modified.
  320. To change the pad between ticks and ticklabels, use `~.AxisLabel.set_pad`.
  321. """
  322. def __init__(self, *, axis_direction="bottom", **kwargs):
  323. super().__init__(**kwargs)
  324. self.set_axis_direction(axis_direction)
  325. self._axislabel_pad = 0
  326. def get_ref_artist(self):
  327. # docstring inherited
  328. return self._axis.get_ticklabels()[0]
  329. def set_axis_direction(self, label_direction):
  330. """
  331. Adjust the text angle and text alignment of ticklabels
  332. according to the Matplotlib convention.
  333. The *label_direction* must be one of [left, right, bottom, top].
  334. ===================== ========== ========= ========== ==========
  335. Property left bottom right top
  336. ===================== ========== ========= ========== ==========
  337. ticklabel angle 90 0 -90 180
  338. ticklabel va center baseline center baseline
  339. ticklabel ha right center right center
  340. ===================== ========== ========= ========== ==========
  341. Note that the text angles are actually relative to (90 + angle
  342. of the direction to the ticklabel), which gives 0 for bottom
  343. axis.
  344. Parameters
  345. ----------
  346. label_direction : {"left", "bottom", "right", "top"}
  347. """
  348. self.set_default_alignment(label_direction)
  349. self.set_default_angle(label_direction)
  350. self._axis_direction = label_direction
  351. def invert_axis_direction(self):
  352. label_direction = self._get_opposite_direction(self._axis_direction)
  353. self.set_axis_direction(label_direction)
  354. def _get_ticklabels_offsets(self, renderer, label_direction):
  355. """
  356. Calculate the ticklabel offsets from the tick and their total heights.
  357. The offset only takes account the offset due to the vertical alignment
  358. of the ticklabels: if axis direction is bottom and va is 'top', it will
  359. return 0; if va is 'baseline', it will return (height-descent).
  360. """
  361. whd_list = self.get_texts_widths_heights_descents(renderer)
  362. if not whd_list:
  363. return 0, 0
  364. r = 0
  365. va, ha = self.get_va(), self.get_ha()
  366. if label_direction == "left":
  367. pad = max(w for w, h, d in whd_list)
  368. if ha == "left":
  369. r = pad
  370. elif ha == "center":
  371. r = .5 * pad
  372. elif label_direction == "right":
  373. pad = max(w for w, h, d in whd_list)
  374. if ha == "right":
  375. r = pad
  376. elif ha == "center":
  377. r = .5 * pad
  378. elif label_direction == "bottom":
  379. pad = max(h for w, h, d in whd_list)
  380. if va == "bottom":
  381. r = pad
  382. elif va == "center":
  383. r = .5 * pad
  384. elif va == "baseline":
  385. max_ascent = max(h - d for w, h, d in whd_list)
  386. max_descent = max(d for w, h, d in whd_list)
  387. r = max_ascent
  388. pad = max_ascent + max_descent
  389. elif label_direction == "top":
  390. pad = max(h for w, h, d in whd_list)
  391. if va == "top":
  392. r = pad
  393. elif va == "center":
  394. r = .5 * pad
  395. elif va == "baseline":
  396. max_ascent = max(h - d for w, h, d in whd_list)
  397. max_descent = max(d for w, h, d in whd_list)
  398. r = max_descent
  399. pad = max_ascent + max_descent
  400. # r : offset
  401. # pad : total height of the ticklabels. This will be used to
  402. # calculate the pad for the axislabel.
  403. return r, pad
  404. _default_alignments = dict(left=("center", "right"),
  405. right=("center", "left"),
  406. bottom=("baseline", "center"),
  407. top=("baseline", "center"))
  408. _default_angles = dict(left=90,
  409. right=-90,
  410. bottom=0,
  411. top=180)
  412. def draw(self, renderer):
  413. if not self.get_visible():
  414. self._axislabel_pad = self._external_pad
  415. return
  416. r, total_width = self._get_ticklabels_offsets(renderer,
  417. self._axis_direction)
  418. pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
  419. self._offset_radius = r + pad
  420. for (x, y), a, l in self._locs_angles_labels:
  421. if not l.strip():
  422. continue
  423. self._ref_angle = a
  424. self.set_x(x)
  425. self.set_y(y)
  426. self.set_text(l)
  427. LabelBase.draw(self, renderer)
  428. # the value saved will be used to draw axislabel.
  429. self._axislabel_pad = total_width + pad
  430. def set_locs_angles_labels(self, locs_angles_labels):
  431. self._locs_angles_labels = locs_angles_labels
  432. def get_window_extents(self, renderer=None):
  433. if renderer is None:
  434. renderer = self.figure._get_renderer()
  435. if not self.get_visible():
  436. self._axislabel_pad = self._external_pad
  437. return []
  438. bboxes = []
  439. r, total_width = self._get_ticklabels_offsets(renderer,
  440. self._axis_direction)
  441. pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
  442. self._offset_radius = r + pad
  443. for (x, y), a, l in self._locs_angles_labels:
  444. self._ref_angle = a
  445. self.set_x(x)
  446. self.set_y(y)
  447. self.set_text(l)
  448. bb = LabelBase.get_window_extent(self, renderer)
  449. bboxes.append(bb)
  450. # the value saved will be used to draw axislabel.
  451. self._axislabel_pad = total_width + pad
  452. return bboxes
  453. def get_texts_widths_heights_descents(self, renderer):
  454. """
  455. Return a list of ``(width, height, descent)`` tuples for ticklabels.
  456. Empty labels are left out.
  457. """
  458. whd_list = []
  459. for _loc, _angle, label in self._locs_angles_labels:
  460. if not label.strip():
  461. continue
  462. clean_line, ismath = self._preprocess_math(label)
  463. whd = renderer.get_text_width_height_descent(
  464. clean_line, self._fontproperties, ismath=ismath)
  465. whd_list.append(whd)
  466. return whd_list
  467. class GridlinesCollection(LineCollection):
  468. def __init__(self, *args, which="major", axis="both", **kwargs):
  469. """
  470. Collection of grid lines.
  471. Parameters
  472. ----------
  473. which : {"major", "minor"}
  474. Which grid to consider.
  475. axis : {"both", "x", "y"}
  476. Which axis to consider.
  477. *args, **kwargs
  478. Passed to `.LineCollection`.
  479. """
  480. self._which = which
  481. self._axis = axis
  482. super().__init__(*args, **kwargs)
  483. self.set_grid_helper(None)
  484. def set_which(self, which):
  485. """
  486. Select major or minor grid lines.
  487. Parameters
  488. ----------
  489. which : {"major", "minor"}
  490. """
  491. self._which = which
  492. def set_axis(self, axis):
  493. """
  494. Select axis.
  495. Parameters
  496. ----------
  497. axis : {"both", "x", "y"}
  498. """
  499. self._axis = axis
  500. def set_grid_helper(self, grid_helper):
  501. """
  502. Set grid helper.
  503. Parameters
  504. ----------
  505. grid_helper : `.GridHelperBase` subclass
  506. """
  507. self._grid_helper = grid_helper
  508. def draw(self, renderer):
  509. if self._grid_helper is not None:
  510. self._grid_helper.update_lim(self.axes)
  511. gl = self._grid_helper.get_gridlines(self._which, self._axis)
  512. self.set_segments([np.transpose(l) for l in gl])
  513. super().draw(renderer)
  514. class AxisArtist(martist.Artist):
  515. """
  516. An artist which draws axis (a line along which the n-th axes coord
  517. is constant) line, ticks, tick labels, and axis label.
  518. """
  519. zorder = 2.5
  520. @property
  521. def LABELPAD(self):
  522. return self.label.get_pad()
  523. @LABELPAD.setter
  524. def LABELPAD(self, v):
  525. self.label.set_pad(v)
  526. def __init__(self, axes,
  527. helper,
  528. offset=None,
  529. axis_direction="bottom",
  530. **kwargs):
  531. """
  532. Parameters
  533. ----------
  534. axes : `mpl_toolkits.axisartist.axislines.Axes`
  535. helper : `~mpl_toolkits.axisartist.axislines.AxisArtistHelper`
  536. """
  537. # axes is also used to follow the axis attribute (tick color, etc).
  538. super().__init__(**kwargs)
  539. self.axes = axes
  540. self._axis_artist_helper = helper
  541. if offset is None:
  542. offset = (0, 0)
  543. self.offset_transform = ScaledTranslation(
  544. *offset,
  545. Affine2D().scale(1 / 72) # points to inches.
  546. + self.axes.figure.dpi_scale_trans)
  547. if axis_direction in ["left", "right"]:
  548. self.axis = axes.yaxis
  549. else:
  550. self.axis = axes.xaxis
  551. self._axisline_style = None
  552. self._axis_direction = axis_direction
  553. self._init_line()
  554. self._init_ticks(**kwargs)
  555. self._init_offsetText(axis_direction)
  556. self._init_label()
  557. # axis direction
  558. self._ticklabel_add_angle = 0.
  559. self._axislabel_add_angle = 0.
  560. self.set_axis_direction(axis_direction)
  561. # axis direction
  562. def set_axis_direction(self, axis_direction):
  563. """
  564. Adjust the direction, text angle, and text alignment of tick labels
  565. and axis labels following the Matplotlib convention for the rectangle
  566. axes.
  567. The *axis_direction* must be one of [left, right, bottom, top].
  568. ===================== ========== ========= ========== ==========
  569. Property left bottom right top
  570. ===================== ========== ========= ========== ==========
  571. ticklabel direction "-" "+" "+" "-"
  572. axislabel direction "-" "+" "+" "-"
  573. ticklabel angle 90 0 -90 180
  574. ticklabel va center baseline center baseline
  575. ticklabel ha right center right center
  576. axislabel angle 180 0 0 180
  577. axislabel va center top center bottom
  578. axislabel ha right center right center
  579. ===================== ========== ========= ========== ==========
  580. Note that the direction "+" and "-" are relative to the direction of
  581. the increasing coordinate. Also, the text angles are actually
  582. relative to (90 + angle of the direction to the ticklabel),
  583. which gives 0 for bottom axis.
  584. Parameters
  585. ----------
  586. axis_direction : {"left", "bottom", "right", "top"}
  587. """
  588. self.major_ticklabels.set_axis_direction(axis_direction)
  589. self.label.set_axis_direction(axis_direction)
  590. self._axis_direction = axis_direction
  591. if axis_direction in ["left", "top"]:
  592. self.set_ticklabel_direction("-")
  593. self.set_axislabel_direction("-")
  594. else:
  595. self.set_ticklabel_direction("+")
  596. self.set_axislabel_direction("+")
  597. def set_ticklabel_direction(self, tick_direction):
  598. r"""
  599. Adjust the direction of the tick labels.
  600. Note that the *tick_direction*\s '+' and '-' are relative to the
  601. direction of the increasing coordinate.
  602. Parameters
  603. ----------
  604. tick_direction : {"+", "-"}
  605. """
  606. self._ticklabel_add_angle = _api.check_getitem(
  607. {"+": 0, "-": 180}, tick_direction=tick_direction)
  608. def invert_ticklabel_direction(self):
  609. self._ticklabel_add_angle = (self._ticklabel_add_angle + 180) % 360
  610. self.major_ticklabels.invert_axis_direction()
  611. self.minor_ticklabels.invert_axis_direction()
  612. def set_axislabel_direction(self, label_direction):
  613. r"""
  614. Adjust the direction of the axis label.
  615. Note that the *label_direction*\s '+' and '-' are relative to the
  616. direction of the increasing coordinate.
  617. Parameters
  618. ----------
  619. label_direction : {"+", "-"}
  620. """
  621. self._axislabel_add_angle = _api.check_getitem(
  622. {"+": 0, "-": 180}, label_direction=label_direction)
  623. def get_transform(self):
  624. return self.axes.transAxes + self.offset_transform
  625. def get_helper(self):
  626. """
  627. Return axis artist helper instance.
  628. """
  629. return self._axis_artist_helper
  630. def set_axisline_style(self, axisline_style=None, **kwargs):
  631. """
  632. Set the axisline style.
  633. The new style is completely defined by the passed attributes. Existing
  634. style attributes are forgotten.
  635. Parameters
  636. ----------
  637. axisline_style : str or None
  638. The line style, e.g. '->', optionally followed by a comma-separated
  639. list of attributes. Alternatively, the attributes can be provided
  640. as keywords.
  641. If *None* this returns a string containing the available styles.
  642. Examples
  643. --------
  644. The following two commands are equal:
  645. >>> set_axisline_style("->,size=1.5")
  646. >>> set_axisline_style("->", size=1.5)
  647. """
  648. if axisline_style is None:
  649. return AxislineStyle.pprint_styles()
  650. if isinstance(axisline_style, AxislineStyle._Base):
  651. self._axisline_style = axisline_style
  652. else:
  653. self._axisline_style = AxislineStyle(axisline_style, **kwargs)
  654. self._init_line()
  655. def get_axisline_style(self):
  656. """Return the current axisline style."""
  657. return self._axisline_style
  658. def _init_line(self):
  659. """
  660. Initialize the *line* artist that is responsible to draw the axis line.
  661. """
  662. tran = (self._axis_artist_helper.get_line_transform(self.axes)
  663. + self.offset_transform)
  664. axisline_style = self.get_axisline_style()
  665. if axisline_style is None:
  666. self.line = PathPatch(
  667. self._axis_artist_helper.get_line(self.axes),
  668. color=mpl.rcParams['axes.edgecolor'],
  669. fill=False,
  670. linewidth=mpl.rcParams['axes.linewidth'],
  671. capstyle=mpl.rcParams['lines.solid_capstyle'],
  672. joinstyle=mpl.rcParams['lines.solid_joinstyle'],
  673. transform=tran)
  674. else:
  675. self.line = axisline_style(self, transform=tran)
  676. def _draw_line(self, renderer):
  677. self.line.set_path(self._axis_artist_helper.get_line(self.axes))
  678. if self.get_axisline_style() is not None:
  679. self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
  680. self.line.draw(renderer)
  681. def _init_ticks(self, **kwargs):
  682. axis_name = self.axis.axis_name
  683. trans = (self._axis_artist_helper.get_tick_transform(self.axes)
  684. + self.offset_transform)
  685. self.major_ticks = Ticks(
  686. kwargs.get(
  687. "major_tick_size",
  688. mpl.rcParams[f"{axis_name}tick.major.size"]),
  689. axis=self.axis, transform=trans)
  690. self.minor_ticks = Ticks(
  691. kwargs.get(
  692. "minor_tick_size",
  693. mpl.rcParams[f"{axis_name}tick.minor.size"]),
  694. axis=self.axis, transform=trans)
  695. size = mpl.rcParams[f"{axis_name}tick.labelsize"]
  696. self.major_ticklabels = TickLabels(
  697. axis=self.axis,
  698. axis_direction=self._axis_direction,
  699. figure=self.axes.figure,
  700. transform=trans,
  701. fontsize=size,
  702. pad=kwargs.get(
  703. "major_tick_pad", mpl.rcParams[f"{axis_name}tick.major.pad"]),
  704. )
  705. self.minor_ticklabels = TickLabels(
  706. axis=self.axis,
  707. axis_direction=self._axis_direction,
  708. figure=self.axes.figure,
  709. transform=trans,
  710. fontsize=size,
  711. pad=kwargs.get(
  712. "minor_tick_pad", mpl.rcParams[f"{axis_name}tick.minor.pad"]),
  713. )
  714. def _get_tick_info(self, tick_iter):
  715. """
  716. Return a pair of:
  717. - list of locs and angles for ticks
  718. - list of locs, angles and labels for ticklabels.
  719. """
  720. ticks_loc_angle = []
  721. ticklabels_loc_angle_label = []
  722. ticklabel_add_angle = self._ticklabel_add_angle
  723. for loc, angle_normal, angle_tangent, label in tick_iter:
  724. angle_label = angle_tangent - 90 + ticklabel_add_angle
  725. angle_tick = (angle_normal
  726. if 90 <= (angle_label - angle_normal) % 360 <= 270
  727. else angle_normal + 180)
  728. ticks_loc_angle.append([loc, angle_tick])
  729. ticklabels_loc_angle_label.append([loc, angle_label, label])
  730. return ticks_loc_angle, ticklabels_loc_angle_label
  731. def _update_ticks(self, renderer=None):
  732. # set extra pad for major and minor ticklabels: use ticksize of
  733. # majorticks even for minor ticks. not clear what is best.
  734. if renderer is None:
  735. renderer = self.figure._get_renderer()
  736. dpi_cor = renderer.points_to_pixels(1.)
  737. if self.major_ticks.get_visible() and self.major_ticks.get_tick_out():
  738. ticklabel_pad = self.major_ticks._ticksize * dpi_cor
  739. self.major_ticklabels._external_pad = ticklabel_pad
  740. self.minor_ticklabels._external_pad = ticklabel_pad
  741. else:
  742. self.major_ticklabels._external_pad = 0
  743. self.minor_ticklabels._external_pad = 0
  744. majortick_iter, minortick_iter = \
  745. self._axis_artist_helper.get_tick_iterators(self.axes)
  746. tick_loc_angle, ticklabel_loc_angle_label = \
  747. self._get_tick_info(majortick_iter)
  748. self.major_ticks.set_locs_angles(tick_loc_angle)
  749. self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
  750. tick_loc_angle, ticklabel_loc_angle_label = \
  751. self._get_tick_info(minortick_iter)
  752. self.minor_ticks.set_locs_angles(tick_loc_angle)
  753. self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
  754. def _draw_ticks(self, renderer):
  755. self._update_ticks(renderer)
  756. self.major_ticks.draw(renderer)
  757. self.major_ticklabels.draw(renderer)
  758. self.minor_ticks.draw(renderer)
  759. self.minor_ticklabels.draw(renderer)
  760. if (self.major_ticklabels.get_visible()
  761. or self.minor_ticklabels.get_visible()):
  762. self._draw_offsetText(renderer)
  763. _offsetText_pos = dict(left=(0, 1, "bottom", "right"),
  764. right=(1, 1, "bottom", "left"),
  765. bottom=(1, 0, "top", "right"),
  766. top=(1, 1, "bottom", "right"))
  767. def _init_offsetText(self, direction):
  768. x, y, va, ha = self._offsetText_pos[direction]
  769. self.offsetText = mtext.Annotation(
  770. "",
  771. xy=(x, y), xycoords="axes fraction",
  772. xytext=(0, 0), textcoords="offset points",
  773. color=mpl.rcParams['xtick.color'],
  774. horizontalalignment=ha, verticalalignment=va,
  775. )
  776. self.offsetText.set_transform(IdentityTransform())
  777. self.axes._set_artist_props(self.offsetText)
  778. def _update_offsetText(self):
  779. self.offsetText.set_text(self.axis.major.formatter.get_offset())
  780. self.offsetText.set_size(self.major_ticklabels.get_size())
  781. offset = (self.major_ticklabels.get_pad()
  782. + self.major_ticklabels.get_size()
  783. + 2)
  784. self.offsetText.xyann = (0, offset)
  785. def _draw_offsetText(self, renderer):
  786. self._update_offsetText()
  787. self.offsetText.draw(renderer)
  788. def _init_label(self, **kwargs):
  789. tr = (self._axis_artist_helper.get_axislabel_transform(self.axes)
  790. + self.offset_transform)
  791. self.label = AxisLabel(
  792. 0, 0, "__from_axes__",
  793. color="auto",
  794. fontsize=kwargs.get("labelsize", mpl.rcParams['axes.labelsize']),
  795. fontweight=mpl.rcParams['axes.labelweight'],
  796. axis=self.axis,
  797. transform=tr,
  798. axis_direction=self._axis_direction,
  799. )
  800. self.label.set_figure(self.axes.figure)
  801. labelpad = kwargs.get("labelpad", 5)
  802. self.label.set_pad(labelpad)
  803. def _update_label(self, renderer):
  804. if not self.label.get_visible():
  805. return
  806. if self._ticklabel_add_angle != self._axislabel_add_angle:
  807. if ((self.major_ticks.get_visible()
  808. and not self.major_ticks.get_tick_out())
  809. or (self.minor_ticks.get_visible()
  810. and not self.major_ticks.get_tick_out())):
  811. axislabel_pad = self.major_ticks._ticksize
  812. else:
  813. axislabel_pad = 0
  814. else:
  815. axislabel_pad = max(self.major_ticklabels._axislabel_pad,
  816. self.minor_ticklabels._axislabel_pad)
  817. self.label._external_pad = axislabel_pad
  818. xy, angle_tangent = \
  819. self._axis_artist_helper.get_axislabel_pos_angle(self.axes)
  820. if xy is None:
  821. return
  822. angle_label = angle_tangent - 90
  823. x, y = xy
  824. self.label._ref_angle = angle_label + self._axislabel_add_angle
  825. self.label.set(x=x, y=y)
  826. def _draw_label(self, renderer):
  827. self._update_label(renderer)
  828. self.label.draw(renderer)
  829. def set_label(self, s):
  830. # docstring inherited
  831. self.label.set_text(s)
  832. def get_tightbbox(self, renderer=None):
  833. if not self.get_visible():
  834. return
  835. self._axis_artist_helper.update_lim(self.axes)
  836. self._update_ticks(renderer)
  837. self._update_label(renderer)
  838. self.line.set_path(self._axis_artist_helper.get_line(self.axes))
  839. if self.get_axisline_style() is not None:
  840. self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
  841. bb = [
  842. *self.major_ticklabels.get_window_extents(renderer),
  843. *self.minor_ticklabels.get_window_extents(renderer),
  844. self.label.get_window_extent(renderer),
  845. self.offsetText.get_window_extent(renderer),
  846. self.line.get_window_extent(renderer),
  847. ]
  848. bb = [b for b in bb if b and (b.width != 0 or b.height != 0)]
  849. if bb:
  850. _bbox = Bbox.union(bb)
  851. return _bbox
  852. else:
  853. return None
  854. @martist.allow_rasterization
  855. def draw(self, renderer):
  856. # docstring inherited
  857. if not self.get_visible():
  858. return
  859. renderer.open_group(__name__, gid=self.get_gid())
  860. self._axis_artist_helper.update_lim(self.axes)
  861. self._draw_ticks(renderer)
  862. self._draw_line(renderer)
  863. self._draw_label(renderer)
  864. renderer.close_group(__name__)
  865. def toggle(self, all=None, ticks=None, ticklabels=None, label=None):
  866. """
  867. Toggle visibility of ticks, ticklabels, and (axis) label.
  868. To turn all off, ::
  869. axis.toggle(all=False)
  870. To turn all off but ticks on ::
  871. axis.toggle(all=False, ticks=True)
  872. To turn all on but (axis) label off ::
  873. axis.toggle(all=True, label=False)
  874. """
  875. if all:
  876. _ticks, _ticklabels, _label = True, True, True
  877. elif all is not None:
  878. _ticks, _ticklabels, _label = False, False, False
  879. else:
  880. _ticks, _ticklabels, _label = None, None, None
  881. if ticks is not None:
  882. _ticks = ticks
  883. if ticklabels is not None:
  884. _ticklabels = ticklabels
  885. if label is not None:
  886. _label = label
  887. if _ticks is not None:
  888. self.major_ticks.set_visible(_ticks)
  889. self.minor_ticks.set_visible(_ticks)
  890. if _ticklabels is not None:
  891. self.major_ticklabels.set_visible(_ticklabels)
  892. self.minor_ticklabels.set_visible(_ticklabels)
  893. if _label is not None:
  894. self.label.set_visible(_label)