1
0

text.py 79 KB

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