text.py 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035
  1. """
  2. Classes for including text in a figure.
  3. """
  4. import functools
  5. import logging
  6. import math
  7. from numbers import Real
  8. import weakref
  9. import numpy as np
  10. import matplotlib as mpl
  11. from . import _api, artist, cbook, _docstring
  12. from .artist import Artist
  13. from .font_manager import FontProperties
  14. from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle
  15. from .textpath import TextPath, TextToPath # noqa # Logically located here
  16. from .transforms import (
  17. Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform)
  18. _log = logging.getLogger(__name__)
  19. def _get_textbox(text, renderer):
  20. """
  21. Calculate the bounding box of the text.
  22. The bbox position takes text rotation into account, but the width and
  23. height are those of the unrotated box (unlike `.Text.get_window_extent`).
  24. """
  25. # TODO : This function may move into the Text class as a method. As a
  26. # matter of fact, the information from the _get_textbox function
  27. # should be available during the Text._get_layout() call, which is
  28. # called within the _get_textbox. So, it would better to move this
  29. # function as a method with some refactoring of _get_layout method.
  30. projected_xs = []
  31. projected_ys = []
  32. theta = np.deg2rad(text.get_rotation())
  33. tr = Affine2D().rotate(-theta)
  34. _, parts, d = text._get_layout(renderer)
  35. for t, wh, x, y in parts:
  36. w, h = wh
  37. xt1, yt1 = tr.transform((x, y))
  38. yt1 -= d
  39. xt2, yt2 = xt1 + w, yt1 + h
  40. projected_xs.extend([xt1, xt2])
  41. projected_ys.extend([yt1, yt2])
  42. xt_box, yt_box = min(projected_xs), min(projected_ys)
  43. w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
  44. x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box))
  45. return x_box, y_box, w_box, h_box
  46. def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi):
  47. """Call ``renderer.get_text_width_height_descent``, caching the results."""
  48. # Cached based on a copy of fontprop so that later in-place mutations of
  49. # the passed-in argument do not mess up the cache.
  50. return _get_text_metrics_with_cache_impl(
  51. weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
  52. @functools.lru_cache(4096)
  53. def _get_text_metrics_with_cache_impl(
  54. renderer_ref, text, fontprop, ismath, dpi):
  55. # dpi is unused, but participates in cache invalidation (via the renderer).
  56. return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
  57. @_docstring.interpd
  58. @_api.define_aliases({
  59. "color": ["c"],
  60. "fontproperties": ["font", "font_properties"],
  61. "fontfamily": ["family"],
  62. "fontname": ["name"],
  63. "fontsize": ["size"],
  64. "fontstretch": ["stretch"],
  65. "fontstyle": ["style"],
  66. "fontvariant": ["variant"],
  67. "fontweight": ["weight"],
  68. "horizontalalignment": ["ha"],
  69. "verticalalignment": ["va"],
  70. "multialignment": ["ma"],
  71. })
  72. class Text(Artist):
  73. """Handle storing and drawing of text in window or data coordinates."""
  74. zorder = 3
  75. _charsize_cache = dict()
  76. def __repr__(self):
  77. return f"Text({self._x}, {self._y}, {self._text!r})"
  78. def __init__(self,
  79. x=0, y=0, text='', *,
  80. color=None, # defaults to rc params
  81. verticalalignment='baseline',
  82. horizontalalignment='left',
  83. multialignment=None,
  84. fontproperties=None, # defaults to FontProperties()
  85. rotation=None,
  86. linespacing=None,
  87. rotation_mode=None,
  88. usetex=None, # defaults to rcParams['text.usetex']
  89. wrap=False,
  90. transform_rotates_text=False,
  91. parse_math=None, # defaults to rcParams['text.parse_math']
  92. antialiased=None, # defaults to rcParams['text.antialiased']
  93. **kwargs
  94. ):
  95. """
  96. Create a `.Text` instance at *x*, *y* with string *text*.
  97. The text is aligned relative to the anchor point (*x*, *y*) according
  98. to ``horizontalalignment`` (default: 'left') and ``verticalalignment``
  99. (default: 'baseline'). See also
  100. :doc:`/gallery/text_labels_and_annotations/text_alignment`.
  101. While Text accepts the 'label' keyword argument, by default it is not
  102. added to the handles of a legend.
  103. Valid keyword arguments are:
  104. %(Text:kwdoc)s
  105. """
  106. super().__init__()
  107. self._x, self._y = x, y
  108. self._text = ''
  109. self._reset_visual_defaults(
  110. text=text,
  111. color=color,
  112. fontproperties=fontproperties,
  113. usetex=usetex,
  114. parse_math=parse_math,
  115. wrap=wrap,
  116. verticalalignment=verticalalignment,
  117. horizontalalignment=horizontalalignment,
  118. multialignment=multialignment,
  119. rotation=rotation,
  120. transform_rotates_text=transform_rotates_text,
  121. linespacing=linespacing,
  122. rotation_mode=rotation_mode,
  123. antialiased=antialiased
  124. )
  125. self.update(kwargs)
  126. def _reset_visual_defaults(
  127. self,
  128. text='',
  129. color=None,
  130. fontproperties=None,
  131. usetex=None,
  132. parse_math=None,
  133. wrap=False,
  134. verticalalignment='baseline',
  135. horizontalalignment='left',
  136. multialignment=None,
  137. rotation=None,
  138. transform_rotates_text=False,
  139. linespacing=None,
  140. rotation_mode=None,
  141. antialiased=None
  142. ):
  143. self.set_text(text)
  144. self.set_color(mpl._val_or_rc(color, "text.color"))
  145. self.set_fontproperties(fontproperties)
  146. self.set_usetex(usetex)
  147. self.set_parse_math(mpl._val_or_rc(parse_math, 'text.parse_math'))
  148. self.set_wrap(wrap)
  149. self.set_verticalalignment(verticalalignment)
  150. self.set_horizontalalignment(horizontalalignment)
  151. self._multialignment = multialignment
  152. self.set_rotation(rotation)
  153. self._transform_rotates_text = transform_rotates_text
  154. self._bbox_patch = None # a FancyBboxPatch instance
  155. self._renderer = None
  156. if linespacing is None:
  157. linespacing = 1.2 # Maybe use rcParam later.
  158. self.set_linespacing(linespacing)
  159. self.set_rotation_mode(rotation_mode)
  160. self.set_antialiased(antialiased if antialiased is not None else
  161. mpl.rcParams['text.antialiased'])
  162. def update(self, kwargs):
  163. # docstring inherited
  164. ret = []
  165. kwargs = cbook.normalize_kwargs(kwargs, Text)
  166. sentinel = object() # bbox can be None, so use another sentinel.
  167. # Update fontproperties first, as it has lowest priority.
  168. fontproperties = kwargs.pop("fontproperties", sentinel)
  169. if fontproperties is not sentinel:
  170. ret.append(self.set_fontproperties(fontproperties))
  171. # Update bbox last, as it depends on font properties.
  172. bbox = kwargs.pop("bbox", sentinel)
  173. ret.extend(super().update(kwargs))
  174. if bbox is not sentinel:
  175. ret.append(self.set_bbox(bbox))
  176. return ret
  177. def __getstate__(self):
  178. d = super().__getstate__()
  179. # remove the cached _renderer (if it exists)
  180. d['_renderer'] = None
  181. return d
  182. def contains(self, mouseevent):
  183. """
  184. Return whether the mouse event occurred inside the axis-aligned
  185. bounding-box of the text.
  186. """
  187. if (self._different_canvas(mouseevent) or not self.get_visible()
  188. or self._renderer is None):
  189. return False, {}
  190. # Explicitly use Text.get_window_extent(self) and not
  191. # self.get_window_extent() so that Annotation.contains does not
  192. # accidentally cover the entire annotation bounding box.
  193. bbox = Text.get_window_extent(self)
  194. inside = (bbox.x0 <= mouseevent.x <= bbox.x1
  195. and bbox.y0 <= mouseevent.y <= bbox.y1)
  196. cattr = {}
  197. # if the text has a surrounding patch, also check containment for it,
  198. # and merge the results with the results for the text.
  199. if self._bbox_patch:
  200. patch_inside, patch_cattr = self._bbox_patch.contains(mouseevent)
  201. inside = inside or patch_inside
  202. cattr["bbox_patch"] = patch_cattr
  203. return inside, cattr
  204. def _get_xy_display(self):
  205. """
  206. Get the (possibly unit converted) transformed x, y in display coords.
  207. """
  208. x, y = self.get_unitless_position()
  209. return self.get_transform().transform((x, y))
  210. def _get_multialignment(self):
  211. if self._multialignment is not None:
  212. return self._multialignment
  213. else:
  214. return self._horizontalalignment
  215. def _char_index_at(self, x):
  216. """
  217. Calculate the index closest to the coordinate x in display space.
  218. The position of text[index] is assumed to be the sum of the widths
  219. of all preceding characters text[:index].
  220. This works only on single line texts.
  221. """
  222. if not self._text:
  223. return 0
  224. text = self._text
  225. fontproperties = str(self._fontproperties)
  226. if fontproperties not in Text._charsize_cache:
  227. Text._charsize_cache[fontproperties] = dict()
  228. charsize_cache = Text._charsize_cache[fontproperties]
  229. for char in set(text):
  230. if char not in charsize_cache:
  231. self.set_text(char)
  232. bb = self.get_window_extent()
  233. charsize_cache[char] = bb.x1 - bb.x0
  234. self.set_text(text)
  235. bb = self.get_window_extent()
  236. size_accum = np.cumsum([0] + [charsize_cache[x] for x in text])
  237. std_x = x - bb.x0
  238. return (np.abs(size_accum - std_x)).argmin()
  239. def get_rotation(self):
  240. """Return the text angle in degrees between 0 and 360."""
  241. if self.get_transform_rotates_text():
  242. return self.get_transform().transform_angles(
  243. [self._rotation], [self.get_unitless_position()]).item(0)
  244. else:
  245. return self._rotation
  246. def get_transform_rotates_text(self):
  247. """
  248. Return whether rotations of the transform affect the text direction.
  249. """
  250. return self._transform_rotates_text
  251. def set_rotation_mode(self, m):
  252. """
  253. Set text rotation mode.
  254. Parameters
  255. ----------
  256. m : {None, 'default', 'anchor'}
  257. If ``"default"``, the text will be first rotated, then aligned according
  258. to their horizontal and vertical alignments. If ``"anchor"``, then
  259. alignment occurs before rotation. Passing ``None`` will set the rotation
  260. mode to ``"default"``.
  261. """
  262. if m is None:
  263. m = "default"
  264. else:
  265. _api.check_in_list(("anchor", "default"), rotation_mode=m)
  266. self._rotation_mode = m
  267. self.stale = True
  268. def get_rotation_mode(self):
  269. """Return the text rotation mode."""
  270. return self._rotation_mode
  271. def set_antialiased(self, antialiased):
  272. """
  273. Set whether to use antialiased rendering.
  274. Parameters
  275. ----------
  276. antialiased : bool
  277. Notes
  278. -----
  279. Antialiasing will be determined by :rc:`text.antialiased`
  280. and the parameter *antialiased* will have no effect if the text contains
  281. math expressions.
  282. """
  283. self._antialiased = antialiased
  284. self.stale = True
  285. def get_antialiased(self):
  286. """Return whether antialiased rendering is used."""
  287. return self._antialiased
  288. def update_from(self, other):
  289. # docstring inherited
  290. super().update_from(other)
  291. self._color = other._color
  292. self._multialignment = other._multialignment
  293. self._verticalalignment = other._verticalalignment
  294. self._horizontalalignment = other._horizontalalignment
  295. self._fontproperties = other._fontproperties.copy()
  296. self._usetex = other._usetex
  297. self._rotation = other._rotation
  298. self._transform_rotates_text = other._transform_rotates_text
  299. self._picker = other._picker
  300. self._linespacing = other._linespacing
  301. self._antialiased = other._antialiased
  302. self.stale = True
  303. def _get_layout(self, renderer):
  304. """
  305. Return the extent (bbox) of the text together with
  306. multiple-alignment information. Note that it returns an extent
  307. of a rotated text when necessary.
  308. """
  309. thisx, thisy = 0.0, 0.0
  310. lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty.
  311. ws = []
  312. hs = []
  313. xs = []
  314. ys = []
  315. # Full vertical extent of font, including ascenders and descenders:
  316. _, lp_h, lp_d = _get_text_metrics_with_cache(
  317. renderer, "lp", self._fontproperties,
  318. ismath="TeX" if self.get_usetex() else False,
  319. dpi=self.get_figure(root=True).dpi)
  320. min_dy = (lp_h - lp_d) * self._linespacing
  321. for i, line in enumerate(lines):
  322. clean_line, ismath = self._preprocess_math(line)
  323. if clean_line:
  324. w, h, d = _get_text_metrics_with_cache(
  325. renderer, clean_line, self._fontproperties,
  326. ismath=ismath, dpi=self.get_figure(root=True).dpi)
  327. else:
  328. w = h = d = 0
  329. # For multiline text, increase the line spacing when the text
  330. # net-height (excluding baseline) is larger than that of a "l"
  331. # (e.g., use of superscripts), which seems what TeX does.
  332. h = max(h, lp_h)
  333. d = max(d, lp_d)
  334. ws.append(w)
  335. hs.append(h)
  336. # Metrics of the last line that are needed later:
  337. baseline = (h - d) - thisy
  338. if i == 0:
  339. # position at baseline
  340. thisy = -(h - d)
  341. else:
  342. # put baseline a good distance from bottom of previous line
  343. thisy -= max(min_dy, (h - d) * self._linespacing)
  344. xs.append(thisx) # == 0.
  345. ys.append(thisy)
  346. thisy -= d
  347. # Metrics of the last line that are needed later:
  348. descent = d
  349. # Bounding box definition:
  350. width = max(ws)
  351. xmin = 0
  352. xmax = width
  353. ymax = 0
  354. ymin = ys[-1] - descent # baseline of last line minus its descent
  355. # get the rotation matrix
  356. M = Affine2D().rotate_deg(self.get_rotation())
  357. # now offset the individual text lines within the box
  358. malign = self._get_multialignment()
  359. if malign == 'left':
  360. offset_layout = [(x, y) for x, y in zip(xs, ys)]
  361. elif malign == 'center':
  362. offset_layout = [(x + width / 2 - w / 2, y)
  363. for x, y, w in zip(xs, ys, ws)]
  364. elif malign == 'right':
  365. offset_layout = [(x + width - w, y)
  366. for x, y, w in zip(xs, ys, ws)]
  367. # the corners of the unrotated bounding box
  368. corners_horiz = np.array(
  369. [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
  370. # now rotate the bbox
  371. corners_rotated = M.transform(corners_horiz)
  372. # compute the bounds of the rotated box
  373. xmin = corners_rotated[:, 0].min()
  374. xmax = corners_rotated[:, 0].max()
  375. ymin = corners_rotated[:, 1].min()
  376. ymax = corners_rotated[:, 1].max()
  377. width = xmax - xmin
  378. height = ymax - ymin
  379. # Now move the box to the target position offset the display
  380. # bbox by alignment
  381. halign = self._horizontalalignment
  382. valign = self._verticalalignment
  383. rotation_mode = self.get_rotation_mode()
  384. if rotation_mode != "anchor":
  385. # compute the text location in display coords and the offsets
  386. # necessary to align the bbox with that location
  387. if halign == 'center':
  388. offsetx = (xmin + xmax) / 2
  389. elif halign == 'right':
  390. offsetx = xmax
  391. else:
  392. offsetx = xmin
  393. if valign == 'center':
  394. offsety = (ymin + ymax) / 2
  395. elif valign == 'top':
  396. offsety = ymax
  397. elif valign == 'baseline':
  398. offsety = ymin + descent
  399. elif valign == 'center_baseline':
  400. offsety = ymin + height - baseline / 2.0
  401. else:
  402. offsety = ymin
  403. else:
  404. xmin1, ymin1 = corners_horiz[0]
  405. xmax1, ymax1 = corners_horiz[2]
  406. if halign == 'center':
  407. offsetx = (xmin1 + xmax1) / 2.0
  408. elif halign == 'right':
  409. offsetx = xmax1
  410. else:
  411. offsetx = xmin1
  412. if valign == 'center':
  413. offsety = (ymin1 + ymax1) / 2.0
  414. elif valign == 'top':
  415. offsety = ymax1
  416. elif valign == 'baseline':
  417. offsety = ymax1 - baseline
  418. elif valign == 'center_baseline':
  419. offsety = ymax1 - baseline / 2.0
  420. else:
  421. offsety = ymin1
  422. offsetx, offsety = M.transform((offsetx, offsety))
  423. xmin -= offsetx
  424. ymin -= offsety
  425. bbox = Bbox.from_bounds(xmin, ymin, width, height)
  426. # now rotate the positions around the first (x, y) position
  427. xys = M.transform(offset_layout) - (offsetx, offsety)
  428. return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
  429. def set_bbox(self, rectprops):
  430. """
  431. Draw a bounding box around self.
  432. Parameters
  433. ----------
  434. rectprops : dict with properties for `.patches.FancyBboxPatch`
  435. The default boxstyle is 'square'. The mutation
  436. scale of the `.patches.FancyBboxPatch` is set to the fontsize.
  437. Examples
  438. --------
  439. ::
  440. t.set_bbox(dict(facecolor='red', alpha=0.5))
  441. """
  442. if rectprops is not None:
  443. props = rectprops.copy()
  444. boxstyle = props.pop("boxstyle", None)
  445. pad = props.pop("pad", None)
  446. if boxstyle is None:
  447. boxstyle = "square"
  448. if pad is None:
  449. pad = 4 # points
  450. pad /= self.get_size() # to fraction of font size
  451. else:
  452. if pad is None:
  453. pad = 0.3
  454. # boxstyle could be a callable or a string
  455. if isinstance(boxstyle, str) and "pad" not in boxstyle:
  456. boxstyle += ",pad=%0.2f" % pad
  457. self._bbox_patch = FancyBboxPatch(
  458. (0, 0), 1, 1,
  459. boxstyle=boxstyle, transform=IdentityTransform(), **props)
  460. else:
  461. self._bbox_patch = None
  462. self._update_clip_properties()
  463. def get_bbox_patch(self):
  464. """
  465. Return the bbox Patch, or None if the `.patches.FancyBboxPatch`
  466. is not made.
  467. """
  468. return self._bbox_patch
  469. def update_bbox_position_size(self, renderer):
  470. """
  471. Update the location and the size of the bbox.
  472. This method should be used when the position and size of the bbox needs
  473. to be updated before actually drawing the bbox.
  474. """
  475. if self._bbox_patch:
  476. # don't use self.get_unitless_position here, which refers to text
  477. # position in Text:
  478. posx = float(self.convert_xunits(self._x))
  479. posy = float(self.convert_yunits(self._y))
  480. posx, posy = self.get_transform().transform((posx, posy))
  481. x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
  482. self._bbox_patch.set_bounds(0., 0., w_box, h_box)
  483. self._bbox_patch.set_transform(
  484. Affine2D()
  485. .rotate_deg(self.get_rotation())
  486. .translate(posx + x_box, posy + y_box))
  487. fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
  488. self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
  489. def _update_clip_properties(self):
  490. if self._bbox_patch:
  491. clipprops = dict(clip_box=self.clipbox,
  492. clip_path=self._clippath,
  493. clip_on=self._clipon)
  494. self._bbox_patch.update(clipprops)
  495. def set_clip_box(self, clipbox):
  496. # docstring inherited.
  497. super().set_clip_box(clipbox)
  498. self._update_clip_properties()
  499. def set_clip_path(self, path, transform=None):
  500. # docstring inherited.
  501. super().set_clip_path(path, transform)
  502. self._update_clip_properties()
  503. def set_clip_on(self, b):
  504. # docstring inherited.
  505. super().set_clip_on(b)
  506. self._update_clip_properties()
  507. def get_wrap(self):
  508. """Return whether the text can be wrapped."""
  509. return self._wrap
  510. def set_wrap(self, wrap):
  511. """
  512. Set whether the text can be wrapped.
  513. Wrapping makes sure the text is confined to the (sub)figure box. It
  514. does not take into account any other artists.
  515. Parameters
  516. ----------
  517. wrap : bool
  518. Notes
  519. -----
  520. Wrapping does not work together with
  521. ``savefig(..., bbox_inches='tight')`` (which is also used internally
  522. by ``%matplotlib inline`` in IPython/Jupyter). The 'tight' setting
  523. rescales the canvas to accommodate all content and happens before
  524. wrapping.
  525. """
  526. self._wrap = wrap
  527. def _get_wrap_line_width(self):
  528. """
  529. Return the maximum line width for wrapping text based on the current
  530. orientation.
  531. """
  532. x0, y0 = self.get_transform().transform(self.get_position())
  533. figure_box = self.get_figure().get_window_extent()
  534. # Calculate available width based on text alignment
  535. alignment = self.get_horizontalalignment()
  536. self.set_rotation_mode('anchor')
  537. rotation = self.get_rotation()
  538. left = self._get_dist_to_box(rotation, x0, y0, figure_box)
  539. right = self._get_dist_to_box(
  540. (180 + rotation) % 360, x0, y0, figure_box)
  541. if alignment == 'left':
  542. line_width = left
  543. elif alignment == 'right':
  544. line_width = right
  545. else:
  546. line_width = 2 * min(left, right)
  547. return line_width
  548. def _get_dist_to_box(self, rotation, x0, y0, figure_box):
  549. """
  550. Return the distance from the given points to the boundaries of a
  551. rotated box, in pixels.
  552. """
  553. if rotation > 270:
  554. quad = rotation - 270
  555. h1 = (y0 - figure_box.y0) / math.cos(math.radians(quad))
  556. h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad))
  557. elif rotation > 180:
  558. quad = rotation - 180
  559. h1 = (x0 - figure_box.x0) / math.cos(math.radians(quad))
  560. h2 = (y0 - figure_box.y0) / math.cos(math.radians(90 - quad))
  561. elif rotation > 90:
  562. quad = rotation - 90
  563. h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad))
  564. h2 = (x0 - figure_box.x0) / math.cos(math.radians(90 - quad))
  565. else:
  566. h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation))
  567. h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation))
  568. return min(h1, h2)
  569. def _get_rendered_text_width(self, text):
  570. """
  571. Return the width of a given text string, in pixels.
  572. """
  573. w, h, d = _get_text_metrics_with_cache(
  574. self._renderer, text, self.get_fontproperties(),
  575. cbook.is_math_text(text),
  576. self.get_figure(root=True).dpi)
  577. return math.ceil(w)
  578. def _get_wrapped_text(self):
  579. """
  580. Return a copy of the text string with new lines added so that the text
  581. is wrapped relative to the parent figure (if `get_wrap` is True).
  582. """
  583. if not self.get_wrap():
  584. return self.get_text()
  585. # Not fit to handle breaking up latex syntax correctly, so
  586. # ignore latex for now.
  587. if self.get_usetex():
  588. return self.get_text()
  589. # Build the line incrementally, for a more accurate measure of length
  590. line_width = self._get_wrap_line_width()
  591. wrapped_lines = []
  592. # New lines in the user's text force a split
  593. unwrapped_lines = self.get_text().split('\n')
  594. # Now wrap each individual unwrapped line
  595. for unwrapped_line in unwrapped_lines:
  596. sub_words = unwrapped_line.split(' ')
  597. # Remove items from sub_words as we go, so stop when empty
  598. while len(sub_words) > 0:
  599. if len(sub_words) == 1:
  600. # Only one word, so just add it to the end
  601. wrapped_lines.append(sub_words.pop(0))
  602. continue
  603. for i in range(2, len(sub_words) + 1):
  604. # Get width of all words up to and including here
  605. line = ' '.join(sub_words[:i])
  606. current_width = self._get_rendered_text_width(line)
  607. # If all these words are too wide, append all not including
  608. # last word
  609. if current_width > line_width:
  610. wrapped_lines.append(' '.join(sub_words[:i - 1]))
  611. sub_words = sub_words[i - 1:]
  612. break
  613. # Otherwise if all words fit in the width, append them all
  614. elif i == len(sub_words):
  615. wrapped_lines.append(' '.join(sub_words[:i]))
  616. sub_words = []
  617. break
  618. return '\n'.join(wrapped_lines)
  619. @artist.allow_rasterization
  620. def draw(self, renderer):
  621. # docstring inherited
  622. if renderer is not None:
  623. self._renderer = renderer
  624. if not self.get_visible():
  625. return
  626. if self.get_text() == '':
  627. return
  628. renderer.open_group('text', self.get_gid())
  629. with self._cm_set(text=self._get_wrapped_text()):
  630. bbox, info, descent = self._get_layout(renderer)
  631. trans = self.get_transform()
  632. # don't use self.get_position here, which refers to text
  633. # position in Text:
  634. x, y = self._x, self._y
  635. if np.ma.is_masked(x):
  636. x = np.nan
  637. if np.ma.is_masked(y):
  638. y = np.nan
  639. posx = float(self.convert_xunits(x))
  640. posy = float(self.convert_yunits(y))
  641. posx, posy = trans.transform((posx, posy))
  642. if np.isnan(posx) or np.isnan(posy):
  643. return # don't throw a warning here
  644. if not np.isfinite(posx) or not np.isfinite(posy):
  645. _log.warning("posx and posy should be finite values")
  646. return
  647. canvasw, canvash = renderer.get_canvas_width_height()
  648. # Update the location and size of the bbox
  649. # (`.patches.FancyBboxPatch`), and draw it.
  650. if self._bbox_patch:
  651. self.update_bbox_position_size(renderer)
  652. self._bbox_patch.draw(renderer)
  653. gc = renderer.new_gc()
  654. gc.set_foreground(self.get_color())
  655. gc.set_alpha(self.get_alpha())
  656. gc.set_url(self._url)
  657. gc.set_antialiased(self._antialiased)
  658. self._set_gc_clip(gc)
  659. angle = self.get_rotation()
  660. for line, wh, x, y in info:
  661. mtext = self if len(info) == 1 else None
  662. x = x + posx
  663. y = y + posy
  664. if renderer.flipy():
  665. y = canvash - y
  666. clean_line, ismath = self._preprocess_math(line)
  667. if self.get_path_effects():
  668. from matplotlib.patheffects import PathEffectRenderer
  669. textrenderer = PathEffectRenderer(
  670. self.get_path_effects(), renderer)
  671. else:
  672. textrenderer = renderer
  673. if self.get_usetex():
  674. textrenderer.draw_tex(gc, x, y, clean_line,
  675. self._fontproperties, angle,
  676. mtext=mtext)
  677. else:
  678. textrenderer.draw_text(gc, x, y, clean_line,
  679. self._fontproperties, angle,
  680. ismath=ismath, mtext=mtext)
  681. gc.restore()
  682. renderer.close_group('text')
  683. self.stale = False
  684. def get_color(self):
  685. """Return the color of the text."""
  686. return self._color
  687. def get_fontproperties(self):
  688. """Return the `.font_manager.FontProperties`."""
  689. return self._fontproperties
  690. def get_fontfamily(self):
  691. """
  692. Return the list of font families used for font lookup.
  693. See Also
  694. --------
  695. .font_manager.FontProperties.get_family
  696. """
  697. return self._fontproperties.get_family()
  698. def get_fontname(self):
  699. """
  700. Return the font name as a string.
  701. See Also
  702. --------
  703. .font_manager.FontProperties.get_name
  704. """
  705. return self._fontproperties.get_name()
  706. def get_fontstyle(self):
  707. """
  708. Return the font style as a string.
  709. See Also
  710. --------
  711. .font_manager.FontProperties.get_style
  712. """
  713. return self._fontproperties.get_style()
  714. def get_fontsize(self):
  715. """
  716. Return the font size as an integer.
  717. See Also
  718. --------
  719. .font_manager.FontProperties.get_size_in_points
  720. """
  721. return self._fontproperties.get_size_in_points()
  722. def get_fontvariant(self):
  723. """
  724. Return the font variant as a string.
  725. See Also
  726. --------
  727. .font_manager.FontProperties.get_variant
  728. """
  729. return self._fontproperties.get_variant()
  730. def get_fontweight(self):
  731. """
  732. Return the font weight as a string or a number.
  733. See Also
  734. --------
  735. .font_manager.FontProperties.get_weight
  736. """
  737. return self._fontproperties.get_weight()
  738. def get_stretch(self):
  739. """
  740. Return the font stretch as a string or a number.
  741. See Also
  742. --------
  743. .font_manager.FontProperties.get_stretch
  744. """
  745. return self._fontproperties.get_stretch()
  746. def get_horizontalalignment(self):
  747. """
  748. Return the horizontal alignment as a string. Will be one of
  749. 'left', 'center' or 'right'.
  750. """
  751. return self._horizontalalignment
  752. def get_unitless_position(self):
  753. """Return the (x, y) unitless position of the text."""
  754. # This will get the position with all unit information stripped away.
  755. # This is here for convenience since it is done in several locations.
  756. x = float(self.convert_xunits(self._x))
  757. y = float(self.convert_yunits(self._y))
  758. return x, y
  759. def get_position(self):
  760. """Return the (x, y) position of the text."""
  761. # This should return the same data (possible unitized) as was
  762. # specified with 'set_x' and 'set_y'.
  763. return self._x, self._y
  764. def get_text(self):
  765. """Return the text string."""
  766. return self._text
  767. def get_verticalalignment(self):
  768. """
  769. Return the vertical alignment as a string. Will be one of
  770. 'top', 'center', 'bottom', 'baseline' or 'center_baseline'.
  771. """
  772. return self._verticalalignment
  773. def get_window_extent(self, renderer=None, dpi=None):
  774. """
  775. Return the `.Bbox` bounding the text, in display units.
  776. In addition to being used internally, this is useful for specifying
  777. clickable regions in a png file on a web page.
  778. Parameters
  779. ----------
  780. renderer : Renderer, optional
  781. A renderer is needed to compute the bounding box. If the artist
  782. has already been drawn, the renderer is cached; thus, it is only
  783. necessary to pass this argument when calling `get_window_extent`
  784. before the first draw. In practice, it is usually easier to
  785. trigger a draw first, e.g. by calling
  786. `~.Figure.draw_without_rendering` or ``plt.show()``.
  787. dpi : float, optional
  788. The dpi value for computing the bbox, defaults to
  789. ``self.get_figure(root=True).dpi`` (*not* the renderer dpi); should be set
  790. e.g. if to match regions with a figure saved with a custom dpi value.
  791. """
  792. if not self.get_visible():
  793. return Bbox.unit()
  794. fig = self.get_figure(root=True)
  795. if dpi is None:
  796. dpi = fig.dpi
  797. if self.get_text() == '':
  798. with cbook._setattr_cm(fig, dpi=dpi):
  799. tx, ty = self._get_xy_display()
  800. return Bbox.from_bounds(tx, ty, 0, 0)
  801. if renderer is not None:
  802. self._renderer = renderer
  803. if self._renderer is None:
  804. self._renderer = fig._get_renderer()
  805. if self._renderer is None:
  806. raise RuntimeError(
  807. "Cannot get window extent of text w/o renderer. You likely "
  808. "want to call 'figure.draw_without_rendering()' first.")
  809. with cbook._setattr_cm(fig, dpi=dpi):
  810. bbox, info, descent = self._get_layout(self._renderer)
  811. x, y = self.get_unitless_position()
  812. x, y = self.get_transform().transform((x, y))
  813. bbox = bbox.translated(x, y)
  814. return bbox
  815. def set_backgroundcolor(self, color):
  816. """
  817. Set the background color of the text by updating the bbox.
  818. Parameters
  819. ----------
  820. color : :mpltype:`color`
  821. See Also
  822. --------
  823. .set_bbox : To change the position of the bounding box
  824. """
  825. if self._bbox_patch is None:
  826. self.set_bbox(dict(facecolor=color, edgecolor=color))
  827. else:
  828. self._bbox_patch.update(dict(facecolor=color))
  829. self._update_clip_properties()
  830. self.stale = True
  831. def set_color(self, color):
  832. """
  833. Set the foreground color of the text
  834. Parameters
  835. ----------
  836. color : :mpltype:`color`
  837. """
  838. # "auto" is only supported by axisartist, but we can just let it error
  839. # out at draw time for simplicity.
  840. if not cbook._str_equal(color, "auto"):
  841. mpl.colors._check_color_like(color=color)
  842. self._color = color
  843. self.stale = True
  844. def set_horizontalalignment(self, align):
  845. """
  846. Set the horizontal alignment relative to the anchor point.
  847. See also :doc:`/gallery/text_labels_and_annotations/text_alignment`.
  848. Parameters
  849. ----------
  850. align : {'left', 'center', 'right'}
  851. """
  852. _api.check_in_list(['center', 'right', 'left'], align=align)
  853. self._horizontalalignment = align
  854. self.stale = True
  855. def set_multialignment(self, align):
  856. """
  857. Set the text alignment for multiline texts.
  858. The layout of the bounding box of all the lines is determined by the
  859. horizontalalignment and verticalalignment properties. This property
  860. controls the alignment of the text lines within that box.
  861. Parameters
  862. ----------
  863. align : {'left', 'right', 'center'}
  864. """
  865. _api.check_in_list(['center', 'right', 'left'], align=align)
  866. self._multialignment = align
  867. self.stale = True
  868. def set_linespacing(self, spacing):
  869. """
  870. Set the line spacing as a multiple of the font size.
  871. The default line spacing is 1.2.
  872. Parameters
  873. ----------
  874. spacing : float (multiple of font size)
  875. """
  876. _api.check_isinstance(Real, spacing=spacing)
  877. self._linespacing = spacing
  878. self.stale = True
  879. def set_fontfamily(self, fontname):
  880. """
  881. Set the font family. Can be either a single string, or a list of
  882. strings in decreasing priority. Each string may be either a real font
  883. name or a generic font class name. If the latter, the specific font
  884. names will be looked up in the corresponding rcParams.
  885. If a `Text` instance is constructed with ``fontfamily=None``, then the
  886. font is set to :rc:`font.family`, and the
  887. same is done when `set_fontfamily()` is called on an existing
  888. `Text` instance.
  889. Parameters
  890. ----------
  891. fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
  892. 'monospace'}
  893. See Also
  894. --------
  895. .font_manager.FontProperties.set_family
  896. """
  897. self._fontproperties.set_family(fontname)
  898. self.stale = True
  899. def set_fontvariant(self, variant):
  900. """
  901. Set the font variant.
  902. Parameters
  903. ----------
  904. variant : {'normal', 'small-caps'}
  905. See Also
  906. --------
  907. .font_manager.FontProperties.set_variant
  908. """
  909. self._fontproperties.set_variant(variant)
  910. self.stale = True
  911. def set_fontstyle(self, fontstyle):
  912. """
  913. Set the font style.
  914. Parameters
  915. ----------
  916. fontstyle : {'normal', 'italic', 'oblique'}
  917. See Also
  918. --------
  919. .font_manager.FontProperties.set_style
  920. """
  921. self._fontproperties.set_style(fontstyle)
  922. self.stale = True
  923. def set_fontsize(self, fontsize):
  924. """
  925. Set the font size.
  926. Parameters
  927. ----------
  928. fontsize : float or {'xx-small', 'x-small', 'small', 'medium', \
  929. 'large', 'x-large', 'xx-large'}
  930. If a float, the fontsize in points. The string values denote sizes
  931. relative to the default font size.
  932. See Also
  933. --------
  934. .font_manager.FontProperties.set_size
  935. """
  936. self._fontproperties.set_size(fontsize)
  937. self.stale = True
  938. def get_math_fontfamily(self):
  939. """
  940. Return the font family name for math text rendered by Matplotlib.
  941. The default value is :rc:`mathtext.fontset`.
  942. See Also
  943. --------
  944. set_math_fontfamily
  945. """
  946. return self._fontproperties.get_math_fontfamily()
  947. def set_math_fontfamily(self, fontfamily):
  948. """
  949. Set the font family for math text rendered by Matplotlib.
  950. This does only affect Matplotlib's own math renderer. It has no effect
  951. when rendering with TeX (``usetex=True``).
  952. Parameters
  953. ----------
  954. fontfamily : str
  955. The name of the font family.
  956. Available font families are defined in the
  957. :ref:`default matplotlibrc file
  958. <customizing-with-matplotlibrc-files>`.
  959. See Also
  960. --------
  961. get_math_fontfamily
  962. """
  963. self._fontproperties.set_math_fontfamily(fontfamily)
  964. def set_fontweight(self, weight):
  965. """
  966. Set the font weight.
  967. Parameters
  968. ----------
  969. weight : {a numeric value in range 0-1000, 'ultralight', 'light', \
  970. 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', \
  971. 'demi', 'bold', 'heavy', 'extra bold', 'black'}
  972. See Also
  973. --------
  974. .font_manager.FontProperties.set_weight
  975. """
  976. self._fontproperties.set_weight(weight)
  977. self.stale = True
  978. def set_fontstretch(self, stretch):
  979. """
  980. Set the font stretch (horizontal condensation or expansion).
  981. Parameters
  982. ----------
  983. stretch : {a numeric value in range 0-1000, 'ultra-condensed', \
  984. 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', \
  985. 'expanded', 'extra-expanded', 'ultra-expanded'}
  986. See Also
  987. --------
  988. .font_manager.FontProperties.set_stretch
  989. """
  990. self._fontproperties.set_stretch(stretch)
  991. self.stale = True
  992. def set_position(self, xy):
  993. """
  994. Set the (*x*, *y*) position of the text.
  995. Parameters
  996. ----------
  997. xy : (float, float)
  998. """
  999. self.set_x(xy[0])
  1000. self.set_y(xy[1])
  1001. def set_x(self, x):
  1002. """
  1003. Set the *x* position of the text.
  1004. Parameters
  1005. ----------
  1006. x : float
  1007. """
  1008. self._x = x
  1009. self.stale = True
  1010. def set_y(self, y):
  1011. """
  1012. Set the *y* position of the text.
  1013. Parameters
  1014. ----------
  1015. y : float
  1016. """
  1017. self._y = y
  1018. self.stale = True
  1019. def set_rotation(self, s):
  1020. """
  1021. Set the rotation of the text.
  1022. Parameters
  1023. ----------
  1024. s : float or {'vertical', 'horizontal'}
  1025. The rotation angle in degrees in mathematically positive direction
  1026. (counterclockwise). 'horizontal' equals 0, 'vertical' equals 90.
  1027. """
  1028. if isinstance(s, Real):
  1029. self._rotation = float(s) % 360
  1030. elif cbook._str_equal(s, 'horizontal') or s is None:
  1031. self._rotation = 0.
  1032. elif cbook._str_equal(s, 'vertical'):
  1033. self._rotation = 90.
  1034. else:
  1035. raise ValueError("rotation must be 'vertical', 'horizontal' or "
  1036. f"a number, not {s}")
  1037. self.stale = True
  1038. def set_transform_rotates_text(self, t):
  1039. """
  1040. Whether rotations of the transform affect the text direction.
  1041. Parameters
  1042. ----------
  1043. t : bool
  1044. """
  1045. self._transform_rotates_text = t
  1046. self.stale = True
  1047. def set_verticalalignment(self, align):
  1048. """
  1049. Set the vertical alignment relative to the anchor point.
  1050. See also :doc:`/gallery/text_labels_and_annotations/text_alignment`.
  1051. Parameters
  1052. ----------
  1053. align : {'baseline', 'bottom', 'center', 'center_baseline', 'top'}
  1054. """
  1055. _api.check_in_list(
  1056. ['top', 'bottom', 'center', 'baseline', 'center_baseline'],
  1057. align=align)
  1058. self._verticalalignment = align
  1059. self.stale = True
  1060. def set_text(self, s):
  1061. r"""
  1062. Set the text string *s*.
  1063. It may contain newlines (``\n``) or math in LaTeX syntax.
  1064. Parameters
  1065. ----------
  1066. s : object
  1067. Any object gets converted to its `str` representation, except for
  1068. ``None`` which is converted to an empty string.
  1069. """
  1070. s = '' if s is None else str(s)
  1071. if s != self._text:
  1072. self._text = s
  1073. self.stale = True
  1074. def _preprocess_math(self, s):
  1075. """
  1076. Return the string *s* after mathtext preprocessing, and the kind of
  1077. mathtext support needed.
  1078. - If *self* is configured to use TeX, return *s* unchanged except that
  1079. a single space gets escaped, and the flag "TeX".
  1080. - Otherwise, if *s* is mathtext (has an even number of unescaped dollar
  1081. signs) and ``parse_math`` is not set to False, return *s* and the
  1082. flag True.
  1083. - Otherwise, return *s* with dollar signs unescaped, and the flag
  1084. False.
  1085. """
  1086. if self.get_usetex():
  1087. if s == " ":
  1088. s = r"\ "
  1089. return s, "TeX"
  1090. elif not self.get_parse_math():
  1091. return s, False
  1092. elif cbook.is_math_text(s):
  1093. return s, True
  1094. else:
  1095. return s.replace(r"\$", "$"), False
  1096. def set_fontproperties(self, fp):
  1097. """
  1098. Set the font properties that control the text.
  1099. Parameters
  1100. ----------
  1101. fp : `.font_manager.FontProperties` or `str` or `pathlib.Path`
  1102. If a `str`, it is interpreted as a fontconfig pattern parsed by
  1103. `.FontProperties`. If a `pathlib.Path`, it is interpreted as the
  1104. absolute path to a font file.
  1105. """
  1106. self._fontproperties = FontProperties._from_any(fp).copy()
  1107. self.stale = True
  1108. @_docstring.kwarg_doc("bool, default: :rc:`text.usetex`")
  1109. def set_usetex(self, usetex):
  1110. """
  1111. Parameters
  1112. ----------
  1113. usetex : bool or None
  1114. Whether to render using TeX, ``None`` means to use
  1115. :rc:`text.usetex`.
  1116. """
  1117. if usetex is None:
  1118. self._usetex = mpl.rcParams['text.usetex']
  1119. else:
  1120. self._usetex = bool(usetex)
  1121. self.stale = True
  1122. def get_usetex(self):
  1123. """Return whether this `Text` object uses TeX for rendering."""
  1124. return self._usetex
  1125. def set_parse_math(self, parse_math):
  1126. """
  1127. Override switch to disable any mathtext parsing for this `Text`.
  1128. Parameters
  1129. ----------
  1130. parse_math : bool
  1131. If False, this `Text` will never use mathtext. If True, mathtext
  1132. will be used if there is an even number of unescaped dollar signs.
  1133. """
  1134. self._parse_math = bool(parse_math)
  1135. def get_parse_math(self):
  1136. """Return whether mathtext parsing is considered for this `Text`."""
  1137. return self._parse_math
  1138. def set_fontname(self, fontname):
  1139. """
  1140. Alias for `set_fontfamily`.
  1141. One-way alias only: the getter differs.
  1142. Parameters
  1143. ----------
  1144. fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
  1145. 'monospace'}
  1146. See Also
  1147. --------
  1148. .font_manager.FontProperties.set_family
  1149. """
  1150. self.set_fontfamily(fontname)
  1151. class OffsetFrom:
  1152. """Callable helper class for working with `Annotation`."""
  1153. def __init__(self, artist, ref_coord, unit="points"):
  1154. """
  1155. Parameters
  1156. ----------
  1157. artist : `~matplotlib.artist.Artist` or `.BboxBase` or `.Transform`
  1158. The object to compute the offset from.
  1159. ref_coord : (float, float)
  1160. If *artist* is an `.Artist` or `.BboxBase`, this values is
  1161. the location to of the offset origin in fractions of the
  1162. *artist* bounding box.
  1163. If *artist* is a transform, the offset origin is the
  1164. transform applied to this value.
  1165. unit : {'points, 'pixels'}, default: 'points'
  1166. The screen units to use (pixels or points) for the offset input.
  1167. """
  1168. self._artist = artist
  1169. x, y = ref_coord # Make copy when ref_coord is an array (and check the shape).
  1170. self._ref_coord = x, y
  1171. self.set_unit(unit)
  1172. def set_unit(self, unit):
  1173. """
  1174. Set the unit for input to the transform used by ``__call__``.
  1175. Parameters
  1176. ----------
  1177. unit : {'points', 'pixels'}
  1178. """
  1179. _api.check_in_list(["points", "pixels"], unit=unit)
  1180. self._unit = unit
  1181. def get_unit(self):
  1182. """Return the unit for input to the transform used by ``__call__``."""
  1183. return self._unit
  1184. def __call__(self, renderer):
  1185. """
  1186. Return the offset transform.
  1187. Parameters
  1188. ----------
  1189. renderer : `RendererBase`
  1190. The renderer to use to compute the offset
  1191. Returns
  1192. -------
  1193. `Transform`
  1194. Maps (x, y) in pixel or point units to screen units
  1195. relative to the given artist.
  1196. """
  1197. if isinstance(self._artist, Artist):
  1198. bbox = self._artist.get_window_extent(renderer)
  1199. xf, yf = self._ref_coord
  1200. x = bbox.x0 + bbox.width * xf
  1201. y = bbox.y0 + bbox.height * yf
  1202. elif isinstance(self._artist, BboxBase):
  1203. bbox = self._artist
  1204. xf, yf = self._ref_coord
  1205. x = bbox.x0 + bbox.width * xf
  1206. y = bbox.y0 + bbox.height * yf
  1207. elif isinstance(self._artist, Transform):
  1208. x, y = self._artist.transform(self._ref_coord)
  1209. else:
  1210. _api.check_isinstance((Artist, BboxBase, Transform), artist=self._artist)
  1211. scale = 1 if self._unit == "pixels" else renderer.points_to_pixels(1)
  1212. return Affine2D().scale(scale).translate(x, y)
  1213. class _AnnotationBase:
  1214. def __init__(self,
  1215. xy,
  1216. xycoords='data',
  1217. annotation_clip=None):
  1218. x, y = xy # Make copy when xy is an array (and check the shape).
  1219. self.xy = x, y
  1220. self.xycoords = xycoords
  1221. self.set_annotation_clip(annotation_clip)
  1222. self._draggable = None
  1223. def _get_xy(self, renderer, xy, coords):
  1224. x, y = xy
  1225. xcoord, ycoord = coords if isinstance(coords, tuple) else (coords, coords)
  1226. if xcoord == 'data':
  1227. x = float(self.convert_xunits(x))
  1228. if ycoord == 'data':
  1229. y = float(self.convert_yunits(y))
  1230. return self._get_xy_transform(renderer, coords).transform((x, y))
  1231. def _get_xy_transform(self, renderer, coords):
  1232. if isinstance(coords, tuple):
  1233. xcoord, ycoord = coords
  1234. from matplotlib.transforms import blended_transform_factory
  1235. tr1 = self._get_xy_transform(renderer, xcoord)
  1236. tr2 = self._get_xy_transform(renderer, ycoord)
  1237. return blended_transform_factory(tr1, tr2)
  1238. elif callable(coords):
  1239. tr = coords(renderer)
  1240. if isinstance(tr, BboxBase):
  1241. return BboxTransformTo(tr)
  1242. elif isinstance(tr, Transform):
  1243. return tr
  1244. else:
  1245. raise TypeError(
  1246. f"xycoords callable must return a BboxBase or Transform, not a "
  1247. f"{type(tr).__name__}")
  1248. elif isinstance(coords, Artist):
  1249. bbox = coords.get_window_extent(renderer)
  1250. return BboxTransformTo(bbox)
  1251. elif isinstance(coords, BboxBase):
  1252. return BboxTransformTo(coords)
  1253. elif isinstance(coords, Transform):
  1254. return coords
  1255. elif not isinstance(coords, str):
  1256. raise TypeError(
  1257. f"'xycoords' must be an instance of str, tuple[str, str], Artist, "
  1258. f"Transform, or Callable, not a {type(coords).__name__}")
  1259. if coords == 'data':
  1260. return self.axes.transData
  1261. elif coords == 'polar':
  1262. from matplotlib.projections import PolarAxes
  1263. tr = PolarAxes.PolarTransform(apply_theta_transforms=False)
  1264. trans = tr + self.axes.transData
  1265. return trans
  1266. try:
  1267. bbox_name, unit = coords.split()
  1268. except ValueError: # i.e. len(coords.split()) != 2.
  1269. raise ValueError(f"{coords!r} is not a valid coordinate") from None
  1270. bbox0, xy0 = None, None
  1271. # if unit is offset-like
  1272. if bbox_name == "figure":
  1273. bbox0 = self.get_figure(root=False).figbbox
  1274. elif bbox_name == "subfigure":
  1275. bbox0 = self.get_figure(root=False).bbox
  1276. elif bbox_name == "axes":
  1277. bbox0 = self.axes.bbox
  1278. # reference x, y in display coordinate
  1279. if bbox0 is not None:
  1280. xy0 = bbox0.p0
  1281. elif bbox_name == "offset":
  1282. xy0 = self._get_position_xy(renderer)
  1283. else:
  1284. raise ValueError(f"{coords!r} is not a valid coordinate")
  1285. if unit == "points":
  1286. tr = Affine2D().scale(
  1287. self.get_figure(root=True).dpi / 72) # dpi/72 dots per point
  1288. elif unit == "pixels":
  1289. tr = Affine2D()
  1290. elif unit == "fontsize":
  1291. tr = Affine2D().scale(
  1292. self.get_size() * self.get_figure(root=True).dpi / 72)
  1293. elif unit == "fraction":
  1294. tr = Affine2D().scale(*bbox0.size)
  1295. else:
  1296. raise ValueError(f"{unit!r} is not a recognized unit")
  1297. return tr.translate(*xy0)
  1298. def set_annotation_clip(self, b):
  1299. """
  1300. Set the annotation's clipping behavior.
  1301. Parameters
  1302. ----------
  1303. b : bool or None
  1304. - True: The annotation will be clipped when ``self.xy`` is
  1305. outside the Axes.
  1306. - False: The annotation will always be drawn.
  1307. - None: The annotation will be clipped when ``self.xy`` is
  1308. outside the Axes and ``self.xycoords == "data"``.
  1309. """
  1310. self._annotation_clip = b
  1311. def get_annotation_clip(self):
  1312. """
  1313. Return the annotation's clipping behavior.
  1314. See `set_annotation_clip` for the meaning of return values.
  1315. """
  1316. return self._annotation_clip
  1317. def _get_position_xy(self, renderer):
  1318. """Return the pixel position of the annotated point."""
  1319. return self._get_xy(renderer, self.xy, self.xycoords)
  1320. def _check_xy(self, renderer=None):
  1321. """Check whether the annotation at *xy_pixel* should be drawn."""
  1322. if renderer is None:
  1323. renderer = self.get_figure(root=True)._get_renderer()
  1324. b = self.get_annotation_clip()
  1325. if b or (b is None and self.xycoords == "data"):
  1326. # check if self.xy is inside the Axes.
  1327. xy_pixel = self._get_position_xy(renderer)
  1328. return self.axes.contains_point(xy_pixel)
  1329. return True
  1330. def draggable(self, state=None, use_blit=False):
  1331. """
  1332. Set whether the annotation is draggable with the mouse.
  1333. Parameters
  1334. ----------
  1335. state : bool or None
  1336. - True or False: set the draggability.
  1337. - None: toggle the draggability.
  1338. use_blit : bool, default: False
  1339. Use blitting for faster image composition. For details see
  1340. :ref:`func-animation`.
  1341. Returns
  1342. -------
  1343. DraggableAnnotation or None
  1344. If the annotation is draggable, the corresponding
  1345. `.DraggableAnnotation` helper is returned.
  1346. """
  1347. from matplotlib.offsetbox import DraggableAnnotation
  1348. is_draggable = self._draggable is not None
  1349. # if state is None we'll toggle
  1350. if state is None:
  1351. state = not is_draggable
  1352. if state:
  1353. if self._draggable is None:
  1354. self._draggable = DraggableAnnotation(self, use_blit)
  1355. else:
  1356. if self._draggable is not None:
  1357. self._draggable.disconnect()
  1358. self._draggable = None
  1359. return self._draggable
  1360. class Annotation(Text, _AnnotationBase):
  1361. """
  1362. An `.Annotation` is a `.Text` that can refer to a specific position *xy*.
  1363. Optionally an arrow pointing from the text to *xy* can be drawn.
  1364. Attributes
  1365. ----------
  1366. xy
  1367. The annotated position.
  1368. xycoords
  1369. The coordinate system for *xy*.
  1370. arrow_patch
  1371. A `.FancyArrowPatch` to point from *xytext* to *xy*.
  1372. """
  1373. def __str__(self):
  1374. return f"Annotation({self.xy[0]:g}, {self.xy[1]:g}, {self._text!r})"
  1375. def __init__(self, text, xy,
  1376. xytext=None,
  1377. xycoords='data',
  1378. textcoords=None,
  1379. arrowprops=None,
  1380. annotation_clip=None,
  1381. **kwargs):
  1382. """
  1383. Annotate the point *xy* with text *text*.
  1384. In the simplest form, the text is placed at *xy*.
  1385. Optionally, the text can be displayed in another position *xytext*.
  1386. An arrow pointing from the text to the annotated point *xy* can then
  1387. be added by defining *arrowprops*.
  1388. Parameters
  1389. ----------
  1390. text : str
  1391. The text of the annotation.
  1392. xy : (float, float)
  1393. The point *(x, y)* to annotate. The coordinate system is determined
  1394. by *xycoords*.
  1395. xytext : (float, float), default: *xy*
  1396. The position *(x, y)* to place the text at. The coordinate system
  1397. is determined by *textcoords*.
  1398. xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \
  1399. callable, default: 'data'
  1400. The coordinate system that *xy* is given in. The following types
  1401. of values are supported:
  1402. - One of the following strings:
  1403. ==================== ============================================
  1404. Value Description
  1405. ==================== ============================================
  1406. 'figure points' Points from the lower left of the figure
  1407. 'figure pixels' Pixels from the lower left of the figure
  1408. 'figure fraction' Fraction of figure from lower left
  1409. 'subfigure points' Points from the lower left of the subfigure
  1410. 'subfigure pixels' Pixels from the lower left of the subfigure
  1411. 'subfigure fraction' Fraction of subfigure from lower left
  1412. 'axes points' Points from lower left corner of the Axes
  1413. 'axes pixels' Pixels from lower left corner of the Axes
  1414. 'axes fraction' Fraction of Axes from lower left
  1415. 'data' Use the coordinate system of the object
  1416. being annotated (default)
  1417. 'polar' *(theta, r)* if not native 'data'
  1418. coordinates
  1419. ==================== ============================================
  1420. Note that 'subfigure pixels' and 'figure pixels' are the same
  1421. for the parent figure, so users who want code that is usable in
  1422. a subfigure can use 'subfigure pixels'.
  1423. - An `.Artist`: *xy* is interpreted as a fraction of the artist's
  1424. `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower
  1425. left corner of the bounding box and *(0.5, 1)* would be the
  1426. center top of the bounding box.
  1427. - A `.Transform` to transform *xy* to screen coordinates.
  1428. - A function with one of the following signatures::
  1429. def transform(renderer) -> Bbox
  1430. def transform(renderer) -> Transform
  1431. where *renderer* is a `.RendererBase` subclass.
  1432. The result of the function is interpreted like the `.Artist` and
  1433. `.Transform` cases above.
  1434. - A tuple *(xcoords, ycoords)* specifying separate coordinate
  1435. systems for *x* and *y*. *xcoords* and *ycoords* must each be
  1436. of one of the above described types.
  1437. See :ref:`plotting-guide-annotation` for more details.
  1438. textcoords : single or two-tuple of str or `.Artist` or `.Transform` \
  1439. or callable, default: value of *xycoords*
  1440. The coordinate system that *xytext* is given in.
  1441. All *xycoords* values are valid as well as the following strings:
  1442. ================= =================================================
  1443. Value Description
  1444. ================= =================================================
  1445. 'offset points' Offset, in points, from the *xy* value
  1446. 'offset pixels' Offset, in pixels, from the *xy* value
  1447. 'offset fontsize' Offset, relative to fontsize, from the *xy* value
  1448. ================= =================================================
  1449. arrowprops : dict, optional
  1450. The properties used to draw a `.FancyArrowPatch` arrow between the
  1451. positions *xy* and *xytext*. Defaults to None, i.e. no arrow is
  1452. drawn.
  1453. For historical reasons there are two different ways to specify
  1454. arrows, "simple" and "fancy":
  1455. **Simple arrow:**
  1456. If *arrowprops* does not contain the key 'arrowstyle' the
  1457. allowed keys are:
  1458. ========== =================================================
  1459. Key Description
  1460. ========== =================================================
  1461. width The width of the arrow in points
  1462. headwidth The width of the base of the arrow head in points
  1463. headlength The length of the arrow head in points
  1464. shrink Fraction of total length to shrink from both ends
  1465. ? Any `.FancyArrowPatch` property
  1466. ========== =================================================
  1467. The arrow is attached to the edge of the text box, the exact
  1468. position (corners or centers) depending on where it's pointing to.
  1469. **Fancy arrow:**
  1470. This is used if 'arrowstyle' is provided in the *arrowprops*.
  1471. Valid keys are the following `.FancyArrowPatch` parameters:
  1472. =============== ===================================
  1473. Key Description
  1474. =============== ===================================
  1475. arrowstyle The arrow style
  1476. connectionstyle The connection style
  1477. relpos See below; default is (0.5, 0.5)
  1478. patchA Default is bounding box of the text
  1479. patchB Default is None
  1480. shrinkA In points. Default is 2 points
  1481. shrinkB In points. Default is 2 points
  1482. mutation_scale Default is text size (in points)
  1483. mutation_aspect Default is 1
  1484. ? Any `.FancyArrowPatch` property
  1485. =============== ===================================
  1486. The exact starting point position of the arrow is defined by
  1487. *relpos*. It's a tuple of relative coordinates of the text box,
  1488. where (0, 0) is the lower left corner and (1, 1) is the upper
  1489. right corner. Values <0 and >1 are supported and specify points
  1490. outside the text box. By default (0.5, 0.5), so the starting point
  1491. is centered in the text box.
  1492. annotation_clip : bool or None, default: None
  1493. Whether to clip (i.e. not draw) the annotation when the annotation
  1494. point *xy* is outside the Axes area.
  1495. - If *True*, the annotation will be clipped when *xy* is outside
  1496. the Axes.
  1497. - If *False*, the annotation will always be drawn.
  1498. - If *None*, the annotation will be clipped when *xy* is outside
  1499. the Axes and *xycoords* is 'data'.
  1500. **kwargs
  1501. Additional kwargs are passed to `.Text`.
  1502. Returns
  1503. -------
  1504. `.Annotation`
  1505. See Also
  1506. --------
  1507. :ref:`annotations`
  1508. """
  1509. _AnnotationBase.__init__(self,
  1510. xy,
  1511. xycoords=xycoords,
  1512. annotation_clip=annotation_clip)
  1513. # warn about wonky input data
  1514. if (xytext is None and
  1515. textcoords is not None and
  1516. textcoords != xycoords):
  1517. _api.warn_external("You have used the `textcoords` kwarg, but "
  1518. "not the `xytext` kwarg. This can lead to "
  1519. "surprising results.")
  1520. # clean up textcoords and assign default
  1521. if textcoords is None:
  1522. textcoords = self.xycoords
  1523. self._textcoords = textcoords
  1524. # cleanup xytext defaults
  1525. if xytext is None:
  1526. xytext = self.xy
  1527. x, y = xytext
  1528. self.arrowprops = arrowprops
  1529. if arrowprops is not None:
  1530. arrowprops = arrowprops.copy()
  1531. if "arrowstyle" in arrowprops:
  1532. self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
  1533. else:
  1534. # modified YAArrow API to be used with FancyArrowPatch
  1535. for key in ['width', 'headwidth', 'headlength', 'shrink']:
  1536. arrowprops.pop(key, None)
  1537. self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops)
  1538. else:
  1539. self.arrow_patch = None
  1540. # Must come last, as some kwargs may be propagated to arrow_patch.
  1541. Text.__init__(self, x, y, text, **kwargs)
  1542. def contains(self, mouseevent):
  1543. if self._different_canvas(mouseevent):
  1544. return False, {}
  1545. contains, tinfo = Text.contains(self, mouseevent)
  1546. if self.arrow_patch is not None:
  1547. in_patch, _ = self.arrow_patch.contains(mouseevent)
  1548. contains = contains or in_patch
  1549. return contains, tinfo
  1550. @property
  1551. def xycoords(self):
  1552. return self._xycoords
  1553. @xycoords.setter
  1554. def xycoords(self, xycoords):
  1555. def is_offset(s):
  1556. return isinstance(s, str) and s.startswith("offset")
  1557. if (isinstance(xycoords, tuple) and any(map(is_offset, xycoords))
  1558. or is_offset(xycoords)):
  1559. raise ValueError("xycoords cannot be an offset coordinate")
  1560. self._xycoords = xycoords
  1561. @property
  1562. def xyann(self):
  1563. """
  1564. The text position.
  1565. See also *xytext* in `.Annotation`.
  1566. """
  1567. return self.get_position()
  1568. @xyann.setter
  1569. def xyann(self, xytext):
  1570. self.set_position(xytext)
  1571. def get_anncoords(self):
  1572. """
  1573. Return the coordinate system to use for `.Annotation.xyann`.
  1574. See also *xycoords* in `.Annotation`.
  1575. """
  1576. return self._textcoords
  1577. def set_anncoords(self, coords):
  1578. """
  1579. Set the coordinate system to use for `.Annotation.xyann`.
  1580. See also *xycoords* in `.Annotation`.
  1581. """
  1582. self._textcoords = coords
  1583. anncoords = property(get_anncoords, set_anncoords, doc="""
  1584. The coordinate system to use for `.Annotation.xyann`.""")
  1585. def set_figure(self, fig):
  1586. # docstring inherited
  1587. if self.arrow_patch is not None:
  1588. self.arrow_patch.set_figure(fig)
  1589. Artist.set_figure(self, fig)
  1590. def update_positions(self, renderer):
  1591. """
  1592. Update the pixel positions of the annotation text and the arrow patch.
  1593. """
  1594. # generate transformation
  1595. self.set_transform(self._get_xy_transform(renderer, self.anncoords))
  1596. arrowprops = self.arrowprops
  1597. if arrowprops is None:
  1598. return
  1599. bbox = Text.get_window_extent(self, renderer)
  1600. arrow_end = x1, y1 = self._get_position_xy(renderer) # Annotated pos.
  1601. ms = arrowprops.get("mutation_scale", self.get_size())
  1602. self.arrow_patch.set_mutation_scale(ms)
  1603. if "arrowstyle" not in arrowprops:
  1604. # Approximately simulate the YAArrow.
  1605. shrink = arrowprops.get('shrink', 0.0)
  1606. width = arrowprops.get('width', 4)
  1607. headwidth = arrowprops.get('headwidth', 12)
  1608. headlength = arrowprops.get('headlength', 12)
  1609. # NB: ms is in pts
  1610. stylekw = dict(head_length=headlength / ms,
  1611. head_width=headwidth / ms,
  1612. tail_width=width / ms)
  1613. self.arrow_patch.set_arrowstyle('simple', **stylekw)
  1614. # using YAArrow style:
  1615. # pick the corner of the text bbox closest to annotated point.
  1616. xpos = [(bbox.x0, 0), ((bbox.x0 + bbox.x1) / 2, 0.5), (bbox.x1, 1)]
  1617. ypos = [(bbox.y0, 0), ((bbox.y0 + bbox.y1) / 2, 0.5), (bbox.y1, 1)]
  1618. x, relposx = min(xpos, key=lambda v: abs(v[0] - x1))
  1619. y, relposy = min(ypos, key=lambda v: abs(v[0] - y1))
  1620. self._arrow_relpos = (relposx, relposy)
  1621. r = np.hypot(y - y1, x - x1)
  1622. shrink_pts = shrink * r / renderer.points_to_pixels(1)
  1623. self.arrow_patch.shrinkA = self.arrow_patch.shrinkB = shrink_pts
  1624. # adjust the starting point of the arrow relative to the textbox.
  1625. # TODO : Rotation needs to be accounted.
  1626. arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos
  1627. # The arrow is drawn from arrow_begin to arrow_end. It will be first
  1628. # clipped by patchA and patchB. Then it will be shrunk by shrinkA and
  1629. # shrinkB (in points). If patchA is not set, self.bbox_patch is used.
  1630. self.arrow_patch.set_positions(arrow_begin, arrow_end)
  1631. if "patchA" in arrowprops:
  1632. patchA = arrowprops["patchA"]
  1633. elif self._bbox_patch:
  1634. patchA = self._bbox_patch
  1635. elif self.get_text() == "":
  1636. patchA = None
  1637. else:
  1638. pad = renderer.points_to_pixels(4)
  1639. patchA = Rectangle(
  1640. xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2),
  1641. width=bbox.width + pad, height=bbox.height + pad,
  1642. transform=IdentityTransform(), clip_on=False)
  1643. self.arrow_patch.set_patchA(patchA)
  1644. @artist.allow_rasterization
  1645. def draw(self, renderer):
  1646. # docstring inherited
  1647. if renderer is not None:
  1648. self._renderer = renderer
  1649. if not self.get_visible() or not self._check_xy(renderer):
  1650. return
  1651. # Update text positions before `Text.draw` would, so that the
  1652. # FancyArrowPatch is correctly positioned.
  1653. self.update_positions(renderer)
  1654. self.update_bbox_position_size(renderer)
  1655. if self.arrow_patch is not None: # FancyArrowPatch
  1656. if (self.arrow_patch.get_figure(root=False) is None and
  1657. (fig := self.get_figure(root=False)) is not None):
  1658. self.arrow_patch.set_figure(fig)
  1659. self.arrow_patch.draw(renderer)
  1660. # Draw text, including FancyBboxPatch, after FancyArrowPatch.
  1661. # Otherwise, a wedge arrowstyle can land partly on top of the Bbox.
  1662. Text.draw(self, renderer)
  1663. def get_window_extent(self, renderer=None):
  1664. # docstring inherited
  1665. # This block is the same as in Text.get_window_extent, but we need to
  1666. # set the renderer before calling update_positions().
  1667. if not self.get_visible() or not self._check_xy(renderer):
  1668. return Bbox.unit()
  1669. if renderer is not None:
  1670. self._renderer = renderer
  1671. if self._renderer is None:
  1672. self._renderer = self.get_figure(root=True)._get_renderer()
  1673. if self._renderer is None:
  1674. raise RuntimeError('Cannot get window extent without renderer')
  1675. self.update_positions(self._renderer)
  1676. text_bbox = Text.get_window_extent(self)
  1677. bboxes = [text_bbox]
  1678. if self.arrow_patch is not None:
  1679. bboxes.append(self.arrow_patch.get_window_extent())
  1680. return Bbox.union(bboxes)
  1681. def get_tightbbox(self, renderer=None):
  1682. # docstring inherited
  1683. if not self._check_xy(renderer):
  1684. return Bbox.null()
  1685. return super().get_tightbbox(renderer)
  1686. _docstring.interpd.register(Annotation=Annotation.__init__.__doc__)