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