You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2436 lines
80 KiB

4 years ago
  1. """
  2. Classes for including text in a figure.
  3. """
  4. import contextlib
  5. import logging
  6. import math
  7. import warnings
  8. import weakref
  9. import numpy as np
  10. from . import artist, cbook, docstring, rcParams
  11. from .artist import Artist
  12. from .font_manager import FontProperties
  13. from .lines import Line2D
  14. from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle
  15. from .textpath import TextPath # Unused, but imported by others.
  16. from .transforms import (
  17. Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform)
  18. _log = logging.getLogger(__name__)
  19. def _process_text_args(override, fontdict=None, **kwargs):
  20. """Return an override dict. See `~pyplot.text' docstring for info."""
  21. if fontdict is not None:
  22. override.update(fontdict)
  23. override.update(kwargs)
  24. return override
  25. @contextlib.contextmanager
  26. def _wrap_text(textobj):
  27. """Temporarily inserts newlines to the text if the wrap option is enabled.
  28. """
  29. if textobj.get_wrap():
  30. old_text = textobj.get_text()
  31. try:
  32. textobj.set_text(textobj._get_wrapped_text())
  33. yield textobj
  34. finally:
  35. textobj.set_text(old_text)
  36. else:
  37. yield textobj
  38. # Extracted from Text's method to serve as a function
  39. def get_rotation(rotation):
  40. """
  41. Return the text angle as float between 0 and 360 degrees.
  42. *rotation* may be 'horizontal', 'vertical', or a numeric value in degrees.
  43. """
  44. try:
  45. return float(rotation) % 360
  46. except (ValueError, TypeError):
  47. if cbook._str_equal(rotation, 'horizontal') or rotation is None:
  48. return 0.
  49. elif cbook._str_equal(rotation, 'vertical'):
  50. return 90.
  51. else:
  52. raise ValueError("rotation is {!r}; expected either 'horizontal', "
  53. "'vertical', numeric value, or None"
  54. .format(rotation))
  55. def _get_textbox(text, renderer):
  56. """
  57. Calculate the bounding box of the text. Unlike
  58. :meth:`matplotlib.text.Text.get_extents` method, The bbox size of
  59. the text before the rotation is calculated.
  60. """
  61. # TODO : This function may move into the Text class as a method. As a
  62. # matter of fact, The information from the _get_textbox function
  63. # should be available during the Text._get_layout() call, which is
  64. # called within the _get_textbox. So, it would better to move this
  65. # function as a method with some refactoring of _get_layout method.
  66. projected_xs = []
  67. projected_ys = []
  68. theta = np.deg2rad(text.get_rotation())
  69. tr = Affine2D().rotate(-theta)
  70. _, parts, d = text._get_layout(renderer)
  71. for t, wh, x, y in parts:
  72. w, h = wh
  73. xt1, yt1 = tr.transform_point((x, y))
  74. yt1 -= d
  75. xt2, yt2 = xt1 + w, yt1 + h
  76. projected_xs.extend([xt1, xt2])
  77. projected_ys.extend([yt1, yt2])
  78. xt_box, yt_box = min(projected_xs), min(projected_ys)
  79. w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
  80. x_box, y_box = Affine2D().rotate(theta).transform_point((xt_box, yt_box))
  81. return x_box, y_box, w_box, h_box
  82. @cbook._define_aliases({
  83. "fontfamily": ["family"],
  84. "fontproperties": ["font_properties"],
  85. "horizontalalignment": ["ha"],
  86. "multialignment": ["ma"],
  87. "fontname": ["name"],
  88. "fontsize": ["size"],
  89. "fontstretch": ["stretch"],
  90. "fontstyle": ["style"],
  91. "fontvariant": ["variant"],
  92. "verticalalignment": ["va"],
  93. "fontweight": ["weight"],
  94. })
  95. class Text(Artist):
  96. """Handle storing and drawing of text in window or data coordinates."""
  97. zorder = 3
  98. _cached = cbook.maxdict(50)
  99. def __repr__(self):
  100. return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text))
  101. def __init__(self,
  102. x=0, y=0, text='',
  103. color=None, # defaults to rc params
  104. verticalalignment='baseline',
  105. horizontalalignment='left',
  106. multialignment=None,
  107. fontproperties=None, # defaults to FontProperties()
  108. rotation=None,
  109. linespacing=None,
  110. rotation_mode=None,
  111. usetex=None, # defaults to rcParams['text.usetex']
  112. wrap=False,
  113. **kwargs
  114. ):
  115. """
  116. Create a `.Text` instance at *x*, *y* with string *text*.
  117. Valid kwargs are
  118. %(Text)s
  119. """
  120. Artist.__init__(self)
  121. self._x, self._y = x, y
  122. if color is None:
  123. color = rcParams['text.color']
  124. if fontproperties is None:
  125. fontproperties = FontProperties()
  126. elif isinstance(fontproperties, str):
  127. fontproperties = FontProperties(fontproperties)
  128. self._text = ''
  129. self.set_text(text)
  130. self.set_color(color)
  131. self.set_usetex(usetex)
  132. self.set_wrap(wrap)
  133. self.set_verticalalignment(verticalalignment)
  134. self.set_horizontalalignment(horizontalalignment)
  135. self._multialignment = multialignment
  136. self._rotation = rotation
  137. self._fontproperties = fontproperties
  138. self._bbox_patch = None # a FancyBboxPatch instance
  139. self._renderer = None
  140. if linespacing is None:
  141. linespacing = 1.2 # Maybe use rcParam later.
  142. self._linespacing = linespacing
  143. self.set_rotation_mode(rotation_mode)
  144. self.update(kwargs)
  145. def update(self, kwargs):
  146. """
  147. Update properties from a dictionary.
  148. """
  149. # Update bbox last, as it depends on font properties.
  150. sentinel = object() # bbox can be None, so use another sentinel.
  151. bbox = kwargs.pop("bbox", sentinel)
  152. super().update(kwargs)
  153. if bbox is not sentinel:
  154. self.set_bbox(bbox)
  155. def __getstate__(self):
  156. d = super().__getstate__()
  157. # remove the cached _renderer (if it exists)
  158. d['_renderer'] = None
  159. return d
  160. def contains(self, mouseevent):
  161. """Test whether the mouse event occurred in the patch.
  162. In the case of text, a hit is true anywhere in the
  163. axis-aligned bounding-box containing the text.
  164. Returns
  165. -------
  166. bool : bool
  167. """
  168. if callable(self._contains):
  169. return self._contains(self, mouseevent)
  170. if not self.get_visible() or self._renderer is None:
  171. return False, {}
  172. l, b, w, h = self.get_window_extent().bounds
  173. r, t = l + w, b + h
  174. x, y = mouseevent.x, mouseevent.y
  175. inside = (l <= x <= r and b <= y <= t)
  176. cattr = {}
  177. # if the text has a surrounding patch, also check containment for it,
  178. # and merge the results with the results for the text.
  179. if self._bbox_patch:
  180. patch_inside, patch_cattr = self._bbox_patch.contains(mouseevent)
  181. inside = inside or patch_inside
  182. cattr["bbox_patch"] = patch_cattr
  183. return inside, cattr
  184. def _get_xy_display(self):
  185. """
  186. Get the (possibly unit converted) transformed x, y in display coords.
  187. """
  188. x, y = self.get_unitless_position()
  189. return self.get_transform().transform_point((x, y))
  190. def _get_multialignment(self):
  191. if self._multialignment is not None:
  192. return self._multialignment
  193. else:
  194. return self._horizontalalignment
  195. def get_rotation(self):
  196. """Return the text angle as float in degrees."""
  197. return get_rotation(self._rotation) # string_or_number -> number
  198. def set_rotation_mode(self, m):
  199. """
  200. Set text rotation mode.
  201. Parameters
  202. ----------
  203. m : {None, 'default', 'anchor'}
  204. If ``None`` or ``"default"``, the text will be first rotated, then
  205. aligned according to their horizontal and vertical alignments. If
  206. ``"anchor"``, then alignment occurs before rotation.
  207. """
  208. if m is None or m in ["anchor", "default"]:
  209. self._rotation_mode = m
  210. else:
  211. raise ValueError("Unknown rotation_mode : %s" % repr(m))
  212. self.stale = True
  213. def get_rotation_mode(self):
  214. """Get the text rotation mode."""
  215. return self._rotation_mode
  216. def update_from(self, other):
  217. """Copy properties from other to self."""
  218. Artist.update_from(self, other)
  219. self._color = other._color
  220. self._multialignment = other._multialignment
  221. self._verticalalignment = other._verticalalignment
  222. self._horizontalalignment = other._horizontalalignment
  223. self._fontproperties = other._fontproperties.copy()
  224. self._rotation = other._rotation
  225. self._picker = other._picker
  226. self._linespacing = other._linespacing
  227. self.stale = True
  228. def _get_layout(self, renderer):
  229. """
  230. return the extent (bbox) of the text together with
  231. multiple-alignment information. Note that it returns an extent
  232. of a rotated text when necessary.
  233. """
  234. key = self.get_prop_tup(renderer=renderer)
  235. if key in self._cached:
  236. return self._cached[key]
  237. horizLayout = []
  238. thisx, thisy = 0.0, 0.0
  239. xmin, ymin = 0.0, 0.0
  240. width, height = 0.0, 0.0
  241. lines = self.get_text().split('\n')
  242. whs = np.zeros((len(lines), 2))
  243. horizLayout = np.zeros((len(lines), 4))
  244. # Find full vertical extent of font,
  245. # including ascenders and descenders:
  246. tmp, lp_h, lp_bl = renderer.get_text_width_height_descent('lp',
  247. self._fontproperties,
  248. ismath=False)
  249. offsety = (lp_h - lp_bl) * self._linespacing
  250. baseline = 0
  251. for i, line in enumerate(lines):
  252. clean_line, ismath = self.is_math_text(line, self.get_usetex())
  253. if clean_line:
  254. w, h, d = renderer.get_text_width_height_descent(clean_line,
  255. self._fontproperties,
  256. ismath=ismath)
  257. else:
  258. w, h, d = 0, 0, 0
  259. # For multiline text, increase the line spacing when the
  260. # text net-height(excluding baseline) is larger than that
  261. # of a "l" (e.g., use of superscripts), which seems
  262. # what TeX does.
  263. h = max(h, lp_h)
  264. d = max(d, lp_bl)
  265. whs[i] = w, h
  266. baseline = (h - d) - thisy
  267. thisy -= max(offsety, (h - d) * self._linespacing)
  268. horizLayout[i] = thisx, thisy, w, h
  269. thisy -= d
  270. width = max(width, w)
  271. descent = d
  272. ymin = horizLayout[-1][1]
  273. ymax = horizLayout[0][1] + horizLayout[0][3]
  274. height = ymax - ymin
  275. xmax = xmin + width
  276. # get the rotation matrix
  277. M = Affine2D().rotate_deg(self.get_rotation())
  278. offsetLayout = np.zeros((len(lines), 2))
  279. offsetLayout[:] = horizLayout[:, 0:2]
  280. # now offset the individual text lines within the box
  281. if len(lines) > 1: # do the multiline aligment
  282. malign = self._get_multialignment()
  283. if malign == 'center':
  284. offsetLayout[:, 0] += width / 2.0 - horizLayout[:, 2] / 2.0
  285. elif malign == 'right':
  286. offsetLayout[:, 0] += width - horizLayout[:, 2]
  287. # the corners of the unrotated bounding box
  288. cornersHoriz = np.array(
  289. [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)], float)
  290. cornersHoriz[:, 1] -= descent
  291. # now rotate the bbox
  292. cornersRotated = M.transform(cornersHoriz)
  293. txs = cornersRotated[:, 0]
  294. tys = cornersRotated[:, 1]
  295. # compute the bounds of the rotated box
  296. xmin, xmax = txs.min(), txs.max()
  297. ymin, ymax = tys.min(), tys.max()
  298. width = xmax - xmin
  299. height = ymax - ymin
  300. # Now move the box to the target position offset the display
  301. # bbox by alignment
  302. halign = self._horizontalalignment
  303. valign = self._verticalalignment
  304. rotation_mode = self.get_rotation_mode()
  305. if rotation_mode != "anchor":
  306. # compute the text location in display coords and the offsets
  307. # necessary to align the bbox with that location
  308. if halign == 'center':
  309. offsetx = (xmin + width / 2.0)
  310. elif halign == 'right':
  311. offsetx = (xmin + width)
  312. else:
  313. offsetx = xmin
  314. if valign == 'center':
  315. offsety = (ymin + height / 2.0)
  316. elif valign == 'top':
  317. offsety = (ymin + height)
  318. elif valign == 'baseline':
  319. offsety = (ymin + height) - baseline
  320. elif valign == 'center_baseline':
  321. offsety = ymin + height - baseline / 2.0
  322. else:
  323. offsety = ymin
  324. else:
  325. xmin1, ymin1 = cornersHoriz[0]
  326. xmax1, ymax1 = cornersHoriz[2]
  327. if halign == 'center':
  328. offsetx = (xmin1 + xmax1) / 2.0
  329. elif halign == 'right':
  330. offsetx = xmax1
  331. else:
  332. offsetx = xmin1
  333. if valign == 'center':
  334. offsety = (ymin1 + ymax1) / 2.0
  335. elif valign == 'top':
  336. offsety = ymax1
  337. elif valign == 'baseline':
  338. offsety = ymax1 - baseline
  339. elif valign == 'center_baseline':
  340. offsety = (ymin1 + ymax1 - baseline) / 2.0
  341. else:
  342. offsety = ymin1
  343. offsetx, offsety = M.transform_point((offsetx, offsety))
  344. xmin -= offsetx
  345. ymin -= offsety
  346. bbox = Bbox.from_bounds(xmin, ymin, width, height)
  347. # now rotate the positions around the first x,y position
  348. xys = M.transform(offsetLayout)
  349. xys -= (offsetx, offsety)
  350. xs, ys = xys[:, 0], xys[:, 1]
  351. ret = bbox, list(zip(lines, whs, xs, ys)), descent
  352. self._cached[key] = ret
  353. return ret
  354. def set_bbox(self, rectprops):
  355. """
  356. Draw a bounding box around self.
  357. Parameters
  358. ----------
  359. rectprops : dict with properties for `.patches.FancyBboxPatch`
  360. The default boxstyle is 'square'. The mutation
  361. scale of the `.patches.FancyBboxPatch` is set to the fontsize.
  362. Examples
  363. --------
  364. ::
  365. t.set_bbox(dict(facecolor='red', alpha=0.5))
  366. """
  367. if rectprops is not None:
  368. props = rectprops.copy()
  369. boxstyle = props.pop("boxstyle", None)
  370. pad = props.pop("pad", None)
  371. if boxstyle is None:
  372. boxstyle = "square"
  373. if pad is None:
  374. pad = 4 # points
  375. pad /= self.get_size() # to fraction of font size
  376. else:
  377. if pad is None:
  378. pad = 0.3
  379. # boxstyle could be a callable or a string
  380. if isinstance(boxstyle, str) and "pad" not in boxstyle:
  381. boxstyle += ",pad=%0.2f" % pad
  382. bbox_transmuter = props.pop("bbox_transmuter", None)
  383. self._bbox_patch = FancyBboxPatch(
  384. (0., 0.),
  385. 1., 1.,
  386. boxstyle=boxstyle,
  387. bbox_transmuter=bbox_transmuter,
  388. transform=IdentityTransform(),
  389. **props)
  390. else:
  391. self._bbox_patch = None
  392. self._update_clip_properties()
  393. def get_bbox_patch(self):
  394. """
  395. Return the bbox Patch, or None if the `.patches.FancyBboxPatch`
  396. is not made.
  397. """
  398. return self._bbox_patch
  399. def update_bbox_position_size(self, renderer):
  400. """
  401. Update the location and the size of the bbox.
  402. This method should be used when the position and size of the bbox needs
  403. to be updated before actually drawing the bbox.
  404. """
  405. if self._bbox_patch:
  406. trans = self.get_transform()
  407. # don't use self.get_unitless_position here, which refers to text
  408. # position in Text, and dash position in TextWithDash:
  409. posx = float(self.convert_xunits(self._x))
  410. posy = float(self.convert_yunits(self._y))
  411. posx, posy = trans.transform_point((posx, posy))
  412. x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
  413. self._bbox_patch.set_bounds(0., 0., w_box, h_box)
  414. theta = np.deg2rad(self.get_rotation())
  415. tr = Affine2D().rotate(theta)
  416. tr = tr.translate(posx + x_box, posy + y_box)
  417. self._bbox_patch.set_transform(tr)
  418. fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
  419. self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
  420. def _draw_bbox(self, renderer, posx, posy):
  421. """
  422. Update the location and size of the bbox (`.patches.FancyBboxPatch`),
  423. and draw.
  424. """
  425. x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
  426. self._bbox_patch.set_bounds(0., 0., w_box, h_box)
  427. theta = np.deg2rad(self.get_rotation())
  428. tr = Affine2D().rotate(theta)
  429. tr = tr.translate(posx + x_box, posy + y_box)
  430. self._bbox_patch.set_transform(tr)
  431. fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
  432. self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
  433. self._bbox_patch.draw(renderer)
  434. def _update_clip_properties(self):
  435. clipprops = dict(clip_box=self.clipbox,
  436. clip_path=self._clippath,
  437. clip_on=self._clipon)
  438. if self._bbox_patch:
  439. bbox = self._bbox_patch.update(clipprops)
  440. def set_clip_box(self, clipbox):
  441. """
  442. Set the artist's clip `~.transforms.Bbox`.
  443. Parameters
  444. ----------
  445. clipbox : `matplotlib.transforms.Bbox`
  446. """
  447. super().set_clip_box(clipbox)
  448. self._update_clip_properties()
  449. def set_clip_path(self, path, transform=None):
  450. """
  451. Set the artist's clip path, which may be:
  452. * a `~matplotlib.patches.Patch` (or subclass) instance
  453. * a `~matplotlib.path.Path` instance, in which case
  454. an optional `~matplotlib.transforms.Transform`
  455. instance may be provided, which will be applied to the
  456. path before using it for clipping.
  457. * *None*, to remove the clipping path
  458. For efficiency, if the path happens to be an axis-aligned
  459. rectangle, this method will set the clipping box to the
  460. corresponding rectangle and set the clipping path to *None*.
  461. ACCEPTS: { (`.path.Path`, `.transforms.Transform`),
  462. `.patches.Patch`, None }
  463. """
  464. super().set_clip_path(path, transform)
  465. self._update_clip_properties()
  466. def set_clip_on(self, b):
  467. """
  468. Set whether artist uses clipping.
  469. When False, artists will be visible outside of the axes, which can lead
  470. to unexpected results.
  471. Parameters
  472. ----------
  473. b : bool
  474. """
  475. super().set_clip_on(b)
  476. self._update_clip_properties()
  477. def get_wrap(self):
  478. """Return the wrapping state for the text."""
  479. return self._wrap
  480. def set_wrap(self, wrap):
  481. """Set the wrapping state for the text.
  482. Parameters
  483. ----------
  484. wrap : bool
  485. """
  486. self._wrap = wrap
  487. def _get_wrap_line_width(self):
  488. """
  489. Return the maximum line width for wrapping text based on the current
  490. orientation.
  491. """
  492. x0, y0 = self.get_transform().transform(self.get_position())
  493. figure_box = self.get_figure().get_window_extent()
  494. # Calculate available width based on text alignment
  495. alignment = self.get_horizontalalignment()
  496. self.set_rotation_mode('anchor')
  497. rotation = self.get_rotation()
  498. left = self._get_dist_to_box(rotation, x0, y0, figure_box)
  499. right = self._get_dist_to_box(
  500. (180 + rotation) % 360, x0, y0, figure_box)
  501. if alignment == 'left':
  502. line_width = left
  503. elif alignment == 'right':
  504. line_width = right
  505. else:
  506. line_width = 2 * min(left, right)
  507. return line_width
  508. def _get_dist_to_box(self, rotation, x0, y0, figure_box):
  509. """
  510. Return the distance from the given points to the boundaries of a
  511. rotated box, in pixels.
  512. """
  513. if rotation > 270:
  514. quad = rotation - 270
  515. h1 = y0 / math.cos(math.radians(quad))
  516. h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad))
  517. elif rotation > 180:
  518. quad = rotation - 180
  519. h1 = x0 / math.cos(math.radians(quad))
  520. h2 = y0 / math.cos(math.radians(90 - quad))
  521. elif rotation > 90:
  522. quad = rotation - 90
  523. h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad))
  524. h2 = x0 / math.cos(math.radians(90 - quad))
  525. else:
  526. h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation))
  527. h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation))
  528. return min(h1, h2)
  529. def _get_rendered_text_width(self, text):
  530. """
  531. Return the width of a given text string, in pixels.
  532. """
  533. w, h, d = self._renderer.get_text_width_height_descent(
  534. text,
  535. self.get_fontproperties(),
  536. False)
  537. return math.ceil(w)
  538. def _get_wrapped_text(self):
  539. """
  540. Return a copy of the text with new lines added, so that
  541. the text is wrapped relative to the parent figure.
  542. """
  543. # Not fit to handle breaking up latex syntax correctly, so
  544. # ignore latex for now.
  545. if self.get_usetex():
  546. return self.get_text()
  547. # Build the line incrementally, for a more accurate measure of length
  548. line_width = self._get_wrap_line_width()
  549. wrapped_str = ""
  550. line = ""
  551. for word in self.get_text().split(' '):
  552. # New lines in the user's test need to force a split, so that it's
  553. # not using the longest current line width in the line being built
  554. sub_words = word.split('\n')
  555. for i in range(len(sub_words)):
  556. current_width = self._get_rendered_text_width(
  557. line + ' ' + sub_words[i])
  558. # Split long lines, and each newline found in the current word
  559. if current_width > line_width or i > 0:
  560. wrapped_str += line + '\n'
  561. line = ""
  562. if line == "":
  563. line = sub_words[i]
  564. else:
  565. line += ' ' + sub_words[i]
  566. return wrapped_str + line
  567. @artist.allow_rasterization
  568. def draw(self, renderer):
  569. """
  570. Draws the `.Text` object to the given *renderer*.
  571. """
  572. if renderer is not None:
  573. self._renderer = renderer
  574. if not self.get_visible():
  575. return
  576. if self.get_text() == '':
  577. return
  578. renderer.open_group('text', self.get_gid())
  579. with _wrap_text(self) as textobj:
  580. bbox, info, descent = textobj._get_layout(renderer)
  581. trans = textobj.get_transform()
  582. # don't use textobj.get_position here, which refers to text
  583. # position in Text, and dash position in TextWithDash:
  584. posx = float(textobj.convert_xunits(textobj._x))
  585. posy = float(textobj.convert_yunits(textobj._y))
  586. posx, posy = trans.transform_point((posx, posy))
  587. if not np.isfinite(posx) or not np.isfinite(posy):
  588. _log.warning("posx and posy should be finite values")
  589. return
  590. canvasw, canvash = renderer.get_canvas_width_height()
  591. # draw the FancyBboxPatch
  592. if textobj._bbox_patch:
  593. textobj._draw_bbox(renderer, posx, posy)
  594. gc = renderer.new_gc()
  595. gc.set_foreground(textobj.get_color())
  596. gc.set_alpha(textobj.get_alpha())
  597. gc.set_url(textobj._url)
  598. textobj._set_gc_clip(gc)
  599. angle = textobj.get_rotation()
  600. for line, wh, x, y in info:
  601. mtext = textobj if len(info) == 1 else None
  602. x = x + posx
  603. y = y + posy
  604. if renderer.flipy():
  605. y = canvash - y
  606. clean_line, ismath = textobj.is_math_text(line,
  607. self.get_usetex())
  608. if textobj.get_path_effects():
  609. from matplotlib.patheffects import PathEffectRenderer
  610. textrenderer = PathEffectRenderer(
  611. textobj.get_path_effects(), renderer)
  612. else:
  613. textrenderer = renderer
  614. if textobj.get_usetex():
  615. textrenderer.draw_tex(gc, x, y, clean_line,
  616. textobj._fontproperties, angle,
  617. mtext=mtext)
  618. else:
  619. textrenderer.draw_text(gc, x, y, clean_line,
  620. textobj._fontproperties, angle,
  621. ismath=ismath, mtext=mtext)
  622. gc.restore()
  623. renderer.close_group('text')
  624. self.stale = False
  625. def get_color(self):
  626. "Return the color of the text"
  627. return self._color
  628. def get_fontproperties(self):
  629. "Return the `.font_manager.FontProperties` object"
  630. return self._fontproperties
  631. def get_fontfamily(self):
  632. """
  633. Return the list of font families used for font lookup
  634. See Also
  635. --------
  636. .font_manager.FontProperties.get_family
  637. """
  638. return self._fontproperties.get_family()
  639. def get_fontname(self):
  640. """
  641. Return the font name as string
  642. See Also
  643. --------
  644. .font_manager.FontProperties.get_name
  645. """
  646. return self._fontproperties.get_name()
  647. def get_fontstyle(self):
  648. """
  649. Return the font style as string
  650. See Also
  651. --------
  652. .font_manager.FontProperties.get_style
  653. """
  654. return self._fontproperties.get_style()
  655. def get_fontsize(self):
  656. """
  657. Return the font size as integer
  658. See Also
  659. --------
  660. .font_manager.FontProperties.get_size_in_points
  661. """
  662. return self._fontproperties.get_size_in_points()
  663. def get_fontvariant(self):
  664. """
  665. Return the font variant as a string
  666. See Also
  667. --------
  668. .font_manager.FontProperties.get_variant
  669. """
  670. return self._fontproperties.get_variant()
  671. def get_fontweight(self):
  672. """
  673. Get the font weight as string or number
  674. See Also
  675. --------
  676. .font_manager.FontProperties.get_weight
  677. """
  678. return self._fontproperties.get_weight()
  679. def get_stretch(self):
  680. """
  681. Get the font stretch as a string or number
  682. See Also
  683. --------
  684. .font_manager.FontProperties.get_stretch
  685. """
  686. return self._fontproperties.get_stretch()
  687. def get_horizontalalignment(self):
  688. """
  689. Return the horizontal alignment as string. Will be one of
  690. 'left', 'center' or 'right'.
  691. """
  692. return self._horizontalalignment
  693. def get_unitless_position(self):
  694. "Return the unitless position of the text as a tuple (*x*, *y*)"
  695. # This will get the position with all unit information stripped away.
  696. # This is here for convenience since it is done in several locations.
  697. x = float(self.convert_xunits(self._x))
  698. y = float(self.convert_yunits(self._y))
  699. return x, y
  700. def get_position(self):
  701. "Return the position of the text as a tuple (*x*, *y*)"
  702. # This should return the same data (possible unitized) as was
  703. # specified with 'set_x' and 'set_y'.
  704. return self._x, self._y
  705. def get_prop_tup(self, renderer=None):
  706. """
  707. Return a hashable tuple of properties.
  708. Not intended to be human readable, but useful for backends who
  709. want to cache derived information about text (e.g., layouts) and
  710. need to know if the text has changed.
  711. """
  712. x, y = self.get_unitless_position()
  713. renderer = renderer or self._renderer
  714. return (x, y, self.get_text(), self._color,
  715. self._verticalalignment, self._horizontalalignment,
  716. hash(self._fontproperties),
  717. self._rotation, self._rotation_mode,
  718. self.figure.dpi, weakref.ref(renderer),
  719. self._linespacing
  720. )
  721. def get_text(self):
  722. "Get the text as string"
  723. return self._text
  724. def get_verticalalignment(self):
  725. """
  726. Return the vertical alignment as string. Will be one of
  727. 'top', 'center', 'bottom' or 'baseline'.
  728. """
  729. return self._verticalalignment
  730. def get_window_extent(self, renderer=None, dpi=None):
  731. """
  732. Return the `Bbox` bounding the text, in display units.
  733. In addition to being used internally, this is useful for specifying
  734. clickable regions in a png file on a web page.
  735. Parameters
  736. ----------
  737. renderer : Renderer, optional
  738. A renderer is needed to compute the bounding box. If the artist
  739. has already been drawn, the renderer is cached; thus, it is only
  740. necessary to pass this argument when calling `get_window_extent`
  741. before the first `draw`. In practice, it is usually easier to
  742. trigger a draw first (e.g. by saving the figure).
  743. dpi : float, optional
  744. The dpi value for computing the bbox, defaults to
  745. ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if
  746. to match regions with a figure saved with a custom dpi value.
  747. """
  748. #return _unit_box
  749. if not self.get_visible():
  750. return Bbox.unit()
  751. if dpi is not None:
  752. dpi_orig = self.figure.dpi
  753. self.figure.dpi = dpi
  754. if self.get_text() == '':
  755. tx, ty = self._get_xy_display()
  756. return Bbox.from_bounds(tx, ty, 0, 0)
  757. if renderer is not None:
  758. self._renderer = renderer
  759. if self._renderer is None:
  760. self._renderer = self.figure._cachedRenderer
  761. if self._renderer is None:
  762. raise RuntimeError('Cannot get window extent w/o renderer')
  763. bbox, info, descent = self._get_layout(self._renderer)
  764. x, y = self.get_unitless_position()
  765. x, y = self.get_transform().transform_point((x, y))
  766. bbox = bbox.translated(x, y)
  767. if dpi is not None:
  768. self.figure.dpi = dpi_orig
  769. return bbox
  770. def set_backgroundcolor(self, color):
  771. """
  772. Set the background color of the text by updating the bbox.
  773. Parameters
  774. ----------
  775. color : color
  776. See Also
  777. --------
  778. .set_bbox : To change the position of the bounding box
  779. """
  780. if self._bbox_patch is None:
  781. self.set_bbox(dict(facecolor=color, edgecolor=color))
  782. else:
  783. self._bbox_patch.update(dict(facecolor=color))
  784. self._update_clip_properties()
  785. self.stale = True
  786. def set_color(self, color):
  787. """
  788. Set the foreground color of the text
  789. Parameters
  790. ----------
  791. color : color
  792. """
  793. # Make sure it is hashable, or get_prop_tup will fail.
  794. try:
  795. hash(color)
  796. except TypeError:
  797. color = tuple(color)
  798. self._color = color
  799. self.stale = True
  800. def set_horizontalalignment(self, align):
  801. """
  802. Set the horizontal alignment to one of
  803. Parameters
  804. ----------
  805. align : {'center', 'right', 'left'}
  806. """
  807. legal = ('center', 'right', 'left')
  808. if align not in legal:
  809. raise ValueError('Horizontal alignment must be one of %s' %
  810. str(legal))
  811. self._horizontalalignment = align
  812. self.stale = True
  813. def set_multialignment(self, align):
  814. """
  815. Set the alignment for multiple lines layout. The layout of the
  816. bounding box of all the lines is determined bu the horizontalalignment
  817. and verticalalignment properties, but the multiline text within that
  818. box can be
  819. Parameters
  820. ----------
  821. align : {'left', 'right', 'center'}
  822. """
  823. legal = ('center', 'right', 'left')
  824. if align not in legal:
  825. raise ValueError('Horizontal alignment must be one of %s' %
  826. str(legal))
  827. self._multialignment = align
  828. self.stale = True
  829. def set_linespacing(self, spacing):
  830. """
  831. Set the line spacing as a multiple of the font size.
  832. Default is 1.2.
  833. Parameters
  834. ----------
  835. spacing : float (multiple of font size)
  836. """
  837. self._linespacing = spacing
  838. self.stale = True
  839. def set_fontfamily(self, fontname):
  840. """
  841. Set the font family. May be either a single string, or a list of
  842. strings in decreasing priority. Each string may be either a real font
  843. name or a generic font class name. If the latter, the specific font
  844. names will be looked up in the corresponding rcParams.
  845. Parameters
  846. ----------
  847. fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
  848. 'monospace'}
  849. See Also
  850. --------
  851. .font_manager.FontProperties.set_family
  852. """
  853. self._fontproperties.set_family(fontname)
  854. self.stale = True
  855. def set_fontvariant(self, variant):
  856. """
  857. Set the font variant, either 'normal' or 'small-caps'.
  858. Parameters
  859. ----------
  860. variant : {'normal', 'small-caps'}
  861. See Also
  862. --------
  863. .font_manager.FontProperties.set_variant
  864. """
  865. self._fontproperties.set_variant(variant)
  866. self.stale = True
  867. def set_fontstyle(self, fontstyle):
  868. """
  869. Set the font style.
  870. Parameters
  871. ----------
  872. fontstyle : {'normal', 'italic', 'oblique'}
  873. See Also
  874. --------
  875. .font_manager.FontProperties.set_style
  876. """
  877. self._fontproperties.set_style(fontstyle)
  878. self.stale = True
  879. def set_fontsize(self, fontsize):
  880. """
  881. Set the font size. May be either a size string, relative to
  882. the default font size, or an absolute font size in points.
  883. Parameters
  884. ----------
  885. fontsize : {size in points, 'xx-small', 'x-small', 'small', 'medium', \
  886. 'large', 'x-large', 'xx-large'}
  887. See Also
  888. --------
  889. .font_manager.FontProperties.set_size
  890. """
  891. self._fontproperties.set_size(fontsize)
  892. self.stale = True
  893. def set_fontweight(self, weight):
  894. """
  895. Set the font weight.
  896. Parameters
  897. ----------
  898. weight : {a numeric value in range 0-1000, 'ultralight', 'light', \
  899. 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', \
  900. 'demi', 'bold', 'heavy', 'extra bold', 'black'}
  901. See Also
  902. --------
  903. .font_manager.FontProperties.set_weight
  904. """
  905. self._fontproperties.set_weight(weight)
  906. self.stale = True
  907. def set_fontstretch(self, stretch):
  908. """
  909. Set the font stretch (horizontal condensation or expansion).
  910. Parameters
  911. ----------
  912. stretch : {a numeric value in range 0-1000, 'ultra-condensed', \
  913. 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', \
  914. 'expanded', 'extra-expanded', 'ultra-expanded'}
  915. See Also
  916. --------
  917. .font_manager.FontProperties.set_stretch
  918. """
  919. self._fontproperties.set_stretch(stretch)
  920. self.stale = True
  921. def set_position(self, xy):
  922. """
  923. Set the (*x*, *y*) position of the text.
  924. Parameters
  925. ----------
  926. xy : (float, float)
  927. """
  928. self.set_x(xy[0])
  929. self.set_y(xy[1])
  930. def set_x(self, x):
  931. """
  932. Set the *x* position of the text.
  933. Parameters
  934. ----------
  935. x : float
  936. """
  937. self._x = x
  938. self.stale = True
  939. def set_y(self, y):
  940. """
  941. Set the *y* position of the text.
  942. Parameters
  943. ----------
  944. y : float
  945. """
  946. self._y = y
  947. self.stale = True
  948. def set_rotation(self, s):
  949. """
  950. Set the rotation of the text.
  951. Parameters
  952. ----------
  953. s : {angle in degrees, 'vertical', 'horizontal'}
  954. """
  955. self._rotation = s
  956. self.stale = True
  957. def set_verticalalignment(self, align):
  958. """
  959. Set the vertical alignment
  960. Parameters
  961. ----------
  962. align : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
  963. """
  964. legal = ('top', 'bottom', 'center', 'baseline', 'center_baseline')
  965. if align not in legal:
  966. raise ValueError('Vertical alignment must be one of %s' %
  967. str(legal))
  968. self._verticalalignment = align
  969. self.stale = True
  970. def set_text(self, s):
  971. """
  972. Set the text string *s*.
  973. It may contain newlines (``\\n``) or math in LaTeX syntax.
  974. Parameters
  975. ----------
  976. s : string or object castable to string (but ``None`` becomes ``''``)
  977. """
  978. if s is None:
  979. s = ''
  980. if s != self._text:
  981. self._text = '%s' % (s,)
  982. self.stale = True
  983. @staticmethod
  984. def is_math_text(s, usetex=None):
  985. """
  986. Returns a cleaned string and a boolean flag.
  987. The flag indicates if the given string *s* contains any mathtext,
  988. determined by counting unescaped dollar signs. If no mathtext
  989. is present, the cleaned string has its dollar signs unescaped.
  990. If usetex is on, the flag always has the value "TeX".
  991. """
  992. # Did we find an even number of non-escaped dollar signs?
  993. # If so, treat is as math text.
  994. if usetex is None:
  995. usetex = rcParams['text.usetex']
  996. if usetex:
  997. if s == ' ':
  998. s = r'\ '
  999. return s, 'TeX'
  1000. if cbook.is_math_text(s):
  1001. return s, True
  1002. else:
  1003. return s.replace(r'\$', '$'), False
  1004. def set_fontproperties(self, fp):
  1005. """
  1006. Set the font properties that control the text.
  1007. Parameters
  1008. ----------
  1009. fp : `.font_manager.FontProperties`
  1010. """
  1011. if isinstance(fp, str):
  1012. fp = FontProperties(fp)
  1013. self._fontproperties = fp.copy()
  1014. self.stale = True
  1015. def set_usetex(self, usetex):
  1016. """
  1017. Parameters
  1018. ----------
  1019. usetex : bool or None
  1020. Whether to render using TeX, ``None`` means to use
  1021. :rc:`text.usetex`.
  1022. """
  1023. if usetex is None:
  1024. self._usetex = rcParams['text.usetex']
  1025. else:
  1026. self._usetex = bool(usetex)
  1027. self.stale = True
  1028. def get_usetex(self):
  1029. """
  1030. Return whether this `Text` object uses TeX for rendering.
  1031. If the user has not manually set this value, it defaults to
  1032. :rc:`text.usetex`.
  1033. """
  1034. if self._usetex is None:
  1035. return rcParams['text.usetex']
  1036. else:
  1037. return self._usetex
  1038. def set_fontname(self, fontname):
  1039. """
  1040. alias for `.set_family`
  1041. One-way alias only: the getter differs.
  1042. Parameters
  1043. ----------
  1044. fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
  1045. 'monospace'}
  1046. See Also
  1047. --------
  1048. .font_manager.FontProperties.set_family
  1049. """
  1050. return self.set_family(fontname)
  1051. docstring.interpd.update(Text=artist.kwdoc(Text))
  1052. docstring.dedent_interpd(Text.__init__)
  1053. class TextWithDash(Text):
  1054. """
  1055. This is basically a :class:`~matplotlib.text.Text` with a dash
  1056. (drawn with a :class:`~matplotlib.lines.Line2D`) before/after
  1057. it. It is intended to be a drop-in replacement for
  1058. :class:`~matplotlib.text.Text`, and should behave identically to
  1059. it when *dashlength* = 0.0.
  1060. The dash always comes between the point specified by
  1061. :meth:`~matplotlib.text.Text.set_position` and the text. When a
  1062. dash exists, the text alignment arguments (*horizontalalignment*,
  1063. *verticalalignment*) are ignored.
  1064. *dashlength* is the length of the dash in canvas units.
  1065. (default = 0.0).
  1066. *dashdirection* is one of 0 or 1, where 0 draws the dash after the
  1067. text and 1 before. (default = 0).
  1068. *dashrotation* specifies the rotation of the dash, and should
  1069. generally stay *None*. In this case
  1070. :meth:`~matplotlib.text.TextWithDash.get_dashrotation` returns
  1071. :meth:`~matplotlib.text.Text.get_rotation`. (i.e., the dash takes
  1072. its rotation from the text's rotation). Because the text center is
  1073. projected onto the dash, major deviations in the rotation cause
  1074. what may be considered visually unappealing results.
  1075. (default = *None*)
  1076. *dashpad* is a padding length to add (or subtract) space
  1077. between the text and the dash, in canvas units.
  1078. (default = 3)
  1079. *dashpush* "pushes" the dash and text away from the point
  1080. specified by :meth:`~matplotlib.text.Text.set_position` by the
  1081. amount in canvas units. (default = 0)
  1082. .. note::
  1083. The alignment of the two objects is based on the bounding box
  1084. of the :class:`~matplotlib.text.Text`, as obtained by
  1085. :meth:`~matplotlib.artist.Artist.get_window_extent`. This, in
  1086. turn, appears to depend on the font metrics as given by the
  1087. rendering backend. Hence the quality of the "centering" of the
  1088. label text with respect to the dash varies depending on the
  1089. backend used.
  1090. .. note::
  1091. I'm not sure that I got the
  1092. :meth:`~matplotlib.text.TextWithDash.get_window_extent` right,
  1093. or whether that's sufficient for providing the object bounding
  1094. box.
  1095. """
  1096. __name__ = 'textwithdash'
  1097. def __str__(self):
  1098. return "TextWithDash(%g, %g, %r)" % (self._x, self._y, self._text)
  1099. def __init__(self,
  1100. x=0, y=0, text='',
  1101. color=None, # defaults to rc params
  1102. verticalalignment='center',
  1103. horizontalalignment='center',
  1104. multialignment=None,
  1105. fontproperties=None, # defaults to FontProperties()
  1106. rotation=None,
  1107. linespacing=None,
  1108. dashlength=0.0,
  1109. dashdirection=0,
  1110. dashrotation=None,
  1111. dashpad=3,
  1112. dashpush=0,
  1113. ):
  1114. Text.__init__(self, x=x, y=y, text=text, color=color,
  1115. verticalalignment=verticalalignment,
  1116. horizontalalignment=horizontalalignment,
  1117. multialignment=multialignment,
  1118. fontproperties=fontproperties,
  1119. rotation=rotation,
  1120. linespacing=linespacing)
  1121. # The position (x,y) values for text and dashline
  1122. # are bogus as given in the instantiation; they will
  1123. # be set correctly by update_coords() in draw()
  1124. self.dashline = Line2D(xdata=(x, x),
  1125. ydata=(y, y),
  1126. color='k',
  1127. linestyle='-')
  1128. self._dashx = float(x)
  1129. self._dashy = float(y)
  1130. self._dashlength = dashlength
  1131. self._dashdirection = dashdirection
  1132. self._dashrotation = dashrotation
  1133. self._dashpad = dashpad
  1134. self._dashpush = dashpush
  1135. #self.set_bbox(dict(pad=0))
  1136. def get_unitless_position(self):
  1137. "Return the unitless position of the text as a tuple (*x*, *y*)"
  1138. # This will get the position with all unit information stripped away.
  1139. # This is here for convenience since it is done in several locations.
  1140. x = float(self.convert_xunits(self._dashx))
  1141. y = float(self.convert_yunits(self._dashy))
  1142. return x, y
  1143. def get_position(self):
  1144. "Return the position of the text as a tuple (*x*, *y*)"
  1145. # This should return the same data (possibly unitized) as was
  1146. # specified with set_x and set_y
  1147. return self._dashx, self._dashy
  1148. def get_prop_tup(self, renderer=None):
  1149. """
  1150. Return a hashable tuple of properties.
  1151. Not intended to be human readable, but useful for backends who
  1152. want to cache derived information about text (e.g., layouts) and
  1153. need to know if the text has changed.
  1154. """
  1155. props = [p for p in Text.get_prop_tup(self, renderer=renderer)]
  1156. props.extend([self._x, self._y, self._dashlength,
  1157. self._dashdirection, self._dashrotation, self._dashpad,
  1158. self._dashpush])
  1159. return tuple(props)
  1160. def draw(self, renderer):
  1161. """
  1162. Draw the :class:`TextWithDash` object to the given *renderer*.
  1163. """
  1164. self.update_coords(renderer)
  1165. Text.draw(self, renderer)
  1166. if self.get_dashlength() > 0.0:
  1167. self.dashline.draw(renderer)
  1168. self.stale = False
  1169. def update_coords(self, renderer):
  1170. """
  1171. Computes the actual *x*, *y* coordinates for text based on the
  1172. input *x*, *y* and the *dashlength*. Since the rotation is
  1173. with respect to the actual canvas's coordinates we need to map
  1174. back and forth.
  1175. """
  1176. dashx, dashy = self.get_unitless_position()
  1177. dashlength = self.get_dashlength()
  1178. # Shortcircuit this process if we don't have a dash
  1179. if dashlength == 0.0:
  1180. self._x, self._y = dashx, dashy
  1181. return
  1182. dashrotation = self.get_dashrotation()
  1183. dashdirection = self.get_dashdirection()
  1184. dashpad = self.get_dashpad()
  1185. dashpush = self.get_dashpush()
  1186. angle = get_rotation(dashrotation)
  1187. theta = np.pi * (angle / 180.0 + dashdirection - 1)
  1188. cos_theta, sin_theta = np.cos(theta), np.sin(theta)
  1189. transform = self.get_transform()
  1190. # Compute the dash end points
  1191. # The 'c' prefix is for canvas coordinates
  1192. cxy = transform.transform_point((dashx, dashy))
  1193. cd = np.array([cos_theta, sin_theta])
  1194. c1 = cxy + dashpush * cd
  1195. c2 = cxy + (dashpush + dashlength) * cd
  1196. inverse = transform.inverted()
  1197. (x1, y1) = inverse.transform_point(tuple(c1))
  1198. (x2, y2) = inverse.transform_point(tuple(c2))
  1199. self.dashline.set_data((x1, x2), (y1, y2))
  1200. # We now need to extend this vector out to
  1201. # the center of the text area.
  1202. # The basic problem here is that we're "rotating"
  1203. # two separate objects but want it to appear as
  1204. # if they're rotated together.
  1205. # This is made non-trivial because of the
  1206. # interaction between text rotation and alignment -
  1207. # text alignment is based on the bbox after rotation.
  1208. # We reset/force both alignments to 'center'
  1209. # so we can do something relatively reasonable.
  1210. # There's probably a better way to do this by
  1211. # embedding all this in the object's transformations,
  1212. # but I don't grok the transformation stuff
  1213. # well enough yet.
  1214. we = Text.get_window_extent(self, renderer=renderer)
  1215. w, h = we.width, we.height
  1216. # Watch for zeros
  1217. if sin_theta == 0.0:
  1218. dx = w
  1219. dy = 0.0
  1220. elif cos_theta == 0.0:
  1221. dx = 0.0
  1222. dy = h
  1223. else:
  1224. tan_theta = sin_theta / cos_theta
  1225. dx = w
  1226. dy = w * tan_theta
  1227. if dy > h or dy < -h:
  1228. dy = h
  1229. dx = h / tan_theta
  1230. cwd = np.array([dx, dy]) / 2
  1231. cwd *= 1 + dashpad / np.sqrt(np.dot(cwd, cwd))
  1232. cw = c2 + (dashdirection * 2 - 1) * cwd
  1233. newx, newy = inverse.transform_point(tuple(cw))
  1234. self._x, self._y = newx, newy
  1235. # Now set the window extent
  1236. # I'm not at all sure this is the right way to do this.
  1237. we = Text.get_window_extent(self, renderer=renderer)
  1238. self._twd_window_extent = we.frozen()
  1239. self._twd_window_extent.update_from_data_xy(np.array([c1]), False)
  1240. # Finally, make text align center
  1241. Text.set_horizontalalignment(self, 'center')
  1242. Text.set_verticalalignment(self, 'center')
  1243. def get_window_extent(self, renderer=None):
  1244. '''
  1245. Return a :class:`~matplotlib.transforms.Bbox` object bounding
  1246. the text, in display units.
  1247. In addition to being used internally, this is useful for
  1248. specifying clickable regions in a png file on a web page.
  1249. *renderer* defaults to the _renderer attribute of the text
  1250. object. This is not assigned until the first execution of
  1251. :meth:`draw`, so you must use this kwarg if you want
  1252. to call :meth:`get_window_extent` prior to the first
  1253. :meth:`draw`. For getting web page regions, it is
  1254. simpler to call the method after saving the figure.
  1255. '''
  1256. self.update_coords(renderer)
  1257. if self.get_dashlength() == 0.0:
  1258. return Text.get_window_extent(self, renderer=renderer)
  1259. else:
  1260. return self._twd_window_extent
  1261. def get_dashlength(self):
  1262. """
  1263. Get the length of the dash.
  1264. """
  1265. return self._dashlength
  1266. def set_dashlength(self, dl):
  1267. """
  1268. Set the length of the dash, in canvas units.
  1269. Parameters
  1270. ----------
  1271. dl : float
  1272. """
  1273. self._dashlength = dl
  1274. self.stale = True
  1275. def get_dashdirection(self):
  1276. """
  1277. Get the direction dash. 1 is before the text and 0 is after.
  1278. """
  1279. return self._dashdirection
  1280. def set_dashdirection(self, dd):
  1281. """
  1282. Set the direction of the dash following the text. 1 is before the text
  1283. and 0 is after. The default is 0, which is what you'd want for the
  1284. typical case of ticks below and on the left of the figure.
  1285. Parameters
  1286. ----------
  1287. dd : int (1 is before, 0 is after)
  1288. """
  1289. self._dashdirection = dd
  1290. self.stale = True
  1291. def get_dashrotation(self):
  1292. """
  1293. Get the rotation of the dash in degrees.
  1294. """
  1295. if self._dashrotation is None:
  1296. return self.get_rotation()
  1297. else:
  1298. return self._dashrotation
  1299. def set_dashrotation(self, dr):
  1300. """
  1301. Set the rotation of the dash, in degrees.
  1302. Parameters
  1303. ----------
  1304. dr : float
  1305. """
  1306. self._dashrotation = dr
  1307. self.stale = True
  1308. def get_dashpad(self):
  1309. """
  1310. Get the extra spacing between the dash and the text, in canvas units.
  1311. """
  1312. return self._dashpad
  1313. def set_dashpad(self, dp):
  1314. """
  1315. Set the "pad" of the TextWithDash, which is the extra spacing
  1316. between the dash and the text, in canvas units.
  1317. Parameters
  1318. ----------
  1319. dp : float
  1320. """
  1321. self._dashpad = dp
  1322. self.stale = True
  1323. def get_dashpush(self):
  1324. """
  1325. Get the extra spacing between the dash and the specified text
  1326. position, in canvas units.
  1327. """
  1328. return self._dashpush
  1329. def set_dashpush(self, dp):
  1330. """
  1331. Set the "push" of the TextWithDash, which is the extra spacing between
  1332. the beginning of the dash and the specified position.
  1333. Parameters
  1334. ----------
  1335. dp : float
  1336. """
  1337. self._dashpush = dp
  1338. self.stale = True
  1339. def set_position(self, xy):
  1340. """
  1341. Set the (*x*, *y*) position of the :class:`TextWithDash`.
  1342. Parameters
  1343. ----------
  1344. xy : (float, float)
  1345. """
  1346. self.set_x(xy[0])
  1347. self.set_y(xy[1])
  1348. def set_x(self, x):
  1349. """
  1350. Set the *x* position of the :class:`TextWithDash`.
  1351. Parameters
  1352. ----------
  1353. x : float
  1354. """
  1355. self._dashx = float(x)
  1356. self.stale = True
  1357. def set_y(self, y):
  1358. """
  1359. Set the *y* position of the :class:`TextWithDash`.
  1360. Parameters
  1361. ----------
  1362. y : float
  1363. """
  1364. self._dashy = float(y)
  1365. self.stale = True
  1366. def set_transform(self, t):
  1367. """
  1368. Set the :class:`matplotlib.transforms.Transform` instance used
  1369. by this artist.
  1370. Parameters
  1371. ----------
  1372. t : matplotlib.transforms.Transform
  1373. """
  1374. Text.set_transform(self, t)
  1375. self.dashline.set_transform(t)
  1376. self.stale = True
  1377. def get_figure(self):
  1378. 'return the figure instance the artist belongs to'
  1379. return self.figure
  1380. def set_figure(self, fig):
  1381. """
  1382. Set the figure instance the artist belongs to.
  1383. Parameters
  1384. ----------
  1385. fig : matplotlib.figure.Figure
  1386. """
  1387. Text.set_figure(self, fig)
  1388. self.dashline.set_figure(fig)
  1389. docstring.interpd.update(TextWithDash=artist.kwdoc(TextWithDash))
  1390. class OffsetFrom(object):
  1391. 'Callable helper class for working with `Annotation`'
  1392. def __init__(self, artist, ref_coord, unit="points"):
  1393. '''
  1394. Parameters
  1395. ----------
  1396. artist : `Artist`, `BboxBase`, or `Transform`
  1397. The object to compute the offset from.
  1398. ref_coord : length 2 sequence
  1399. If `artist` is an `Artist` or `BboxBase`, this values is
  1400. the location to of the offset origin in fractions of the
  1401. `artist` bounding box.
  1402. If `artist` is a transform, the offset origin is the
  1403. transform applied to this value.
  1404. unit : {'points, 'pixels'}
  1405. The screen units to use (pixels or points) for the offset
  1406. input.
  1407. '''
  1408. self._artist = artist
  1409. self._ref_coord = ref_coord
  1410. self.set_unit(unit)
  1411. def set_unit(self, unit):
  1412. '''
  1413. The unit for input to the transform used by ``__call__``
  1414. Parameters
  1415. ----------
  1416. unit : {'points', 'pixels'}
  1417. '''
  1418. if unit not in ["points", "pixels"]:
  1419. raise ValueError("'unit' must be one of [ 'points' | 'pixels' ]")
  1420. self._unit = unit
  1421. def get_unit(self):
  1422. 'The unit for input to the transform used by ``__call__``'
  1423. return self._unit
  1424. def _get_scale(self, renderer):
  1425. unit = self.get_unit()
  1426. if unit == "pixels":
  1427. return 1.
  1428. else:
  1429. return renderer.points_to_pixels(1.)
  1430. def __call__(self, renderer):
  1431. '''
  1432. Return the offset transform.
  1433. Parameters
  1434. ----------
  1435. renderer : `RendererBase`
  1436. The renderer to use to compute the offset
  1437. Returns
  1438. -------
  1439. transform : `Transform`
  1440. Maps (x, y) in pixel or point units to screen units
  1441. relative to the given artist.
  1442. '''
  1443. if isinstance(self._artist, Artist):
  1444. bbox = self._artist.get_window_extent(renderer)
  1445. l, b, w, h = bbox.bounds
  1446. xf, yf = self._ref_coord
  1447. x, y = l + w * xf, b + h * yf
  1448. elif isinstance(self._artist, BboxBase):
  1449. l, b, w, h = self._artist.bounds
  1450. xf, yf = self._ref_coord
  1451. x, y = l + w * xf, b + h * yf
  1452. elif isinstance(self._artist, Transform):
  1453. x, y = self._artist.transform_point(self._ref_coord)
  1454. else:
  1455. raise RuntimeError("unknown type")
  1456. sc = self._get_scale(renderer)
  1457. tr = Affine2D().scale(sc, sc).translate(x, y)
  1458. return tr
  1459. class _AnnotationBase(object):
  1460. def __init__(self,
  1461. xy,
  1462. xycoords='data',
  1463. annotation_clip=None):
  1464. self.xy = xy
  1465. self.xycoords = xycoords
  1466. self.set_annotation_clip(annotation_clip)
  1467. self._draggable = None
  1468. def _get_xy(self, renderer, x, y, s):
  1469. if isinstance(s, tuple):
  1470. s1, s2 = s
  1471. else:
  1472. s1, s2 = s, s
  1473. if s1 == 'data':
  1474. x = float(self.convert_xunits(x))
  1475. if s2 == 'data':
  1476. y = float(self.convert_yunits(y))
  1477. tr = self._get_xy_transform(renderer, s)
  1478. x1, y1 = tr.transform_point((x, y))
  1479. return x1, y1
  1480. def _get_xy_transform(self, renderer, s):
  1481. if isinstance(s, tuple):
  1482. s1, s2 = s
  1483. from matplotlib.transforms import blended_transform_factory
  1484. tr1 = self._get_xy_transform(renderer, s1)
  1485. tr2 = self._get_xy_transform(renderer, s2)
  1486. tr = blended_transform_factory(tr1, tr2)
  1487. return tr
  1488. elif callable(s):
  1489. tr = s(renderer)
  1490. if isinstance(tr, BboxBase):
  1491. return BboxTransformTo(tr)
  1492. elif isinstance(tr, Transform):
  1493. return tr
  1494. else:
  1495. raise RuntimeError("unknown return type ...")
  1496. elif isinstance(s, Artist):
  1497. bbox = s.get_window_extent(renderer)
  1498. return BboxTransformTo(bbox)
  1499. elif isinstance(s, BboxBase):
  1500. return BboxTransformTo(s)
  1501. elif isinstance(s, Transform):
  1502. return s
  1503. elif not isinstance(s, str):
  1504. raise RuntimeError("unknown coordinate type : %s" % s)
  1505. if s == 'data':
  1506. return self.axes.transData
  1507. elif s == 'polar':
  1508. from matplotlib.projections import PolarAxes
  1509. tr = PolarAxes.PolarTransform()
  1510. trans = tr + self.axes.transData
  1511. return trans
  1512. s_ = s.split()
  1513. if len(s_) != 2:
  1514. raise ValueError("%s is not a recognized coordinate" % s)
  1515. bbox0, xy0 = None, None
  1516. bbox_name, unit = s_
  1517. # if unit is offset-like
  1518. if bbox_name == "figure":
  1519. bbox0 = self.figure.bbox
  1520. elif bbox_name == "axes":
  1521. bbox0 = self.axes.bbox
  1522. # elif bbox_name == "bbox":
  1523. # if bbox is None:
  1524. # raise RuntimeError("bbox is specified as a coordinate but "
  1525. # "never set")
  1526. # bbox0 = self._get_bbox(renderer, bbox)
  1527. if bbox0 is not None:
  1528. xy0 = bbox0.bounds[:2]
  1529. elif bbox_name == "offset":
  1530. xy0 = self._get_ref_xy(renderer)
  1531. if xy0 is not None:
  1532. # reference x, y in display coordinate
  1533. ref_x, ref_y = xy0
  1534. from matplotlib.transforms import Affine2D
  1535. if unit == "points":
  1536. # dots per points
  1537. dpp = self.figure.get_dpi() / 72.
  1538. tr = Affine2D().scale(dpp, dpp)
  1539. elif unit == "pixels":
  1540. tr = Affine2D()
  1541. elif unit == "fontsize":
  1542. fontsize = self.get_size()
  1543. dpp = fontsize * self.figure.get_dpi() / 72.
  1544. tr = Affine2D().scale(dpp, dpp)
  1545. elif unit == "fraction":
  1546. w, h = bbox0.bounds[2:]
  1547. tr = Affine2D().scale(w, h)
  1548. else:
  1549. raise ValueError("%s is not a recognized coordinate" % s)
  1550. return tr.translate(ref_x, ref_y)
  1551. else:
  1552. raise ValueError("%s is not a recognized coordinate" % s)
  1553. def _get_ref_xy(self, renderer):
  1554. """
  1555. return x, y (in display coordinate) that is to be used for a reference
  1556. of any offset coordinate
  1557. """
  1558. def is_offset(s):
  1559. return isinstance(s, str) and s.split()[0] == "offset"
  1560. if isinstance(self.xycoords, tuple):
  1561. s1, s2 = self.xycoords
  1562. if is_offset(s1) or is_offset(s2):
  1563. raise ValueError("xycoords should not be an offset coordinate")
  1564. x, y = self.xy
  1565. x1, y1 = self._get_xy(renderer, x, y, s1)
  1566. x2, y2 = self._get_xy(renderer, x, y, s2)
  1567. return x1, y2
  1568. elif is_offset(self.xycoords):
  1569. raise ValueError("xycoords should not be an offset coordinate")
  1570. else:
  1571. x, y = self.xy
  1572. return self._get_xy(renderer, x, y, self.xycoords)
  1573. #raise RuntimeError("must be defined by the derived class")
  1574. # def _get_bbox(self, renderer):
  1575. # if hasattr(bbox, "bounds"):
  1576. # return bbox
  1577. # elif hasattr(bbox, "get_window_extent"):
  1578. # bbox = bbox.get_window_extent()
  1579. # return bbox
  1580. # else:
  1581. # raise ValueError("A bbox instance is expected but got %s" %
  1582. # str(bbox))
  1583. def set_annotation_clip(self, b):
  1584. """
  1585. set *annotation_clip* attribute.
  1586. * True: the annotation will only be drawn when self.xy is inside
  1587. the axes.
  1588. * False: the annotation will always be drawn regardless of its
  1589. position.
  1590. * None: the self.xy will be checked only if *xycoords* is "data"
  1591. """
  1592. self._annotation_clip = b
  1593. def get_annotation_clip(self):
  1594. """
  1595. Return *annotation_clip* attribute.
  1596. See :meth:`set_annotation_clip` for the meaning of return values.
  1597. """
  1598. return self._annotation_clip
  1599. def _get_position_xy(self, renderer):
  1600. "Return the pixel position of the annotated point."
  1601. x, y = self.xy
  1602. return self._get_xy(renderer, x, y, self.xycoords)
  1603. def _check_xy(self, renderer, xy_pixel):
  1604. """
  1605. given the xy pixel coordinate, check if the annotation need to
  1606. be drawn.
  1607. """
  1608. b = self.get_annotation_clip()
  1609. if b or (b is None and self.xycoords == "data"):
  1610. # check if self.xy is inside the axes.
  1611. if not self.axes.contains_point(xy_pixel):
  1612. return False
  1613. return True
  1614. def draggable(self, state=None, use_blit=False):
  1615. """
  1616. Set the draggable state -- if state is
  1617. * None : toggle the current state
  1618. * True : turn draggable on
  1619. * False : turn draggable off
  1620. If draggable is on, you can drag the annotation on the canvas with
  1621. the mouse. The DraggableAnnotation helper instance is returned if
  1622. draggable is on.
  1623. """
  1624. from matplotlib.offsetbox import DraggableAnnotation
  1625. is_draggable = self._draggable is not None
  1626. # if state is None we'll toggle
  1627. if state is None:
  1628. state = not is_draggable
  1629. if state:
  1630. if self._draggable is None:
  1631. self._draggable = DraggableAnnotation(self, use_blit)
  1632. else:
  1633. if self._draggable is not None:
  1634. self._draggable.disconnect()
  1635. self._draggable = None
  1636. return self._draggable
  1637. class Annotation(Text, _AnnotationBase):
  1638. """
  1639. An `.Annotation` is a `.Text` that can refer to a specific position *xy*.
  1640. Optionally an arrow pointing from the text to *xy* can be drawn.
  1641. Attributes
  1642. ----------
  1643. xy
  1644. The annotated position.
  1645. xycoords
  1646. The coordinate system for *xy*.
  1647. arrow_patch
  1648. A `.FancyArrowPatch` to point from *xytext* to *xy*.
  1649. """
  1650. def __str__(self):
  1651. return "Annotation(%g, %g, %r)" % (self.xy[0], self.xy[1], self._text)
  1652. def __init__(self, s, xy,
  1653. xytext=None,
  1654. xycoords='data',
  1655. textcoords=None,
  1656. arrowprops=None,
  1657. annotation_clip=None,
  1658. **kwargs):
  1659. """
  1660. Annotate the point *xy* with text *s*.
  1661. In the simplest form, the text is placed at *xy*.
  1662. Optionally, the text can be displayed in another position *xytext*.
  1663. An arrow pointing from the text to the annotated point *xy* can then
  1664. be added by defining *arrowprops*.
  1665. Parameters
  1666. ----------
  1667. s : str
  1668. The text of the annotation.
  1669. xy : (float, float)
  1670. The point *(x,y)* to annotate.
  1671. xytext : (float, float), optional
  1672. The position *(x,y)* to place the text at.
  1673. If *None*, defaults to *xy*.
  1674. xycoords : str, `.Artist`, `.Transform`, callable or tuple, optional
  1675. The coordinate system that *xy* is given in. The following types
  1676. of values are supported:
  1677. - One of the following strings:
  1678. ================= =============================================
  1679. Value Description
  1680. ================= =============================================
  1681. 'figure points' Points from the lower left of the figure
  1682. 'figure pixels' Pixels from the lower left of the figure
  1683. 'figure fraction' Fraction of figure from lower left
  1684. 'axes points' Points from lower left corner of axes
  1685. 'axes pixels' Pixels from lower left corner of axes
  1686. 'axes fraction' Fraction of axes from lower left
  1687. 'data' Use the coordinate system of the object being
  1688. annotated (default)
  1689. 'polar' *(theta,r)* if not native 'data' coordinates
  1690. ================= =============================================
  1691. - An `.Artist`: *xy* is interpreted as a fraction of the artists
  1692. `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower
  1693. left corner of the bounding box and *(0.5, 1)* would be the
  1694. center top of the bounding box.
  1695. - A `.Transform` to transform *xy* to screen coordinates.
  1696. - A function with one of the following signatures::
  1697. def transform(renderer) -> Bbox
  1698. def transform(renderer) -> Transform
  1699. where *renderer* is a `.RendererBase` subclass.
  1700. The result of the function is interpreted like the `.Artist` and
  1701. `.Transform` cases above.
  1702. - A tuple *(xcoords, ycoords)* specifying separate coordinate
  1703. systems for *x* and *y*. *xcoords* and *ycoords* must each be
  1704. of one of the above described types.
  1705. See :ref:`plotting-guide-annotation` for more details.
  1706. Defaults to 'data'.
  1707. textcoords : str, `.Artist`, `.Transform`, callable or tuple, optional
  1708. The coordinate system that *xytext* is given in.
  1709. All *xycoords* values are valid as well as the following
  1710. strings:
  1711. ================= =========================================
  1712. Value Description
  1713. ================= =========================================
  1714. 'offset points' Offset (in points) from the *xy* value
  1715. 'offset pixels' Offset (in pixels) from the *xy* value
  1716. ================= =========================================
  1717. Defaults to the value of *xycoords*, i.e. use the same coordinate
  1718. system for annotation point and text position.
  1719. arrowprops : dict, optional
  1720. The properties used to draw a
  1721. `~matplotlib.patches.FancyArrowPatch` arrow between the
  1722. positions *xy* and *xytext*.
  1723. If *arrowprops* does not contain the key 'arrowstyle' the
  1724. allowed keys are:
  1725. ========== ======================================================
  1726. Key Description
  1727. ========== ======================================================
  1728. width The width of the arrow in points
  1729. headwidth The width of the base of the arrow head in points
  1730. headlength The length of the arrow head in points
  1731. shrink Fraction of total length to shrink from both ends
  1732. ? Any key to :class:`matplotlib.patches.FancyArrowPatch`
  1733. ========== ======================================================
  1734. If *arrowprops* contains the key 'arrowstyle' the
  1735. above keys are forbidden. The allowed values of
  1736. ``'arrowstyle'`` are:
  1737. ============ =============================================
  1738. Name Attrs
  1739. ============ =============================================
  1740. ``'-'`` None
  1741. ``'->'`` head_length=0.4,head_width=0.2
  1742. ``'-['`` widthB=1.0,lengthB=0.2,angleB=None
  1743. ``'|-|'`` widthA=1.0,widthB=1.0
  1744. ``'-|>'`` head_length=0.4,head_width=0.2
  1745. ``'<-'`` head_length=0.4,head_width=0.2
  1746. ``'<->'`` head_length=0.4,head_width=0.2
  1747. ``'<|-'`` head_length=0.4,head_width=0.2
  1748. ``'<|-|>'`` head_length=0.4,head_width=0.2
  1749. ``'fancy'`` head_length=0.4,head_width=0.4,tail_width=0.4
  1750. ``'simple'`` head_length=0.5,head_width=0.5,tail_width=0.2
  1751. ``'wedge'`` tail_width=0.3,shrink_factor=0.5
  1752. ============ =============================================
  1753. Valid keys for `~matplotlib.patches.FancyArrowPatch` are:
  1754. =============== ==================================================
  1755. Key Description
  1756. =============== ==================================================
  1757. arrowstyle the arrow style
  1758. connectionstyle the connection style
  1759. relpos default is (0.5, 0.5)
  1760. patchA default is bounding box of the text
  1761. patchB default is None
  1762. shrinkA default is 2 points
  1763. shrinkB default is 2 points
  1764. mutation_scale default is text size (in points)
  1765. mutation_aspect default is 1.
  1766. ? any key for :class:`matplotlib.patches.PathPatch`
  1767. =============== ==================================================
  1768. Defaults to None, i.e. no arrow is drawn.
  1769. annotation_clip : bool or None, optional
  1770. Whether to draw the annotation when the annotation point *xy* is
  1771. outside the axes area.
  1772. - If *True*, the annotation will only be drawn when *xy* is
  1773. within the axes.
  1774. - If *False*, the annotation will always be drawn.
  1775. - If *None*, the annotation will only be drawn when *xy* is
  1776. within the axes and *xycoords* is 'data'.
  1777. Defaults to *None*.
  1778. **kwargs
  1779. Additional kwargs are passed to `~matplotlib.text.Text`.
  1780. Returns
  1781. -------
  1782. annotation : `.Annotation`
  1783. See Also
  1784. --------
  1785. :ref:`plotting-guide-annotation`.
  1786. """
  1787. _AnnotationBase.__init__(self,
  1788. xy,
  1789. xycoords=xycoords,
  1790. annotation_clip=annotation_clip)
  1791. # warn about wonky input data
  1792. if (xytext is None and
  1793. textcoords is not None and
  1794. textcoords != xycoords):
  1795. warnings.warn("You have used the `textcoords` kwarg, but not "
  1796. "the `xytext` kwarg. This can lead to surprising "
  1797. "results.")
  1798. # clean up textcoords and assign default
  1799. if textcoords is None:
  1800. textcoords = self.xycoords
  1801. self._textcoords = textcoords
  1802. # cleanup xytext defaults
  1803. if xytext is None:
  1804. xytext = self.xy
  1805. x, y = xytext
  1806. Text.__init__(self, x, y, s, **kwargs)
  1807. self.arrowprops = arrowprops
  1808. if arrowprops is not None:
  1809. if "arrowstyle" in arrowprops:
  1810. arrowprops = self.arrowprops.copy()
  1811. self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
  1812. else:
  1813. # modified YAArrow API to be used with FancyArrowPatch
  1814. shapekeys = ('width', 'headwidth', 'headlength',
  1815. 'shrink', 'frac')
  1816. arrowprops = dict()
  1817. for key, val in self.arrowprops.items():
  1818. if key not in shapekeys:
  1819. arrowprops[key] = val # basic Patch properties
  1820. self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
  1821. **arrowprops)
  1822. else:
  1823. self.arrow_patch = None
  1824. def contains(self, event):
  1825. contains, tinfo = Text.contains(self, event)
  1826. if self.arrow_patch is not None:
  1827. in_patch, _ = self.arrow_patch.contains(event)
  1828. contains = contains or in_patch
  1829. return contains, tinfo
  1830. @property
  1831. def xyann(self):
  1832. """
  1833. The the text position.
  1834. See also *xytext* in `.Annotation`.
  1835. """
  1836. return self.get_position()
  1837. @xyann.setter
  1838. def xyann(self, xytext):
  1839. self.set_position(xytext)
  1840. @property
  1841. def anncoords(self):
  1842. """The coordinate system to use for `.Annotation.xyann`."""
  1843. return self._textcoords
  1844. @anncoords.setter
  1845. def anncoords(self, coords):
  1846. self._textcoords = coords
  1847. get_anncoords = anncoords.fget
  1848. get_anncoords.__doc__ = """
  1849. Return the coordinate system to use for `.Annotation.xyann`.
  1850. See also *xycoords* in `.Annotation`.
  1851. """
  1852. set_anncoords = anncoords.fset
  1853. set_anncoords.__doc__ = """
  1854. Set the coordinate system to use for `.Annotation.xyann`.
  1855. See also *xycoords* in `.Annotation`.
  1856. """
  1857. def set_figure(self, fig):
  1858. if self.arrow_patch is not None:
  1859. self.arrow_patch.set_figure(fig)
  1860. Artist.set_figure(self, fig)
  1861. def update_positions(self, renderer):
  1862. """Update the pixel positions of the annotated point and the text."""
  1863. xy_pixel = self._get_position_xy(renderer)
  1864. self._update_position_xytext(renderer, xy_pixel)
  1865. def _update_position_xytext(self, renderer, xy_pixel):
  1866. """
  1867. Update the pixel positions of the annotation text and the arrow patch.
  1868. """
  1869. # generate transformation,
  1870. self.set_transform(self._get_xy_transform(renderer, self.anncoords))
  1871. ox0, oy0 = self._get_xy_display()
  1872. ox1, oy1 = xy_pixel
  1873. if self.arrowprops is not None:
  1874. x0, y0 = xy_pixel
  1875. l, b, w, h = Text.get_window_extent(self, renderer).bounds
  1876. r = l + w
  1877. t = b + h
  1878. xc = 0.5 * (l + r)
  1879. yc = 0.5 * (b + t)
  1880. d = self.arrowprops.copy()
  1881. ms = d.pop("mutation_scale", self.get_size())
  1882. self.arrow_patch.set_mutation_scale(ms)
  1883. if "arrowstyle" not in d:
  1884. # Approximately simulate the YAArrow.
  1885. # Pop its kwargs:
  1886. shrink = d.pop('shrink', 0.0)
  1887. width = d.pop('width', 4)
  1888. headwidth = d.pop('headwidth', 12)
  1889. # Ignore frac--it is useless.
  1890. frac = d.pop('frac', None)
  1891. if frac is not None:
  1892. warnings.warn(
  1893. "'frac' option in 'arrowprops' is no longer supported;"
  1894. " use 'headlength' to set the head length in points.")
  1895. headlength = d.pop('headlength', 12)
  1896. # NB: ms is in pts
  1897. stylekw = dict(head_length=headlength / ms,
  1898. head_width=headwidth / ms,
  1899. tail_width=width / ms)
  1900. self.arrow_patch.set_arrowstyle('simple', **stylekw)
  1901. # using YAArrow style:
  1902. # pick the x,y corner of the text bbox closest to point
  1903. # annotated
  1904. xpos = ((l, 0), (xc, 0.5), (r, 1))
  1905. ypos = ((b, 0), (yc, 0.5), (t, 1))
  1906. _, (x, relposx) = min((abs(val[0] - x0), val) for val in xpos)
  1907. _, (y, relposy) = min((abs(val[0] - y0), val) for val in ypos)
  1908. self._arrow_relpos = (relposx, relposy)
  1909. r = np.hypot((y - y0), (x - x0))
  1910. shrink_pts = shrink * r / renderer.points_to_pixels(1)
  1911. self.arrow_patch.shrinkA = shrink_pts
  1912. self.arrow_patch.shrinkB = shrink_pts
  1913. # adjust the starting point of the arrow relative to
  1914. # the textbox.
  1915. # TODO : Rotation needs to be accounted.
  1916. relpos = self._arrow_relpos
  1917. bbox = Text.get_window_extent(self, renderer)
  1918. ox0 = bbox.x0 + bbox.width * relpos[0]
  1919. oy0 = bbox.y0 + bbox.height * relpos[1]
  1920. # The arrow will be drawn from (ox0, oy0) to (ox1,
  1921. # oy1). It will be first clipped by patchA and patchB.
  1922. # Then it will be shrunk by shrinkA and shrinkB
  1923. # (in points). If patch A is not set, self.bbox_patch
  1924. # is used.
  1925. self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
  1926. if "patchA" in d:
  1927. self.arrow_patch.set_patchA(d.pop("patchA"))
  1928. else:
  1929. if self._bbox_patch:
  1930. self.arrow_patch.set_patchA(self._bbox_patch)
  1931. else:
  1932. pad = renderer.points_to_pixels(4)
  1933. if self.get_text() == "":
  1934. self.arrow_patch.set_patchA(None)
  1935. return
  1936. bbox = Text.get_window_extent(self, renderer)
  1937. l, b, w, h = bbox.bounds
  1938. l -= pad / 2.
  1939. b -= pad / 2.
  1940. w += pad
  1941. h += pad
  1942. r = Rectangle(xy=(l, b),
  1943. width=w,
  1944. height=h,
  1945. )
  1946. r.set_transform(IdentityTransform())
  1947. r.set_clip_on(False)
  1948. self.arrow_patch.set_patchA(r)
  1949. @artist.allow_rasterization
  1950. def draw(self, renderer):
  1951. """
  1952. Draw the :class:`Annotation` object to the given *renderer*.
  1953. """
  1954. if renderer is not None:
  1955. self._renderer = renderer
  1956. if not self.get_visible():
  1957. return
  1958. xy_pixel = self._get_position_xy(renderer)
  1959. if not self._check_xy(renderer, xy_pixel):
  1960. return
  1961. self._update_position_xytext(renderer, xy_pixel)
  1962. self.update_bbox_position_size(renderer)
  1963. if self.arrow_patch is not None: # FancyArrowPatch
  1964. if self.arrow_patch.figure is None and self.figure is not None:
  1965. self.arrow_patch.figure = self.figure
  1966. self.arrow_patch.draw(renderer)
  1967. # Draw text, including FancyBboxPatch, after FancyArrowPatch.
  1968. # Otherwise, a wedge arrowstyle can land partly on top of the Bbox.
  1969. Text.draw(self, renderer)
  1970. def get_window_extent(self, renderer=None):
  1971. """
  1972. Return the `Bbox` bounding the text and arrow, in display units.
  1973. Parameters
  1974. ----------
  1975. renderer : Renderer, optional
  1976. A renderer is needed to compute the bounding box. If the artist
  1977. has already been drawn, the renderer is cached; thus, it is only
  1978. necessary to pass this argument when calling `get_window_extent`
  1979. before the first `draw`. In practice, it is usually easier to
  1980. trigger a draw first (e.g. by saving the figure).
  1981. """
  1982. # This block is the same as in Text.get_window_extent, but we need to
  1983. # set the renderer before calling update_positions().
  1984. if not self.get_visible():
  1985. return Bbox.unit()
  1986. if renderer is not None:
  1987. self._renderer = renderer
  1988. if self._renderer is None:
  1989. self._renderer = self.figure._cachedRenderer
  1990. if self._renderer is None:
  1991. raise RuntimeError('Cannot get window extent w/o renderer')
  1992. self.update_positions(self._renderer)
  1993. text_bbox = Text.get_window_extent(self)
  1994. bboxes = [text_bbox]
  1995. if self.arrow_patch is not None:
  1996. bboxes.append(self.arrow_patch.get_window_extent())
  1997. return Bbox.union(bboxes)
  1998. arrow = property(
  1999. fget=cbook.deprecated("3.0", message="arrow was deprecated in "
  2000. "Matplotlib 3.0 and will be removed in 3.2. Use arrow_patch "
  2001. "instead.")(lambda self: None),
  2002. fset=cbook.deprecated("3.0")(lambda self, value: None))
  2003. docstring.interpd.update(Annotation=Annotation.__init__.__doc__)