Coverage for /usr/lib/python3/dist-packages/matplotlib/text.py: 17%
800 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
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 # noqa # Unused, but imported by others.
19from .transforms import (
20 Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform)
23_log = logging.getLogger(__name__)
26@_api.deprecated("3.6")
27def get_rotation(rotation):
28 """
29 Return *rotation* normalized to an angle between 0 and 360 degrees.
31 Parameters
32 ----------
33 rotation : float or {None, 'horizontal', 'vertical'}
34 Rotation angle in degrees. *None* and 'horizontal' equal 0,
35 'vertical' equals 90.
37 Returns
38 -------
39 float
40 """
41 try:
42 return float(rotation) % 360
43 except (ValueError, TypeError) as err:
44 if cbook._str_equal(rotation, 'horizontal') or rotation is None:
45 return 0.
46 elif cbook._str_equal(rotation, 'vertical'):
47 return 90.
48 else:
49 raise ValueError("rotation is {!r}; expected either 'horizontal', "
50 "'vertical', numeric value, or None"
51 .format(rotation)) from err
54def _get_textbox(text, renderer):
55 """
56 Calculate the bounding box of the text.
58 The bbox position takes text rotation into account, but the width and
59 height are those of the unrotated box (unlike `.Text.get_window_extent`).
60 """
61 # TODO : This function may move into the Text class as a method. As a
62 # matter of fact, the information from the _get_textbox function
63 # should be available during the Text._get_layout() call, which is
64 # called within the _get_textbox. So, it would better to move this
65 # function as a method with some refactoring of _get_layout method.
67 projected_xs = []
68 projected_ys = []
70 theta = np.deg2rad(text.get_rotation())
71 tr = Affine2D().rotate(-theta)
73 _, parts, d = text._get_layout(renderer)
75 for t, wh, x, y in parts:
76 w, h = wh
78 xt1, yt1 = tr.transform((x, y))
79 yt1 -= d
80 xt2, yt2 = xt1 + w, yt1 + h
82 projected_xs.extend([xt1, xt2])
83 projected_ys.extend([yt1, yt2])
85 xt_box, yt_box = min(projected_xs), min(projected_ys)
86 w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
88 x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box))
90 return x_box, y_box, w_box, h_box
93def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi):
94 """Call ``renderer.get_text_width_height_descent``, caching the results."""
95 # Cached based on a copy of fontprop so that later in-place mutations of
96 # the passed-in argument do not mess up the cache.
97 return _get_text_metrics_with_cache_impl(
98 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
101@functools.lru_cache(4096)
102def _get_text_metrics_with_cache_impl(
103 renderer_ref, text, fontprop, ismath, dpi):
104 # dpi is unused, but participates in cache invalidation (via the renderer).
105 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
108@_docstring.interpd
109@_api.define_aliases({
110 "color": ["c"],
111 "fontfamily": ["family"],
112 "fontproperties": ["font", "font_properties"],
113 "horizontalalignment": ["ha"],
114 "multialignment": ["ma"],
115 "fontname": ["name"],
116 "fontsize": ["size"],
117 "fontstretch": ["stretch"],
118 "fontstyle": ["style"],
119 "fontvariant": ["variant"],
120 "verticalalignment": ["va"],
121 "fontweight": ["weight"],
122})
123class Text(Artist):
124 """Handle storing and drawing of text in window or data coordinates."""
126 zorder = 3
128 def __repr__(self):
129 return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text))
131 @_api.make_keyword_only("3.6", name="color")
132 def __init__(self,
133 x=0, y=0, text='',
134 color=None, # defaults to rc params
135 verticalalignment='baseline',
136 horizontalalignment='left',
137 multialignment=None,
138 fontproperties=None, # defaults to FontProperties()
139 rotation=None,
140 linespacing=None,
141 rotation_mode=None,
142 usetex=None, # defaults to rcParams['text.usetex']
143 wrap=False,
144 transform_rotates_text=False,
145 *,
146 parse_math=None, # defaults to rcParams['text.parse_math']
147 **kwargs
148 ):
149 """
150 Create a `.Text` instance at *x*, *y* with string *text*.
152 The text is aligned relative to the anchor point (*x*, *y*) according
153 to ``horizontalalignment`` (default: 'left') and ``verticalalignment``
154 (default: 'bottom'). See also
155 :doc:`/gallery/text_labels_and_annotations/text_alignment`.
157 While Text accepts the 'label' keyword argument, by default it is not
158 added to the handles of a legend.
160 Valid keyword arguments are:
162 %(Text:kwdoc)s
163 """
164 super().__init__()
165 self._x, self._y = x, y
166 self._text = ''
167 self.set_text(text)
168 self.set_color(
169 color if color is not None else mpl.rcParams["text.color"])
170 self.set_fontproperties(fontproperties)
171 self.set_usetex(usetex)
172 self.set_parse_math(parse_math if parse_math is not None else
173 mpl.rcParams['text.parse_math'])
174 self.set_wrap(wrap)
175 self.set_verticalalignment(verticalalignment)
176 self.set_horizontalalignment(horizontalalignment)
177 self._multialignment = multialignment
178 self.set_rotation(rotation)
179 self._transform_rotates_text = transform_rotates_text
180 self._bbox_patch = None # a FancyBboxPatch instance
181 self._renderer = None
182 if linespacing is None:
183 linespacing = 1.2 # Maybe use rcParam later.
184 self.set_linespacing(linespacing)
185 self.set_rotation_mode(rotation_mode)
186 self.update(kwargs)
188 def update(self, kwargs):
189 # docstring inherited
190 kwargs = cbook.normalize_kwargs(kwargs, Text)
191 sentinel = object() # bbox can be None, so use another sentinel.
192 # Update fontproperties first, as it has lowest priority.
193 fontproperties = kwargs.pop("fontproperties", sentinel)
194 if fontproperties is not sentinel:
195 self.set_fontproperties(fontproperties)
196 # Update bbox last, as it depends on font properties.
197 bbox = kwargs.pop("bbox", sentinel)
198 super().update(kwargs)
199 if bbox is not sentinel:
200 self.set_bbox(bbox)
202 def __getstate__(self):
203 d = super().__getstate__()
204 # remove the cached _renderer (if it exists)
205 d['_renderer'] = None
206 return d
208 def contains(self, mouseevent):
209 """
210 Return whether the mouse event occurred inside the axis-aligned
211 bounding-box of the text.
212 """
213 inside, info = self._default_contains(mouseevent)
214 if inside is not None:
215 return inside, info
217 if not self.get_visible() or self._renderer is None:
218 return False, {}
220 # Explicitly use Text.get_window_extent(self) and not
221 # self.get_window_extent() so that Annotation.contains does not
222 # accidentally cover the entire annotation bounding box.
223 bbox = Text.get_window_extent(self)
224 inside = (bbox.x0 <= mouseevent.x <= bbox.x1
225 and bbox.y0 <= mouseevent.y <= bbox.y1)
227 cattr = {}
228 # if the text has a surrounding patch, also check containment for it,
229 # and merge the results with the results for the text.
230 if self._bbox_patch:
231 patch_inside, patch_cattr = self._bbox_patch.contains(mouseevent)
232 inside = inside or patch_inside
233 cattr["bbox_patch"] = patch_cattr
235 return inside, cattr
237 def _get_xy_display(self):
238 """
239 Get the (possibly unit converted) transformed x, y in display coords.
240 """
241 x, y = self.get_unitless_position()
242 return self.get_transform().transform((x, y))
244 def _get_multialignment(self):
245 if self._multialignment is not None:
246 return self._multialignment
247 else:
248 return self._horizontalalignment
250 def get_rotation(self):
251 """Return the text angle in degrees between 0 and 360."""
252 if self.get_transform_rotates_text():
253 return self.get_transform().transform_angles(
254 [self._rotation], [self.get_unitless_position()]).item(0)
255 else:
256 return self._rotation
258 def get_transform_rotates_text(self):
259 """
260 Return whether rotations of the transform affect the text direction.
261 """
262 return self._transform_rotates_text
264 def set_rotation_mode(self, m):
265 """
266 Set text rotation mode.
268 Parameters
269 ----------
270 m : {None, 'default', 'anchor'}
271 If ``None`` or ``"default"``, the text will be first rotated, then
272 aligned according to their horizontal and vertical alignments. If
273 ``"anchor"``, then alignment occurs before rotation.
274 """
275 _api.check_in_list(["anchor", "default", None], rotation_mode=m)
276 self._rotation_mode = m
277 self.stale = True
279 def get_rotation_mode(self):
280 """Return the text rotation mode."""
281 return self._rotation_mode
283 def update_from(self, other):
284 # docstring inherited
285 super().update_from(other)
286 self._color = other._color
287 self._multialignment = other._multialignment
288 self._verticalalignment = other._verticalalignment
289 self._horizontalalignment = other._horizontalalignment
290 self._fontproperties = other._fontproperties.copy()
291 self._usetex = other._usetex
292 self._rotation = other._rotation
293 self._transform_rotates_text = other._transform_rotates_text
294 self._picker = other._picker
295 self._linespacing = other._linespacing
296 self.stale = True
298 def _get_layout(self, renderer):
299 """
300 Return the extent (bbox) of the text together with
301 multiple-alignment information. Note that it returns an extent
302 of a rotated text when necessary.
303 """
304 thisx, thisy = 0.0, 0.0
305 lines = self.get_text().split("\n") # Ensures lines is not empty.
307 ws = []
308 hs = []
309 xs = []
310 ys = []
312 # Full vertical extent of font, including ascenders and descenders:
313 _, lp_h, lp_d = _get_text_metrics_with_cache(
314 renderer, "lp", self._fontproperties,
315 ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi)
316 min_dy = (lp_h - lp_d) * self._linespacing
318 for i, line in enumerate(lines):
319 clean_line, ismath = self._preprocess_math(line)
320 if clean_line:
321 w, h, d = _get_text_metrics_with_cache(
322 renderer, clean_line, self._fontproperties,
323 ismath=ismath, dpi=self.figure.dpi)
324 else:
325 w = h = d = 0
327 # For multiline text, increase the line spacing when the text
328 # net-height (excluding baseline) is larger than that of a "l"
329 # (e.g., use of superscripts), which seems what TeX does.
330 h = max(h, lp_h)
331 d = max(d, lp_d)
333 ws.append(w)
334 hs.append(h)
336 # Metrics of the last line that are needed later:
337 baseline = (h - d) - thisy
339 if i == 0:
340 # position at baseline
341 thisy = -(h - d)
342 else:
343 # put baseline a good distance from bottom of previous line
344 thisy -= max(min_dy, (h - d) * self._linespacing)
346 xs.append(thisx) # == 0.
347 ys.append(thisy)
349 thisy -= d
351 # Metrics of the last line that are needed later:
352 descent = d
354 # Bounding box definition:
355 width = max(ws)
356 xmin = 0
357 xmax = width
358 ymax = 0
359 ymin = ys[-1] - descent # baseline of last line minus its descent
361 # get the rotation matrix
362 M = Affine2D().rotate_deg(self.get_rotation())
364 # now offset the individual text lines within the box
365 malign = self._get_multialignment()
366 if malign == 'left':
367 offset_layout = [(x, y) for x, y in zip(xs, ys)]
368 elif malign == 'center':
369 offset_layout = [(x + width / 2 - w / 2, y)
370 for x, y, w in zip(xs, ys, ws)]
371 elif malign == 'right':
372 offset_layout = [(x + width - w, y)
373 for x, y, w in zip(xs, ys, ws)]
375 # the corners of the unrotated bounding box
376 corners_horiz = np.array(
377 [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
379 # now rotate the bbox
380 corners_rotated = M.transform(corners_horiz)
381 # compute the bounds of the rotated box
382 xmin = corners_rotated[:, 0].min()
383 xmax = corners_rotated[:, 0].max()
384 ymin = corners_rotated[:, 1].min()
385 ymax = corners_rotated[:, 1].max()
386 width = xmax - xmin
387 height = ymax - ymin
389 # Now move the box to the target position offset the display
390 # bbox by alignment
391 halign = self._horizontalalignment
392 valign = self._verticalalignment
394 rotation_mode = self.get_rotation_mode()
395 if rotation_mode != "anchor":
396 # compute the text location in display coords and the offsets
397 # necessary to align the bbox with that location
398 if halign == 'center':
399 offsetx = (xmin + xmax) / 2
400 elif halign == 'right':
401 offsetx = xmax
402 else:
403 offsetx = xmin
405 if valign == 'center':
406 offsety = (ymin + ymax) / 2
407 elif valign == 'top':
408 offsety = ymax
409 elif valign == 'baseline':
410 offsety = ymin + descent
411 elif valign == 'center_baseline':
412 offsety = ymin + height - baseline / 2.0
413 else:
414 offsety = ymin
415 else:
416 xmin1, ymin1 = corners_horiz[0]
417 xmax1, ymax1 = corners_horiz[2]
419 if halign == 'center':
420 offsetx = (xmin1 + xmax1) / 2.0
421 elif halign == 'right':
422 offsetx = xmax1
423 else:
424 offsetx = xmin1
426 if valign == 'center':
427 offsety = (ymin1 + ymax1) / 2.0
428 elif valign == 'top':
429 offsety = ymax1
430 elif valign == 'baseline':
431 offsety = ymax1 - baseline
432 elif valign == 'center_baseline':
433 offsety = ymax1 - baseline / 2.0
434 else:
435 offsety = ymin1
437 offsetx, offsety = M.transform((offsetx, offsety))
439 xmin -= offsetx
440 ymin -= offsety
442 bbox = Bbox.from_bounds(xmin, ymin, width, height)
444 # now rotate the positions around the first (x, y) position
445 xys = M.transform(offset_layout) - (offsetx, offsety)
447 return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
449 def set_bbox(self, rectprops):
450 """
451 Draw a bounding box around self.
453 Parameters
454 ----------
455 rectprops : dict with properties for `.patches.FancyBboxPatch`
456 The default boxstyle is 'square'. The mutation
457 scale of the `.patches.FancyBboxPatch` is set to the fontsize.
459 Examples
460 --------
461 ::
463 t.set_bbox(dict(facecolor='red', alpha=0.5))
464 """
466 if rectprops is not None:
467 props = rectprops.copy()
468 boxstyle = props.pop("boxstyle", None)
469 pad = props.pop("pad", None)
470 if boxstyle is None:
471 boxstyle = "square"
472 if pad is None:
473 pad = 4 # points
474 pad /= self.get_size() # to fraction of font size
475 else:
476 if pad is None:
477 pad = 0.3
478 # boxstyle could be a callable or a string
479 if isinstance(boxstyle, str) and "pad" not in boxstyle:
480 boxstyle += ",pad=%0.2f" % pad
481 self._bbox_patch = FancyBboxPatch(
482 (0, 0), 1, 1,
483 boxstyle=boxstyle, transform=IdentityTransform(), **props)
484 else:
485 self._bbox_patch = None
487 self._update_clip_properties()
489 def get_bbox_patch(self):
490 """
491 Return the bbox Patch, or None if the `.patches.FancyBboxPatch`
492 is not made.
493 """
494 return self._bbox_patch
496 def update_bbox_position_size(self, renderer):
497 """
498 Update the location and the size of the bbox.
500 This method should be used when the position and size of the bbox needs
501 to be updated before actually drawing the bbox.
502 """
503 if self._bbox_patch:
504 # don't use self.get_unitless_position here, which refers to text
505 # position in Text:
506 posx = float(self.convert_xunits(self._x))
507 posy = float(self.convert_yunits(self._y))
508 posx, posy = self.get_transform().transform((posx, posy))
510 x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
511 self._bbox_patch.set_bounds(0., 0., w_box, h_box)
512 self._bbox_patch.set_transform(
513 Affine2D()
514 .rotate_deg(self.get_rotation())
515 .translate(posx + x_box, posy + y_box))
516 fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
517 self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
519 def _update_clip_properties(self):
520 clipprops = dict(clip_box=self.clipbox,
521 clip_path=self._clippath,
522 clip_on=self._clipon)
523 if self._bbox_patch:
524 self._bbox_patch.update(clipprops)
526 def set_clip_box(self, clipbox):
527 # docstring inherited.
528 super().set_clip_box(clipbox)
529 self._update_clip_properties()
531 def set_clip_path(self, path, transform=None):
532 # docstring inherited.
533 super().set_clip_path(path, transform)
534 self._update_clip_properties()
536 def set_clip_on(self, b):
537 # docstring inherited.
538 super().set_clip_on(b)
539 self._update_clip_properties()
541 def get_wrap(self):
542 """Return whether the text can be wrapped."""
543 return self._wrap
545 def set_wrap(self, wrap):
546 """
547 Set whether the text can be wrapped.
549 Parameters
550 ----------
551 wrap : bool
553 Notes
554 -----
555 Wrapping does not work together with
556 ``savefig(..., bbox_inches='tight')`` (which is also used internally
557 by ``%matplotlib inline`` in IPython/Jupyter). The 'tight' setting
558 rescales the canvas to accommodate all content and happens before
559 wrapping.
560 """
561 self._wrap = wrap
563 def _get_wrap_line_width(self):
564 """
565 Return the maximum line width for wrapping text based on the current
566 orientation.
567 """
568 x0, y0 = self.get_transform().transform(self.get_position())
569 figure_box = self.get_figure().get_window_extent()
571 # Calculate available width based on text alignment
572 alignment = self.get_horizontalalignment()
573 self.set_rotation_mode('anchor')
574 rotation = self.get_rotation()
576 left = self._get_dist_to_box(rotation, x0, y0, figure_box)
577 right = self._get_dist_to_box(
578 (180 + rotation) % 360, x0, y0, figure_box)
580 if alignment == 'left':
581 line_width = left
582 elif alignment == 'right':
583 line_width = right
584 else:
585 line_width = 2 * min(left, right)
587 return line_width
589 def _get_dist_to_box(self, rotation, x0, y0, figure_box):
590 """
591 Return the distance from the given points to the boundaries of a
592 rotated box, in pixels.
593 """
594 if rotation > 270:
595 quad = rotation - 270
596 h1 = y0 / math.cos(math.radians(quad))
597 h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad))
598 elif rotation > 180:
599 quad = rotation - 180
600 h1 = x0 / math.cos(math.radians(quad))
601 h2 = y0 / math.cos(math.radians(90 - quad))
602 elif rotation > 90:
603 quad = rotation - 90
604 h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad))
605 h2 = x0 / math.cos(math.radians(90 - quad))
606 else:
607 h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation))
608 h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation))
610 return min(h1, h2)
612 def _get_rendered_text_width(self, text):
613 """
614 Return the width of a given text string, in pixels.
615 """
616 w, h, d = self._renderer.get_text_width_height_descent(
617 text,
618 self.get_fontproperties(),
619 False)
620 return math.ceil(w)
622 def _get_wrapped_text(self):
623 """
624 Return a copy of the text string with new lines added so that the text
625 is wrapped relative to the parent figure (if `get_wrap` is True).
626 """
627 if not self.get_wrap():
628 return self.get_text()
630 # Not fit to handle breaking up latex syntax correctly, so
631 # ignore latex for now.
632 if self.get_usetex():
633 return self.get_text()
635 # Build the line incrementally, for a more accurate measure of length
636 line_width = self._get_wrap_line_width()
637 wrapped_lines = []
639 # New lines in the user's text force a split
640 unwrapped_lines = self.get_text().split('\n')
642 # Now wrap each individual unwrapped line
643 for unwrapped_line in unwrapped_lines:
645 sub_words = unwrapped_line.split(' ')
646 # Remove items from sub_words as we go, so stop when empty
647 while len(sub_words) > 0:
648 if len(sub_words) == 1:
649 # Only one word, so just add it to the end
650 wrapped_lines.append(sub_words.pop(0))
651 continue
653 for i in range(2, len(sub_words) + 1):
654 # Get width of all words up to and including here
655 line = ' '.join(sub_words[:i])
656 current_width = self._get_rendered_text_width(line)
658 # If all these words are too wide, append all not including
659 # last word
660 if current_width > line_width:
661 wrapped_lines.append(' '.join(sub_words[:i - 1]))
662 sub_words = sub_words[i - 1:]
663 break
665 # Otherwise if all words fit in the width, append them all
666 elif i == len(sub_words):
667 wrapped_lines.append(' '.join(sub_words[:i]))
668 sub_words = []
669 break
671 return '\n'.join(wrapped_lines)
673 @artist.allow_rasterization
674 def draw(self, renderer):
675 # docstring inherited
677 if renderer is not None:
678 self._renderer = renderer
679 if not self.get_visible():
680 return
681 if self.get_text() == '':
682 return
684 renderer.open_group('text', self.get_gid())
686 with self._cm_set(text=self._get_wrapped_text()):
687 bbox, info, descent = self._get_layout(renderer)
688 trans = self.get_transform()
690 # don't use self.get_position here, which refers to text
691 # position in Text:
692 posx = float(self.convert_xunits(self._x))
693 posy = float(self.convert_yunits(self._y))
694 posx, posy = trans.transform((posx, posy))
695 if not np.isfinite(posx) or not np.isfinite(posy):
696 _log.warning("posx and posy should be finite values")
697 return
698 canvasw, canvash = renderer.get_canvas_width_height()
700 # Update the location and size of the bbox
701 # (`.patches.FancyBboxPatch`), and draw it.
702 if self._bbox_patch:
703 self.update_bbox_position_size(renderer)
704 self._bbox_patch.draw(renderer)
706 gc = renderer.new_gc()
707 gc.set_foreground(self.get_color())
708 gc.set_alpha(self.get_alpha())
709 gc.set_url(self._url)
710 self._set_gc_clip(gc)
712 angle = self.get_rotation()
714 for line, wh, x, y in info:
716 mtext = self if len(info) == 1 else None
717 x = x + posx
718 y = y + posy
719 if renderer.flipy():
720 y = canvash - y
721 clean_line, ismath = self._preprocess_math(line)
723 if self.get_path_effects():
724 from matplotlib.patheffects import PathEffectRenderer
725 textrenderer = PathEffectRenderer(
726 self.get_path_effects(), renderer)
727 else:
728 textrenderer = renderer
730 if self.get_usetex():
731 textrenderer.draw_tex(gc, x, y, clean_line,
732 self._fontproperties, angle,
733 mtext=mtext)
734 else:
735 textrenderer.draw_text(gc, x, y, clean_line,
736 self._fontproperties, angle,
737 ismath=ismath, mtext=mtext)
739 gc.restore()
740 renderer.close_group('text')
741 self.stale = False
743 def get_color(self):
744 """Return the color of the text."""
745 return self._color
747 def get_fontproperties(self):
748 """Return the `.font_manager.FontProperties`."""
749 return self._fontproperties
751 def get_fontfamily(self):
752 """
753 Return the list of font families used for font lookup.
755 See Also
756 --------
757 .font_manager.FontProperties.get_family
758 """
759 return self._fontproperties.get_family()
761 def get_fontname(self):
762 """
763 Return the font name as a string.
765 See Also
766 --------
767 .font_manager.FontProperties.get_name
768 """
769 return self._fontproperties.get_name()
771 def get_fontstyle(self):
772 """
773 Return the font style as a string.
775 See Also
776 --------
777 .font_manager.FontProperties.get_style
778 """
779 return self._fontproperties.get_style()
781 def get_fontsize(self):
782 """
783 Return the font size as an integer.
785 See Also
786 --------
787 .font_manager.FontProperties.get_size_in_points
788 """
789 return self._fontproperties.get_size_in_points()
791 def get_fontvariant(self):
792 """
793 Return the font variant as a string.
795 See Also
796 --------
797 .font_manager.FontProperties.get_variant
798 """
799 return self._fontproperties.get_variant()
801 def get_fontweight(self):
802 """
803 Return the font weight as a string or a number.
805 See Also
806 --------
807 .font_manager.FontProperties.get_weight
808 """
809 return self._fontproperties.get_weight()
811 def get_stretch(self):
812 """
813 Return the font stretch as a string or a number.
815 See Also
816 --------
817 .font_manager.FontProperties.get_stretch
818 """
819 return self._fontproperties.get_stretch()
821 def get_horizontalalignment(self):
822 """
823 Return the horizontal alignment as a string. Will be one of
824 'left', 'center' or 'right'.
825 """
826 return self._horizontalalignment
828 def get_unitless_position(self):
829 """Return the (x, y) unitless position of the text."""
830 # This will get the position with all unit information stripped away.
831 # This is here for convenience since it is done in several locations.
832 x = float(self.convert_xunits(self._x))
833 y = float(self.convert_yunits(self._y))
834 return x, y
836 def get_position(self):
837 """Return the (x, y) position of the text."""
838 # This should return the same data (possible unitized) as was
839 # specified with 'set_x' and 'set_y'.
840 return self._x, self._y
842 # When removing, also remove the hash(color) check in set_color()
843 @_api.deprecated("3.5")
844 def get_prop_tup(self, renderer=None):
845 """
846 Return a hashable tuple of properties.
848 Not intended to be human readable, but useful for backends who
849 want to cache derived information about text (e.g., layouts) and
850 need to know if the text has changed.
851 """
852 x, y = self.get_unitless_position()
853 renderer = renderer or self._renderer
854 return (x, y, self.get_text(), self._color,
855 self._verticalalignment, self._horizontalalignment,
856 hash(self._fontproperties),
857 self._rotation, self._rotation_mode,
858 self._transform_rotates_text,
859 self.figure.dpi, weakref.ref(renderer),
860 self._linespacing
861 )
863 def get_text(self):
864 """Return the text string."""
865 return self._text
867 def get_verticalalignment(self):
868 """
869 Return the vertical alignment as a string. Will be one of
870 'top', 'center', 'bottom', 'baseline' or 'center_baseline'.
871 """
872 return self._verticalalignment
874 def get_window_extent(self, renderer=None, dpi=None):
875 """
876 Return the `.Bbox` bounding the text, in display units.
878 In addition to being used internally, this is useful for specifying
879 clickable regions in a png file on a web page.
881 Parameters
882 ----------
883 renderer : Renderer, optional
884 A renderer is needed to compute the bounding box. If the artist
885 has already been drawn, the renderer is cached; thus, it is only
886 necessary to pass this argument when calling `get_window_extent`
887 before the first draw. In practice, it is usually easier to
888 trigger a draw first, e.g. by calling
889 `~.Figure.draw_without_rendering` or ``plt.show()``.
891 dpi : float, optional
892 The dpi value for computing the bbox, defaults to
893 ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if
894 to match regions with a figure saved with a custom dpi value.
895 """
896 if not self.get_visible():
897 return Bbox.unit()
898 if dpi is None:
899 dpi = self.figure.dpi
900 if self.get_text() == '':
901 with cbook._setattr_cm(self.figure, dpi=dpi):
902 tx, ty = self._get_xy_display()
903 return Bbox.from_bounds(tx, ty, 0, 0)
905 if renderer is not None:
906 self._renderer = renderer
907 if self._renderer is None:
908 self._renderer = self.figure._get_renderer()
909 if self._renderer is None:
910 raise RuntimeError(
911 "Cannot get window extent of text w/o renderer. You likely "
912 "want to call 'figure.draw_without_rendering()' first.")
914 with cbook._setattr_cm(self.figure, dpi=dpi):
915 bbox, info, descent = self._get_layout(self._renderer)
916 x, y = self.get_unitless_position()
917 x, y = self.get_transform().transform((x, y))
918 bbox = bbox.translated(x, y)
919 return bbox
921 def set_backgroundcolor(self, color):
922 """
923 Set the background color of the text by updating the bbox.
925 Parameters
926 ----------
927 color : color
929 See Also
930 --------
931 .set_bbox : To change the position of the bounding box
932 """
933 if self._bbox_patch is None:
934 self.set_bbox(dict(facecolor=color, edgecolor=color))
935 else:
936 self._bbox_patch.update(dict(facecolor=color))
938 self._update_clip_properties()
939 self.stale = True
941 def set_color(self, color):
942 """
943 Set the foreground color of the text
945 Parameters
946 ----------
947 color : color
948 """
949 # "auto" is only supported by axisartist, but we can just let it error
950 # out at draw time for simplicity.
951 if not cbook._str_equal(color, "auto"):
952 mpl.colors._check_color_like(color=color)
953 # Make sure it is hashable, or get_prop_tup will fail (remove this once
954 # get_prop_tup is removed).
955 try:
956 hash(color)
957 except TypeError:
958 color = tuple(color)
959 self._color = color
960 self.stale = True
962 def set_horizontalalignment(self, align):
963 """
964 Set the horizontal alignment relative to the anchor point.
966 See also :doc:`/gallery/text_labels_and_annotations/text_alignment`.
968 Parameters
969 ----------
970 align : {'left', 'center', 'right'}
971 """
972 _api.check_in_list(['center', 'right', 'left'], align=align)
973 self._horizontalalignment = align
974 self.stale = True
976 def set_multialignment(self, align):
977 """
978 Set the text alignment for multiline texts.
980 The layout of the bounding box of all the lines is determined by the
981 horizontalalignment and verticalalignment properties. This property
982 controls the alignment of the text lines within that box.
984 Parameters
985 ----------
986 align : {'left', 'right', 'center'}
987 """
988 _api.check_in_list(['center', 'right', 'left'], align=align)
989 self._multialignment = align
990 self.stale = True
992 def set_linespacing(self, spacing):
993 """
994 Set the line spacing as a multiple of the font size.
996 The default line spacing is 1.2.
998 Parameters
999 ----------
1000 spacing : float (multiple of font size)
1001 """
1002 _api.check_isinstance(Real, spacing=spacing)
1003 self._linespacing = spacing
1004 self.stale = True
1006 def set_fontfamily(self, fontname):
1007 """
1008 Set the font family. Can be either a single string, or a list of
1009 strings in decreasing priority. Each string may be either a real font
1010 name or a generic font class name. If the latter, the specific font
1011 names will be looked up in the corresponding rcParams.
1013 If a `Text` instance is constructed with ``fontfamily=None``, then the
1014 font is set to :rc:`font.family`, and the
1015 same is done when `set_fontfamily()` is called on an existing
1016 `Text` instance.
1018 Parameters
1019 ----------
1020 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
1021'monospace'}
1023 See Also
1024 --------
1025 .font_manager.FontProperties.set_family
1026 """
1027 self._fontproperties.set_family(fontname)
1028 self.stale = True
1030 def set_fontvariant(self, variant):
1031 """
1032 Set the font variant.
1034 Parameters
1035 ----------
1036 variant : {'normal', 'small-caps'}
1038 See Also
1039 --------
1040 .font_manager.FontProperties.set_variant
1041 """
1042 self._fontproperties.set_variant(variant)
1043 self.stale = True
1045 def set_fontstyle(self, fontstyle):
1046 """
1047 Set the font style.
1049 Parameters
1050 ----------
1051 fontstyle : {'normal', 'italic', 'oblique'}
1053 See Also
1054 --------
1055 .font_manager.FontProperties.set_style
1056 """
1057 self._fontproperties.set_style(fontstyle)
1058 self.stale = True
1060 def set_fontsize(self, fontsize):
1061 """
1062 Set the font size.
1064 Parameters
1065 ----------
1066 fontsize : float or {'xx-small', 'x-small', 'small', 'medium', \
1067'large', 'x-large', 'xx-large'}
1068 If a float, the fontsize in points. The string values denote sizes
1069 relative to the default font size.
1071 See Also
1072 --------
1073 .font_manager.FontProperties.set_size
1074 """
1075 self._fontproperties.set_size(fontsize)
1076 self.stale = True
1078 def get_math_fontfamily(self):
1079 """
1080 Return the font family name for math text rendered by Matplotlib.
1082 The default value is :rc:`mathtext.fontset`.
1084 See Also
1085 --------
1086 set_math_fontfamily
1087 """
1088 return self._fontproperties.get_math_fontfamily()
1090 def set_math_fontfamily(self, fontfamily):
1091 """
1092 Set the font family for math text rendered by Matplotlib.
1094 This does only affect Matplotlib's own math renderer. It has no effect
1095 when rendering with TeX (``usetex=True``).
1097 Parameters
1098 ----------
1099 fontfamily : str
1100 The name of the font family.
1102 Available font families are defined in the
1103 :ref:`matplotlibrc.template file
1104 <customizing-with-matplotlibrc-files>`.
1106 See Also
1107 --------
1108 get_math_fontfamily
1109 """
1110 self._fontproperties.set_math_fontfamily(fontfamily)
1112 def set_fontweight(self, weight):
1113 """
1114 Set the font weight.
1116 Parameters
1117 ----------
1118 weight : {a numeric value in range 0-1000, 'ultralight', 'light', \
1119'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', \
1120'demi', 'bold', 'heavy', 'extra bold', 'black'}
1122 See Also
1123 --------
1124 .font_manager.FontProperties.set_weight
1125 """
1126 self._fontproperties.set_weight(weight)
1127 self.stale = True
1129 def set_fontstretch(self, stretch):
1130 """
1131 Set the font stretch (horizontal condensation or expansion).
1133 Parameters
1134 ----------
1135 stretch : {a numeric value in range 0-1000, 'ultra-condensed', \
1136'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', \
1137'expanded', 'extra-expanded', 'ultra-expanded'}
1139 See Also
1140 --------
1141 .font_manager.FontProperties.set_stretch
1142 """
1143 self._fontproperties.set_stretch(stretch)
1144 self.stale = True
1146 def set_position(self, xy):
1147 """
1148 Set the (*x*, *y*) position of the text.
1150 Parameters
1151 ----------
1152 xy : (float, float)
1153 """
1154 self.set_x(xy[0])
1155 self.set_y(xy[1])
1157 def set_x(self, x):
1158 """
1159 Set the *x* position of the text.
1161 Parameters
1162 ----------
1163 x : float
1164 """
1165 self._x = x
1166 self.stale = True
1168 def set_y(self, y):
1169 """
1170 Set the *y* position of the text.
1172 Parameters
1173 ----------
1174 y : float
1175 """
1176 self._y = y
1177 self.stale = True
1179 def set_rotation(self, s):
1180 """
1181 Set the rotation of the text.
1183 Parameters
1184 ----------
1185 s : float or {'vertical', 'horizontal'}
1186 The rotation angle in degrees in mathematically positive direction
1187 (counterclockwise). 'horizontal' equals 0, 'vertical' equals 90.
1188 """
1189 if isinstance(s, Real):
1190 self._rotation = float(s) % 360
1191 elif cbook._str_equal(s, 'horizontal') or s is None:
1192 self._rotation = 0.
1193 elif cbook._str_equal(s, 'vertical'):
1194 self._rotation = 90.
1195 else:
1196 raise ValueError("rotation must be 'vertical', 'horizontal' or "
1197 f"a number, not {s}")
1198 self.stale = True
1200 def set_transform_rotates_text(self, t):
1201 """
1202 Whether rotations of the transform affect the text direction.
1204 Parameters
1205 ----------
1206 t : bool
1207 """
1208 self._transform_rotates_text = t
1209 self.stale = True
1211 def set_verticalalignment(self, align):
1212 """
1213 Set the vertical alignment relative to the anchor point.
1215 See also :doc:`/gallery/text_labels_and_annotations/text_alignment`.
1217 Parameters
1218 ----------
1219 align : {'bottom', 'baseline', 'center', 'center_baseline', 'top'}
1220 """
1221 _api.check_in_list(
1222 ['top', 'bottom', 'center', 'baseline', 'center_baseline'],
1223 align=align)
1224 self._verticalalignment = align
1225 self.stale = True
1227 def set_text(self, s):
1228 r"""
1229 Set the text string *s*.
1231 It may contain newlines (``\n``) or math in LaTeX syntax.
1233 Parameters
1234 ----------
1235 s : object
1236 Any object gets converted to its `str` representation, except for
1237 ``None`` which is converted to an empty string.
1238 """
1239 if s is None:
1240 s = ''
1241 if s != self._text:
1242 self._text = str(s)
1243 self.stale = True
1245 def _preprocess_math(self, s):
1246 """
1247 Return the string *s* after mathtext preprocessing, and the kind of
1248 mathtext support needed.
1250 - If *self* is configured to use TeX, return *s* unchanged except that
1251 a single space gets escaped, and the flag "TeX".
1252 - Otherwise, if *s* is mathtext (has an even number of unescaped dollar
1253 signs) and ``parse_math`` is not set to False, return *s* and the
1254 flag True.
1255 - Otherwise, return *s* with dollar signs unescaped, and the flag
1256 False.
1257 """
1258 if self.get_usetex():
1259 if s == " ":
1260 s = r"\ "
1261 return s, "TeX"
1262 elif not self.get_parse_math():
1263 return s, False
1264 elif cbook.is_math_text(s):
1265 return s, True
1266 else:
1267 return s.replace(r"\$", "$"), False
1269 def set_fontproperties(self, fp):
1270 """
1271 Set the font properties that control the text.
1273 Parameters
1274 ----------
1275 fp : `.font_manager.FontProperties` or `str` or `pathlib.Path`
1276 If a `str`, it is interpreted as a fontconfig pattern parsed by
1277 `.FontProperties`. If a `pathlib.Path`, it is interpreted as the
1278 absolute path to a font file.
1279 """
1280 self._fontproperties = FontProperties._from_any(fp).copy()
1281 self.stale = True
1283 def set_usetex(self, usetex):
1284 """
1285 Parameters
1286 ----------
1287 usetex : bool or None
1288 Whether to render using TeX, ``None`` means to use
1289 :rc:`text.usetex`.
1290 """
1291 if usetex is None:
1292 self._usetex = mpl.rcParams['text.usetex']
1293 else:
1294 self._usetex = bool(usetex)
1295 self.stale = True
1297 def get_usetex(self):
1298 """Return whether this `Text` object uses TeX for rendering."""
1299 return self._usetex
1301 def set_parse_math(self, parse_math):
1302 """
1303 Override switch to disable any mathtext parsing for this `Text`.
1305 Parameters
1306 ----------
1307 parse_math : bool
1308 If False, this `Text` will never use mathtext. If True, mathtext
1309 will be used if there is an even number of unescaped dollar signs.
1310 """
1311 self._parse_math = bool(parse_math)
1313 def get_parse_math(self):
1314 """Return whether mathtext parsing is considered for this `Text`."""
1315 return self._parse_math
1317 def set_fontname(self, fontname):
1318 """
1319 Alias for `set_family`.
1321 One-way alias only: the getter differs.
1323 Parameters
1324 ----------
1325 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
1326'monospace'}
1328 See Also
1329 --------
1330 .font_manager.FontProperties.set_family
1332 """
1333 return self.set_family(fontname)
1336class OffsetFrom:
1337 """Callable helper class for working with `Annotation`."""
1339 def __init__(self, artist, ref_coord, unit="points"):
1340 """
1341 Parameters
1342 ----------
1343 artist : `.Artist` or `.BboxBase` or `.Transform`
1344 The object to compute the offset from.
1346 ref_coord : (float, float)
1347 If *artist* is an `.Artist` or `.BboxBase`, this values is
1348 the location to of the offset origin in fractions of the
1349 *artist* bounding box.
1351 If *artist* is a transform, the offset origin is the
1352 transform applied to this value.
1354 unit : {'points, 'pixels'}, default: 'points'
1355 The screen units to use (pixels or points) for the offset input.
1356 """
1357 self._artist = artist
1358 self._ref_coord = ref_coord
1359 self.set_unit(unit)
1361 def set_unit(self, unit):
1362 """
1363 Set the unit for input to the transform used by ``__call__``.
1365 Parameters
1366 ----------
1367 unit : {'points', 'pixels'}
1368 """
1369 _api.check_in_list(["points", "pixels"], unit=unit)
1370 self._unit = unit
1372 def get_unit(self):
1373 """Return the unit for input to the transform used by ``__call__``."""
1374 return self._unit
1376 def _get_scale(self, renderer):
1377 unit = self.get_unit()
1378 if unit == "pixels":
1379 return 1.
1380 else:
1381 return renderer.points_to_pixels(1.)
1383 def __call__(self, renderer):
1384 """
1385 Return the offset transform.
1387 Parameters
1388 ----------
1389 renderer : `RendererBase`
1390 The renderer to use to compute the offset
1392 Returns
1393 -------
1394 `Transform`
1395 Maps (x, y) in pixel or point units to screen units
1396 relative to the given artist.
1397 """
1398 if isinstance(self._artist, Artist):
1399 bbox = self._artist.get_window_extent(renderer)
1400 xf, yf = self._ref_coord
1401 x = bbox.x0 + bbox.width * xf
1402 y = bbox.y0 + bbox.height * yf
1403 elif isinstance(self._artist, BboxBase):
1404 bbox = self._artist
1405 xf, yf = self._ref_coord
1406 x = bbox.x0 + bbox.width * xf
1407 y = bbox.y0 + bbox.height * yf
1408 elif isinstance(self._artist, Transform):
1409 x, y = self._artist.transform(self._ref_coord)
1410 else:
1411 raise RuntimeError("unknown type")
1413 sc = self._get_scale(renderer)
1414 tr = Affine2D().scale(sc).translate(x, y)
1416 return tr
1419class _AnnotationBase:
1420 def __init__(self,
1421 xy,
1422 xycoords='data',
1423 annotation_clip=None):
1425 self.xy = xy
1426 self.xycoords = xycoords
1427 self.set_annotation_clip(annotation_clip)
1429 self._draggable = None
1431 def _get_xy(self, renderer, x, y, s):
1432 if isinstance(s, tuple):
1433 s1, s2 = s
1434 else:
1435 s1, s2 = s, s
1436 if s1 == 'data':
1437 x = float(self.convert_xunits(x))
1438 if s2 == 'data':
1439 y = float(self.convert_yunits(y))
1440 return self._get_xy_transform(renderer, s).transform((x, y))
1442 def _get_xy_transform(self, renderer, s):
1444 if isinstance(s, tuple):
1445 s1, s2 = s
1446 from matplotlib.transforms import blended_transform_factory
1447 tr1 = self._get_xy_transform(renderer, s1)
1448 tr2 = self._get_xy_transform(renderer, s2)
1449 tr = blended_transform_factory(tr1, tr2)
1450 return tr
1451 elif callable(s):
1452 tr = s(renderer)
1453 if isinstance(tr, BboxBase):
1454 return BboxTransformTo(tr)
1455 elif isinstance(tr, Transform):
1456 return tr
1457 else:
1458 raise RuntimeError("Unknown return type")
1459 elif isinstance(s, Artist):
1460 bbox = s.get_window_extent(renderer)
1461 return BboxTransformTo(bbox)
1462 elif isinstance(s, BboxBase):
1463 return BboxTransformTo(s)
1464 elif isinstance(s, Transform):
1465 return s
1466 elif not isinstance(s, str):
1467 raise RuntimeError(f"Unknown coordinate type: {s!r}")
1469 if s == 'data':
1470 return self.axes.transData
1471 elif s == 'polar':
1472 from matplotlib.projections import PolarAxes
1473 tr = PolarAxes.PolarTransform()
1474 trans = tr + self.axes.transData
1475 return trans
1477 s_ = s.split()
1478 if len(s_) != 2:
1479 raise ValueError(f"{s!r} is not a recognized coordinate")
1481 bbox0, xy0 = None, None
1483 bbox_name, unit = s_
1484 # if unit is offset-like
1485 if bbox_name == "figure":
1486 bbox0 = self.figure.figbbox
1487 elif bbox_name == "subfigure":
1488 bbox0 = self.figure.bbox
1489 elif bbox_name == "axes":
1490 bbox0 = self.axes.bbox
1491 # elif bbox_name == "bbox":
1492 # if bbox is None:
1493 # raise RuntimeError("bbox is specified as a coordinate but "
1494 # "never set")
1495 # bbox0 = self._get_bbox(renderer, bbox)
1497 if bbox0 is not None:
1498 xy0 = bbox0.p0
1499 elif bbox_name == "offset":
1500 xy0 = self._get_ref_xy(renderer)
1502 if xy0 is not None:
1503 # reference x, y in display coordinate
1504 ref_x, ref_y = xy0
1505 if unit == "points":
1506 # dots per points
1507 dpp = self.figure.dpi / 72
1508 tr = Affine2D().scale(dpp)
1509 elif unit == "pixels":
1510 tr = Affine2D()
1511 elif unit == "fontsize":
1512 fontsize = self.get_size()
1513 dpp = fontsize * self.figure.dpi / 72
1514 tr = Affine2D().scale(dpp)
1515 elif unit == "fraction":
1516 w, h = bbox0.size
1517 tr = Affine2D().scale(w, h)
1518 else:
1519 raise ValueError(f"{unit!r} is not a recognized unit")
1521 return tr.translate(ref_x, ref_y)
1523 else:
1524 raise ValueError(f"{s!r} is not a recognized coordinate")
1526 def _get_ref_xy(self, renderer):
1527 """
1528 Return x, y (in display coordinates) that is to be used for a reference
1529 of any offset coordinate.
1530 """
1531 return self._get_xy(renderer, *self.xy, self.xycoords)
1533 # def _get_bbox(self, renderer):
1534 # if hasattr(bbox, "bounds"):
1535 # return bbox
1536 # elif hasattr(bbox, "get_window_extent"):
1537 # bbox = bbox.get_window_extent()
1538 # return bbox
1539 # else:
1540 # raise ValueError("A bbox instance is expected but got %s" %
1541 # str(bbox))
1543 def set_annotation_clip(self, b):
1544 """
1545 Set the annotation's clipping behavior.
1547 Parameters
1548 ----------
1549 b : bool or None
1550 - True: The annotation will be clipped when ``self.xy`` is
1551 outside the axes.
1552 - False: The annotation will always be drawn.
1553 - None: The annotation will be clipped when ``self.xy`` is
1554 outside the axes and ``self.xycoords == "data"``.
1555 """
1556 self._annotation_clip = b
1558 def get_annotation_clip(self):
1559 """
1560 Return the annotation's clipping behavior.
1562 See `set_annotation_clip` for the meaning of return values.
1563 """
1564 return self._annotation_clip
1566 def _get_position_xy(self, renderer):
1567 """Return the pixel position of the annotated point."""
1568 x, y = self.xy
1569 return self._get_xy(renderer, x, y, 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.
1592 Returns
1593 -------
1594 DraggableAnnotation or None
1595 If the annotation is draggable, the corresponding
1596 `.DraggableAnnotation` helper is returned.
1597 """
1598 from matplotlib.offsetbox import DraggableAnnotation
1599 is_draggable = self._draggable is not None
1601 # if state is None we'll toggle
1602 if state is None:
1603 state = not is_draggable
1605 if state:
1606 if self._draggable is None:
1607 self._draggable = DraggableAnnotation(self, use_blit)
1608 else:
1609 if self._draggable is not None:
1610 self._draggable.disconnect()
1611 self._draggable = None
1613 return self._draggable
1616class Annotation(Text, _AnnotationBase):
1617 """
1618 An `.Annotation` is a `.Text` that can refer to a specific position *xy*.
1619 Optionally an arrow pointing from the text to *xy* can be drawn.
1621 Attributes
1622 ----------
1623 xy
1624 The annotated position.
1625 xycoords
1626 The coordinate system for *xy*.
1627 arrow_patch
1628 A `.FancyArrowPatch` to point from *xytext* to *xy*.
1629 """
1631 def __str__(self):
1632 return "Annotation(%g, %g, %r)" % (self.xy[0], self.xy[1], self._text)
1634 def __init__(self, text, xy,
1635 xytext=None,
1636 xycoords='data',
1637 textcoords=None,
1638 arrowprops=None,
1639 annotation_clip=None,
1640 **kwargs):
1641 """
1642 Annotate the point *xy* with text *text*.
1644 In the simplest form, the text is placed at *xy*.
1646 Optionally, the text can be displayed in another position *xytext*.
1647 An arrow pointing from the text to the annotated point *xy* can then
1648 be added by defining *arrowprops*.
1650 Parameters
1651 ----------
1652 text : str
1653 The text of the annotation.
1655 xy : (float, float)
1656 The point *(x, y)* to annotate. The coordinate system is determined
1657 by *xycoords*.
1659 xytext : (float, float), default: *xy*
1660 The position *(x, y)* to place the text at. The coordinate system
1661 is determined by *textcoords*.
1663 xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \
1664callable, default: 'data'
1666 The coordinate system that *xy* is given in. The following types
1667 of values are supported:
1669 - One of the following strings:
1671 ==================== ============================================
1672 Value Description
1673 ==================== ============================================
1674 'figure points' Points from the lower left of the figure
1675 'figure pixels' Pixels from the lower left of the figure
1676 'figure fraction' Fraction of figure from lower left
1677 'subfigure points' Points from the lower left of the subfigure
1678 'subfigure pixels' Pixels from the lower left of the subfigure
1679 'subfigure fraction' Fraction of subfigure from lower left
1680 'axes points' Points from lower left corner of axes
1681 'axes pixels' Pixels from lower left corner of axes
1682 'axes fraction' Fraction of axes from lower left
1683 'data' Use the coordinate system of the object
1684 being annotated (default)
1685 'polar' *(theta, r)* if not native 'data'
1686 coordinates
1687 ==================== ============================================
1689 Note that 'subfigure pixels' and 'figure pixels' are the same
1690 for the parent figure, so users who want code that is usable in
1691 a subfigure can use 'subfigure pixels'.
1693 - An `.Artist`: *xy* is interpreted as a fraction of the artist's
1694 `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower
1695 left corner of the bounding box and *(0.5, 1)* would be the
1696 center top of the bounding box.
1698 - A `.Transform` to transform *xy* to screen coordinates.
1700 - A function with one of the following signatures::
1702 def transform(renderer) -> Bbox
1703 def transform(renderer) -> Transform
1705 where *renderer* is a `.RendererBase` subclass.
1707 The result of the function is interpreted like the `.Artist` and
1708 `.Transform` cases above.
1710 - A tuple *(xcoords, ycoords)* specifying separate coordinate
1711 systems for *x* and *y*. *xcoords* and *ycoords* must each be
1712 of one of the above described types.
1714 See :ref:`plotting-guide-annotation` for more details.
1716 textcoords : single or two-tuple of str or `.Artist` or `.Transform` \
1717or callable, default: value of *xycoords*
1718 The coordinate system that *xytext* is given in.
1720 All *xycoords* values are valid as well as the following
1721 strings:
1723 ================= =========================================
1724 Value Description
1725 ================= =========================================
1726 'offset points' Offset (in points) from the *xy* value
1727 'offset pixels' Offset (in pixels) from the *xy* value
1728 ================= =========================================
1730 arrowprops : dict, optional
1731 The properties used to draw a `.FancyArrowPatch` arrow between the
1732 positions *xy* and *xytext*. Defaults to None, i.e. no arrow is
1733 drawn.
1735 For historical reasons there are two different ways to specify
1736 arrows, "simple" and "fancy":
1738 **Simple arrow:**
1740 If *arrowprops* does not contain the key 'arrowstyle' the
1741 allowed keys are:
1743 ========== ======================================================
1744 Key Description
1745 ========== ======================================================
1746 width The width of the arrow in points
1747 headwidth The width of the base of the arrow head in points
1748 headlength The length of the arrow head in points
1749 shrink Fraction of total length to shrink from both ends
1750 ? Any key to :class:`matplotlib.patches.FancyArrowPatch`
1751 ========== ======================================================
1753 The arrow is attached to the edge of the text box, the exact
1754 position (corners or centers) depending on where it's pointing to.
1756 **Fancy arrow:**
1758 This is used if 'arrowstyle' is provided in the *arrowprops*.
1760 Valid keys are the following `~matplotlib.patches.FancyArrowPatch`
1761 parameters:
1763 =============== ==================================================
1764 Key Description
1765 =============== ==================================================
1766 arrowstyle the arrow style
1767 connectionstyle the connection style
1768 relpos see below; default is (0.5, 0.5)
1769 patchA default is bounding box of the text
1770 patchB default is None
1771 shrinkA default is 2 points
1772 shrinkB default is 2 points
1773 mutation_scale default is text size (in points)
1774 mutation_aspect default is 1.
1775 ? any key for :class:`matplotlib.patches.PathPatch`
1776 =============== ==================================================
1778 The exact starting point position of the arrow is defined by
1779 *relpos*. It's a tuple of relative coordinates of the text box,
1780 where (0, 0) is the lower left corner and (1, 1) is the upper
1781 right corner. Values <0 and >1 are supported and specify points
1782 outside the text box. By default (0.5, 0.5), so the starting point
1783 is centered in the text box.
1785 annotation_clip : bool or None, default: None
1786 Whether to clip (i.e. not draw) the annotation when the annotation
1787 point *xy* is outside the axes area.
1789 - If *True*, the annotation will be clipped when *xy* is outside
1790 the axes.
1791 - If *False*, the annotation will always be drawn.
1792 - If *None*, the annotation will be clipped when *xy* is outside
1793 the axes and *xycoords* is 'data'.
1795 **kwargs
1796 Additional kwargs are passed to `~matplotlib.text.Text`.
1798 Returns
1799 -------
1800 `.Annotation`
1802 See Also
1803 --------
1804 :ref:`plotting-guide-annotation`
1806 """
1807 _AnnotationBase.__init__(self,
1808 xy,
1809 xycoords=xycoords,
1810 annotation_clip=annotation_clip)
1811 # warn about wonky input data
1812 if (xytext is None and
1813 textcoords is not None and
1814 textcoords != xycoords):
1815 _api.warn_external("You have used the `textcoords` kwarg, but "
1816 "not the `xytext` kwarg. This can lead to "
1817 "surprising results.")
1819 # clean up textcoords and assign default
1820 if textcoords is None:
1821 textcoords = self.xycoords
1822 self._textcoords = textcoords
1824 # cleanup xytext defaults
1825 if xytext is None:
1826 xytext = self.xy
1827 x, y = xytext
1829 self.arrowprops = arrowprops
1830 if arrowprops is not None:
1831 arrowprops = arrowprops.copy()
1832 if "arrowstyle" in arrowprops:
1833 self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
1834 else:
1835 # modified YAArrow API to be used with FancyArrowPatch
1836 for key in [
1837 'width', 'headwidth', 'headlength', 'shrink', 'frac']:
1838 arrowprops.pop(key, None)
1839 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops)
1840 else:
1841 self.arrow_patch = None
1843 # Must come last, as some kwargs may be propagated to arrow_patch.
1844 Text.__init__(self, x, y, text, **kwargs)
1846 def contains(self, event):
1847 inside, info = self._default_contains(event)
1848 if inside is not None:
1849 return inside, info
1850 contains, tinfo = Text.contains(self, event)
1851 if self.arrow_patch is not None:
1852 in_patch, _ = self.arrow_patch.contains(event)
1853 contains = contains or in_patch
1854 return contains, tinfo
1856 @property
1857 def xycoords(self):
1858 return self._xycoords
1860 @xycoords.setter
1861 def xycoords(self, xycoords):
1862 def is_offset(s):
1863 return isinstance(s, str) and s.startswith("offset")
1865 if (isinstance(xycoords, tuple) and any(map(is_offset, xycoords))
1866 or is_offset(xycoords)):
1867 raise ValueError("xycoords cannot be an offset coordinate")
1868 self._xycoords = xycoords
1870 @property
1871 def xyann(self):
1872 """
1873 The text position.
1875 See also *xytext* in `.Annotation`.
1876 """
1877 return self.get_position()
1879 @xyann.setter
1880 def xyann(self, xytext):
1881 self.set_position(xytext)
1883 def get_anncoords(self):
1884 """
1885 Return the coordinate system to use for `.Annotation.xyann`.
1887 See also *xycoords* in `.Annotation`.
1888 """
1889 return self._textcoords
1891 def set_anncoords(self, coords):
1892 """
1893 Set the coordinate system to use for `.Annotation.xyann`.
1895 See also *xycoords* in `.Annotation`.
1896 """
1897 self._textcoords = coords
1899 anncoords = property(get_anncoords, set_anncoords, doc="""
1900 The coordinate system to use for `.Annotation.xyann`.""")
1902 def set_figure(self, fig):
1903 # docstring inherited
1904 if self.arrow_patch is not None:
1905 self.arrow_patch.set_figure(fig)
1906 Artist.set_figure(self, fig)
1908 def update_positions(self, renderer):
1909 """
1910 Update the pixel positions of the annotation text and the arrow patch.
1911 """
1912 # generate transformation
1913 self.set_transform(self._get_xy_transform(renderer, self.anncoords))
1915 arrowprops = self.arrowprops
1916 if arrowprops is None:
1917 return
1919 bbox = Text.get_window_extent(self, renderer)
1921 arrow_end = x1, y1 = self._get_position_xy(renderer) # Annotated pos.
1923 ms = arrowprops.get("mutation_scale", self.get_size())
1924 self.arrow_patch.set_mutation_scale(ms)
1926 if "arrowstyle" not in arrowprops:
1927 # Approximately simulate the YAArrow.
1928 shrink = arrowprops.get('shrink', 0.0)
1929 width = arrowprops.get('width', 4)
1930 headwidth = arrowprops.get('headwidth', 12)
1931 if 'frac' in arrowprops:
1932 _api.warn_external(
1933 "'frac' option in 'arrowprops' is no longer supported;"
1934 " use 'headlength' to set the head length in points.")
1935 headlength = arrowprops.get('headlength', 12)
1937 # NB: ms is in pts
1938 stylekw = dict(head_length=headlength / ms,
1939 head_width=headwidth / ms,
1940 tail_width=width / ms)
1942 self.arrow_patch.set_arrowstyle('simple', **stylekw)
1944 # using YAArrow style:
1945 # pick the corner of the text bbox closest to annotated point.
1946 xpos = [(bbox.x0, 0), ((bbox.x0 + bbox.x1) / 2, 0.5), (bbox.x1, 1)]
1947 ypos = [(bbox.y0, 0), ((bbox.y0 + bbox.y1) / 2, 0.5), (bbox.y1, 1)]
1948 x, relposx = min(xpos, key=lambda v: abs(v[0] - x1))
1949 y, relposy = min(ypos, key=lambda v: abs(v[0] - y1))
1950 self._arrow_relpos = (relposx, relposy)
1951 r = np.hypot(y - y1, x - x1)
1952 shrink_pts = shrink * r / renderer.points_to_pixels(1)
1953 self.arrow_patch.shrinkA = self.arrow_patch.shrinkB = shrink_pts
1955 # adjust the starting point of the arrow relative to the textbox.
1956 # TODO : Rotation needs to be accounted.
1957 arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos
1958 # The arrow is drawn from arrow_begin to arrow_end. It will be first
1959 # clipped by patchA and patchB. Then it will be shrunk by shrinkA and
1960 # shrinkB (in points). If patchA is not set, self.bbox_patch is used.
1961 self.arrow_patch.set_positions(arrow_begin, arrow_end)
1963 if "patchA" in arrowprops:
1964 patchA = arrowprops["patchA"]
1965 elif self._bbox_patch:
1966 patchA = self._bbox_patch
1967 elif self.get_text() == "":
1968 patchA = None
1969 else:
1970 pad = renderer.points_to_pixels(4)
1971 patchA = Rectangle(
1972 xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2),
1973 width=bbox.width + pad, height=bbox.height + pad,
1974 transform=IdentityTransform(), clip_on=False)
1975 self.arrow_patch.set_patchA(patchA)
1977 @artist.allow_rasterization
1978 def draw(self, renderer):
1979 # docstring inherited
1980 if renderer is not None:
1981 self._renderer = renderer
1982 if not self.get_visible() or not self._check_xy(renderer):
1983 return
1984 # Update text positions before `Text.draw` would, so that the
1985 # FancyArrowPatch is correctly positioned.
1986 self.update_positions(renderer)
1987 self.update_bbox_position_size(renderer)
1988 if self.arrow_patch is not None: # FancyArrowPatch
1989 if self.arrow_patch.figure is None and self.figure is not None:
1990 self.arrow_patch.figure = self.figure
1991 self.arrow_patch.draw(renderer)
1992 # Draw text, including FancyBboxPatch, after FancyArrowPatch.
1993 # Otherwise, a wedge arrowstyle can land partly on top of the Bbox.
1994 Text.draw(self, renderer)
1996 def get_window_extent(self, renderer=None):
1997 # docstring inherited
1998 # This block is the same as in Text.get_window_extent, but we need to
1999 # set the renderer before calling update_positions().
2000 if not self.get_visible() or not self._check_xy(renderer):
2001 return Bbox.unit()
2002 if renderer is not None:
2003 self._renderer = renderer
2004 if self._renderer is None:
2005 self._renderer = self.figure._get_renderer()
2006 if self._renderer is None:
2007 raise RuntimeError('Cannot get window extent w/o renderer')
2009 self.update_positions(self._renderer)
2011 text_bbox = Text.get_window_extent(self)
2012 bboxes = [text_bbox]
2014 if self.arrow_patch is not None:
2015 bboxes.append(self.arrow_patch.get_window_extent())
2017 return Bbox.union(bboxes)
2019 def get_tightbbox(self, renderer=None):
2020 # docstring inherited
2021 if not self._check_xy(renderer):
2022 return Bbox.null()
2023 return super().get_tightbbox(renderer)
2026_docstring.interpd.update(Annotation=Annotation.__init__.__doc__)