Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/text.py : 16%

Hot-keys on this page
r m x p 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 contextlib
6import logging
7import math
8import weakref
10import numpy as np
12from . import artist, cbook, docstring, rcParams
13from .artist import Artist
14from .font_manager import FontProperties
15from .lines import Line2D
16from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle
17from .textpath import TextPath # Unused, but imported by others.
18from .transforms import (
19 Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform)
22_log = logging.getLogger(__name__)
25@contextlib.contextmanager
26def _wrap_text(textobj):
27 """Temporarily inserts newlines to the text if the wrap option is enabled.
28 """
29 if textobj.get_wrap():
30 old_text = textobj.get_text()
31 try:
32 textobj.set_text(textobj._get_wrapped_text())
33 yield textobj
34 finally:
35 textobj.set_text(old_text)
36 else:
37 yield textobj
40# Extracted from Text's method to serve as a function
41def get_rotation(rotation):
42 """
43 Return the text angle as float between 0 and 360 degrees.
45 *rotation* may be 'horizontal', 'vertical', or a numeric value in degrees.
46 """
47 try:
48 return float(rotation) % 360
49 except (ValueError, TypeError):
50 if cbook._str_equal(rotation, 'horizontal') or rotation is None:
51 return 0.
52 elif cbook._str_equal(rotation, 'vertical'):
53 return 90.
54 else:
55 raise ValueError("rotation is {!r}; expected either 'horizontal', "
56 "'vertical', numeric value, or None"
57 .format(rotation))
60def _get_textbox(text, renderer):
61 """
62 Calculate the bounding box of the text. Unlike
63 :meth:`matplotlib.text.Text.get_extents` method, The bbox size of
64 the text before the rotation is calculated.
65 """
66 # TODO : This function may move into the Text class as a method. As a
67 # matter of fact, The information from the _get_textbox function
68 # should be available during the Text._get_layout() call, which is
69 # called within the _get_textbox. So, it would better to move this
70 # function as a method with some refactoring of _get_layout method.
72 projected_xs = []
73 projected_ys = []
75 theta = np.deg2rad(text.get_rotation())
76 tr = Affine2D().rotate(-theta)
78 _, parts, d = text._get_layout(renderer)
80 for t, wh, x, y in parts:
81 w, h = wh
83 xt1, yt1 = tr.transform((x, y))
84 yt1 -= d
85 xt2, yt2 = xt1 + w, yt1 + h
87 projected_xs.extend([xt1, xt2])
88 projected_ys.extend([yt1, yt2])
90 xt_box, yt_box = min(projected_xs), min(projected_ys)
91 w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
93 x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box))
95 return x_box, y_box, w_box, h_box
98@cbook._define_aliases({
99 "color": ["c"],
100 "fontfamily": ["family"],
101 "fontproperties": ["font_properties"],
102 "horizontalalignment": ["ha"],
103 "multialignment": ["ma"],
104 "fontname": ["name"],
105 "fontsize": ["size"],
106 "fontstretch": ["stretch"],
107 "fontstyle": ["style"],
108 "fontvariant": ["variant"],
109 "verticalalignment": ["va"],
110 "fontweight": ["weight"],
111})
112class Text(Artist):
113 """Handle storing and drawing of text in window or data coordinates."""
115 zorder = 3
116 _cached = cbook.maxdict(50)
118 def __repr__(self):
119 return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text))
121 def __init__(self,
122 x=0, y=0, text='',
123 color=None, # defaults to rc params
124 verticalalignment='baseline',
125 horizontalalignment='left',
126 multialignment=None,
127 fontproperties=None, # defaults to FontProperties()
128 rotation=None,
129 linespacing=None,
130 rotation_mode=None,
131 usetex=None, # defaults to rcParams['text.usetex']
132 wrap=False,
133 **kwargs
134 ):
135 """
136 Create a `.Text` instance at *x*, *y* with string *text*.
138 Valid keyword arguments are:
140 %(Text)s
141 """
142 Artist.__init__(self)
143 self._x, self._y = x, y
145 if color is None:
146 color = rcParams['text.color']
147 if fontproperties is None:
148 fontproperties = FontProperties()
149 elif isinstance(fontproperties, str):
150 fontproperties = FontProperties(fontproperties)
152 self._text = ''
153 self.set_text(text)
154 self.set_color(color)
155 self.set_usetex(usetex)
156 self.set_wrap(wrap)
157 self.set_verticalalignment(verticalalignment)
158 self.set_horizontalalignment(horizontalalignment)
159 self._multialignment = multialignment
160 self._rotation = rotation
161 self._fontproperties = fontproperties
162 self._bbox_patch = None # a FancyBboxPatch instance
163 self._renderer = None
164 if linespacing is None:
165 linespacing = 1.2 # Maybe use rcParam later.
166 self._linespacing = linespacing
167 self.set_rotation_mode(rotation_mode)
168 self.update(kwargs)
170 def update(self, kwargs):
171 """
172 Update properties from a dictionary.
173 """
174 # Update bbox last, as it depends on font properties.
175 sentinel = object() # bbox can be None, so use another sentinel.
176 bbox = kwargs.pop("bbox", sentinel)
177 super().update(kwargs)
178 if bbox is not sentinel:
179 self.set_bbox(bbox)
181 def __getstate__(self):
182 d = super().__getstate__()
183 # remove the cached _renderer (if it exists)
184 d['_renderer'] = None
185 return d
187 def contains(self, mouseevent):
188 """Test whether the mouse event occurred in the patch.
190 In the case of text, a hit is true anywhere in the
191 axis-aligned bounding-box containing the text.
193 Returns
194 -------
195 bool : bool
196 """
197 inside, info = self._default_contains(mouseevent)
198 if inside is not None:
199 return inside, info
201 if not self.get_visible() or self._renderer is None:
202 return False, {}
204 # Explicitly use Text.get_window_extent(self) and not
205 # self.get_window_extent() so that Annotation.contains does not
206 # accidentally cover the entire annotation bounding box.
207 l, b, w, h = Text.get_window_extent(self).bounds
208 r, t = l + w, b + h
210 x, y = mouseevent.x, mouseevent.y
211 inside = (l <= x <= r and b <= y <= t)
212 cattr = {}
214 # if the text has a surrounding patch, also check containment for it,
215 # and merge the results with the results for the text.
216 if self._bbox_patch:
217 patch_inside, patch_cattr = self._bbox_patch.contains(mouseevent)
218 inside = inside or patch_inside
219 cattr["bbox_patch"] = patch_cattr
221 return inside, cattr
223 def _get_xy_display(self):
224 """
225 Get the (possibly unit converted) transformed x, y in display coords.
226 """
227 x, y = self.get_unitless_position()
228 return self.get_transform().transform((x, y))
230 def _get_multialignment(self):
231 if self._multialignment is not None:
232 return self._multialignment
233 else:
234 return self._horizontalalignment
236 def get_rotation(self):
237 """Return the text angle as float in degrees."""
238 return get_rotation(self._rotation) # string_or_number -> number
240 def set_rotation_mode(self, m):
241 """
242 Set text rotation mode.
244 Parameters
245 ----------
246 m : {None, 'default', 'anchor'}
247 If ``None`` or ``"default"``, the text will be first rotated, then
248 aligned according to their horizontal and vertical alignments. If
249 ``"anchor"``, then alignment occurs before rotation.
250 """
251 cbook._check_in_list(["anchor", "default", None], rotation_mode=m)
252 self._rotation_mode = m
253 self.stale = True
255 def get_rotation_mode(self):
256 """Get the text rotation mode."""
257 return self._rotation_mode
259 def update_from(self, other):
260 """Copy properties from other to self."""
261 Artist.update_from(self, other)
262 self._color = other._color
263 self._multialignment = other._multialignment
264 self._verticalalignment = other._verticalalignment
265 self._horizontalalignment = other._horizontalalignment
266 self._fontproperties = other._fontproperties.copy()
267 self._rotation = other._rotation
268 self._picker = other._picker
269 self._linespacing = other._linespacing
270 self.stale = True
272 def _get_layout(self, renderer):
273 """
274 return the extent (bbox) of the text together with
275 multiple-alignment information. Note that it returns an extent
276 of a rotated text when necessary.
277 """
278 key = self.get_prop_tup(renderer=renderer)
279 if key in self._cached:
280 return self._cached[key]
282 thisx, thisy = 0.0, 0.0
283 lines = self.get_text().split("\n") # Ensures lines is not empty.
285 ws = []
286 hs = []
287 xs = []
288 ys = []
290 # Full vertical extent of font, including ascenders and descenders:
291 _, lp_h, lp_d = renderer.get_text_width_height_descent(
292 "lp", self._fontproperties,
293 ismath="TeX" if self.get_usetex() else False)
294 min_dy = (lp_h - lp_d) * self._linespacing
296 for i, line in enumerate(lines):
297 clean_line, ismath = self._preprocess_math(line)
298 if clean_line:
299 w, h, d = renderer.get_text_width_height_descent(
300 clean_line, self._fontproperties, ismath=ismath)
301 else:
302 w = h = d = 0
304 # For multiline text, increase the line spacing when the text
305 # net-height (excluding baseline) is larger than that of a "l"
306 # (e.g., use of superscripts), which seems what TeX does.
307 h = max(h, lp_h)
308 d = max(d, lp_d)
310 ws.append(w)
311 hs.append(h)
313 # Metrics of the last line that are needed later:
314 baseline = (h - d) - thisy
316 if i == 0:
317 # position at baseline
318 thisy = -(h - d)
319 else:
320 # put baseline a good distance from bottom of previous line
321 thisy -= max(min_dy, (h - d) * self._linespacing)
323 xs.append(thisx) # == 0.
324 ys.append(thisy)
326 thisy -= d
328 # Metrics of the last line that are needed later:
329 descent = d
331 # Bounding box definition:
332 width = max(ws)
333 xmin = 0
334 xmax = width
335 ymax = 0
336 ymin = ys[-1] - descent # baseline of last line minus its descent
337 height = ymax - ymin
339 # get the rotation matrix
340 M = Affine2D().rotate_deg(self.get_rotation())
342 # now offset the individual text lines within the box
343 malign = self._get_multialignment()
344 if malign == 'left':
345 offset_layout = [(x, y) for x, y in zip(xs, ys)]
346 elif malign == 'center':
347 offset_layout = [(x + width / 2 - w / 2, y)
348 for x, y, w in zip(xs, ys, ws)]
349 elif malign == 'right':
350 offset_layout = [(x + width - w, y)
351 for x, y, w in zip(xs, ys, ws)]
353 # the corners of the unrotated bounding box
354 corners_horiz = np.array(
355 [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
357 # now rotate the bbox
358 corners_rotated = M.transform(corners_horiz)
359 # compute the bounds of the rotated box
360 xmin = corners_rotated[:, 0].min()
361 xmax = corners_rotated[:, 0].max()
362 ymin = corners_rotated[:, 1].min()
363 ymax = corners_rotated[:, 1].max()
364 width = xmax - xmin
365 height = ymax - ymin
367 # Now move the box to the target position offset the display
368 # bbox by alignment
369 halign = self._horizontalalignment
370 valign = self._verticalalignment
372 rotation_mode = self.get_rotation_mode()
373 if rotation_mode != "anchor":
374 # compute the text location in display coords and the offsets
375 # necessary to align the bbox with that location
376 if halign == 'center':
377 offsetx = (xmin + xmax) / 2
378 elif halign == 'right':
379 offsetx = xmax
380 else:
381 offsetx = xmin
383 if valign == 'center':
384 offsety = (ymin + ymax) / 2
385 elif valign == 'top':
386 offsety = ymax
387 elif valign == 'baseline':
388 offsety = ymin + descent
389 elif valign == 'center_baseline':
390 offsety = ymin + height - baseline / 2.0
391 else:
392 offsety = ymin
393 else:
394 xmin1, ymin1 = corners_horiz[0]
395 xmax1, ymax1 = corners_horiz[2]
397 if halign == 'center':
398 offsetx = (xmin1 + xmax1) / 2.0
399 elif halign == 'right':
400 offsetx = xmax1
401 else:
402 offsetx = xmin1
404 if valign == 'center':
405 offsety = (ymin1 + ymax1) / 2.0
406 elif valign == 'top':
407 offsety = ymax1
408 elif valign == 'baseline':
409 offsety = ymax1 - baseline
410 elif valign == 'center_baseline':
411 offsety = ymax1 - baseline / 2.0
412 else:
413 offsety = ymin1
415 offsetx, offsety = M.transform((offsetx, offsety))
417 xmin -= offsetx
418 ymin -= offsety
420 bbox = Bbox.from_bounds(xmin, ymin, width, height)
422 # now rotate the positions around the first (x, y) position
423 xys = M.transform(offset_layout) - (offsetx, offsety)
425 ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
426 self._cached[key] = ret
427 return ret
429 def set_bbox(self, rectprops):
430 """
431 Draw a bounding box around self.
433 Parameters
434 ----------
435 rectprops : dict with properties for `.patches.FancyBboxPatch`
436 The default boxstyle is 'square'. The mutation
437 scale of the `.patches.FancyBboxPatch` is set to the fontsize.
439 Examples
440 --------
441 ::
443 t.set_bbox(dict(facecolor='red', alpha=0.5))
444 """
446 if rectprops is not None:
447 props = rectprops.copy()
448 boxstyle = props.pop("boxstyle", None)
449 pad = props.pop("pad", None)
450 if boxstyle is None:
451 boxstyle = "square"
452 if pad is None:
453 pad = 4 # points
454 pad /= self.get_size() # to fraction of font size
455 else:
456 if pad is None:
457 pad = 0.3
459 # boxstyle could be a callable or a string
460 if isinstance(boxstyle, str) and "pad" not in boxstyle:
461 boxstyle += ",pad=%0.2f" % pad
463 bbox_transmuter = props.pop("bbox_transmuter", None)
465 self._bbox_patch = FancyBboxPatch(
466 (0., 0.),
467 1., 1.,
468 boxstyle=boxstyle,
469 bbox_transmuter=bbox_transmuter,
470 transform=IdentityTransform(),
471 **props)
472 else:
473 self._bbox_patch = None
475 self._update_clip_properties()
477 def get_bbox_patch(self):
478 """
479 Return the bbox Patch, or None if the `.patches.FancyBboxPatch`
480 is not made.
481 """
482 return self._bbox_patch
484 def update_bbox_position_size(self, renderer):
485 """
486 Update the location and the size of the bbox.
488 This method should be used when the position and size of the bbox needs
489 to be updated before actually drawing the bbox.
490 """
492 if self._bbox_patch:
494 trans = self.get_transform()
496 # don't use self.get_unitless_position here, which refers to text
497 # position in Text, and dash position in TextWithDash:
498 posx = float(self.convert_xunits(self._x))
499 posy = float(self.convert_yunits(self._y))
501 posx, posy = trans.transform((posx, posy))
503 x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
504 self._bbox_patch.set_bounds(0., 0., w_box, h_box)
505 theta = np.deg2rad(self.get_rotation())
506 tr = Affine2D().rotate(theta)
507 tr = tr.translate(posx + x_box, posy + y_box)
508 self._bbox_patch.set_transform(tr)
509 fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
510 self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
512 def _draw_bbox(self, renderer, posx, posy):
513 """
514 Update the location and size of the bbox (`.patches.FancyBboxPatch`),
515 and draw.
516 """
518 x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
519 self._bbox_patch.set_bounds(0., 0., w_box, h_box)
520 theta = np.deg2rad(self.get_rotation())
521 tr = Affine2D().rotate(theta)
522 tr = tr.translate(posx + x_box, posy + y_box)
523 self._bbox_patch.set_transform(tr)
524 fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
525 self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
526 self._bbox_patch.draw(renderer)
528 def _update_clip_properties(self):
529 clipprops = dict(clip_box=self.clipbox,
530 clip_path=self._clippath,
531 clip_on=self._clipon)
532 if self._bbox_patch:
533 self._bbox_patch.update(clipprops)
535 def set_clip_box(self, clipbox):
536 # docstring inherited.
537 super().set_clip_box(clipbox)
538 self._update_clip_properties()
540 def set_clip_path(self, path, transform=None):
541 # docstring inherited.
542 super().set_clip_path(path, transform)
543 self._update_clip_properties()
545 def set_clip_on(self, b):
546 # docstring inherited.
547 super().set_clip_on(b)
548 self._update_clip_properties()
550 def get_wrap(self):
551 """Return the wrapping state for the text."""
552 return self._wrap
554 def set_wrap(self, wrap):
555 """Set the wrapping state for the text.
557 Parameters
558 ----------
559 wrap : bool
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 with new lines added, so that
625 the text is wrapped relative to the parent figure.
626 """
627 # Not fit to handle breaking up latex syntax correctly, so
628 # ignore latex for now.
629 if self.get_usetex():
630 return self.get_text()
632 # Build the line incrementally, for a more accurate measure of length
633 line_width = self._get_wrap_line_width()
634 wrapped_lines = []
636 # New lines in the user's text force a split
637 unwrapped_lines = self.get_text().split('\n')
639 # Now wrap each individual unwrapped line
640 for unwrapped_line in unwrapped_lines:
642 sub_words = unwrapped_line.split(' ')
643 # Remove items from sub_words as we go, so stop when empty
644 while len(sub_words) > 0:
645 if len(sub_words) == 1:
646 # Only one word, so just add it to the end
647 wrapped_lines.append(sub_words.pop(0))
648 continue
650 for i in range(2, len(sub_words) + 1):
651 # Get width of all words up to and including here
652 line = ' '.join(sub_words[:i])
653 current_width = self._get_rendered_text_width(line)
655 # If all these words are too wide, append all not including
656 # last word
657 if current_width > line_width:
658 wrapped_lines.append(' '.join(sub_words[:i - 1]))
659 sub_words = sub_words[i - 1:]
660 break
662 # Otherwise if all words fit in the width, append them all
663 elif i == len(sub_words):
664 wrapped_lines.append(' '.join(sub_words[:i]))
665 sub_words = []
666 break
668 return '\n'.join(wrapped_lines)
670 @artist.allow_rasterization
671 def draw(self, renderer):
672 """
673 Draws the `.Text` object to the given *renderer*.
674 """
675 if renderer is not None:
676 self._renderer = renderer
677 if not self.get_visible():
678 return
679 if self.get_text() == '':
680 return
682 renderer.open_group('text', self.get_gid())
684 with _wrap_text(self) as textobj:
685 bbox, info, descent = textobj._get_layout(renderer)
686 trans = textobj.get_transform()
688 # don't use textobj.get_position here, which refers to text
689 # position in Text, and dash position in TextWithDash:
690 posx = float(textobj.convert_xunits(textobj._x))
691 posy = float(textobj.convert_yunits(textobj._y))
692 posx, posy = trans.transform((posx, posy))
693 if not np.isfinite(posx) or not np.isfinite(posy):
694 _log.warning("posx and posy should be finite values")
695 return
696 canvasw, canvash = renderer.get_canvas_width_height()
698 # draw the FancyBboxPatch
699 if textobj._bbox_patch:
700 textobj._draw_bbox(renderer, posx, posy)
702 gc = renderer.new_gc()
703 gc.set_foreground(textobj.get_color())
704 gc.set_alpha(textobj.get_alpha())
705 gc.set_url(textobj._url)
706 textobj._set_gc_clip(gc)
708 angle = textobj.get_rotation()
710 for line, wh, x, y in info:
712 mtext = textobj if len(info) == 1 else None
713 x = x + posx
714 y = y + posy
715 if renderer.flipy():
716 y = canvash - y
717 clean_line, ismath = textobj._preprocess_math(line)
719 if textobj.get_path_effects():
720 from matplotlib.patheffects import PathEffectRenderer
721 textrenderer = PathEffectRenderer(
722 textobj.get_path_effects(), renderer)
723 else:
724 textrenderer = renderer
726 if textobj.get_usetex():
727 textrenderer.draw_tex(gc, x, y, clean_line,
728 textobj._fontproperties, angle,
729 mtext=mtext)
730 else:
731 textrenderer.draw_text(gc, x, y, clean_line,
732 textobj._fontproperties, angle,
733 ismath=ismath, mtext=mtext)
735 gc.restore()
736 renderer.close_group('text')
737 self.stale = False
739 def get_color(self):
740 "Return the color of the text"
741 return self._color
743 def get_fontproperties(self):
744 "Return the `.font_manager.FontProperties` object"
745 return self._fontproperties
747 def get_fontfamily(self):
748 """
749 Return the list of font families used for font lookup
751 See Also
752 --------
753 .font_manager.FontProperties.get_family
754 """
755 return self._fontproperties.get_family()
757 def get_fontname(self):
758 """
759 Return the font name as string
761 See Also
762 --------
763 .font_manager.FontProperties.get_name
764 """
765 return self._fontproperties.get_name()
767 def get_fontstyle(self):
768 """
769 Return the font style as string
771 See Also
772 --------
773 .font_manager.FontProperties.get_style
774 """
775 return self._fontproperties.get_style()
777 def get_fontsize(self):
778 """
779 Return the font size as integer
781 See Also
782 --------
783 .font_manager.FontProperties.get_size_in_points
784 """
785 return self._fontproperties.get_size_in_points()
787 def get_fontvariant(self):
788 """
789 Return the font variant as a string
791 See Also
792 --------
793 .font_manager.FontProperties.get_variant
794 """
795 return self._fontproperties.get_variant()
797 def get_fontweight(self):
798 """
799 Get the font weight as string or number
801 See Also
802 --------
803 .font_manager.FontProperties.get_weight
804 """
805 return self._fontproperties.get_weight()
807 def get_stretch(self):
808 """
809 Get the font stretch as a string or number
811 See Also
812 --------
813 .font_manager.FontProperties.get_stretch
814 """
815 return self._fontproperties.get_stretch()
817 def get_horizontalalignment(self):
818 """
819 Return the horizontal alignment as string. Will be one of
820 'left', 'center' or 'right'.
821 """
822 return self._horizontalalignment
824 def get_unitless_position(self):
825 "Return the unitless position of the text as a tuple (*x*, *y*)"
826 # This will get the position with all unit information stripped away.
827 # This is here for convenience since it is done in several locations.
828 x = float(self.convert_xunits(self._x))
829 y = float(self.convert_yunits(self._y))
830 return x, y
832 def get_position(self):
833 "Return the position of the text as a tuple (*x*, *y*)"
834 # This should return the same data (possible unitized) as was
835 # specified with 'set_x' and 'set_y'.
836 return self._x, self._y
838 def get_prop_tup(self, renderer=None):
839 """
840 Return a hashable tuple of properties.
842 Not intended to be human readable, but useful for backends who
843 want to cache derived information about text (e.g., layouts) and
844 need to know if the text has changed.
845 """
846 x, y = self.get_unitless_position()
847 renderer = renderer or self._renderer
848 return (x, y, self.get_text(), self._color,
849 self._verticalalignment, self._horizontalalignment,
850 hash(self._fontproperties),
851 self._rotation, self._rotation_mode,
852 self.figure.dpi, weakref.ref(renderer),
853 self._linespacing
854 )
856 def get_text(self):
857 "Get the text as string"
858 return self._text
860 def get_verticalalignment(self):
861 """
862 Return the vertical alignment as string. Will be one of
863 'top', 'center', 'bottom' or 'baseline'.
864 """
865 return self._verticalalignment
867 def get_window_extent(self, renderer=None, dpi=None):
868 """
869 Return the `.Bbox` bounding the text, in display units.
871 In addition to being used internally, this is useful for specifying
872 clickable regions in a png file on a web page.
874 Parameters
875 ----------
876 renderer : Renderer, optional
877 A renderer is needed to compute the bounding box. If the artist
878 has already been drawn, the renderer is cached; thus, it is only
879 necessary to pass this argument when calling `get_window_extent`
880 before the first `draw`. In practice, it is usually easier to
881 trigger a draw first (e.g. by saving the figure).
883 dpi : float, optional
884 The dpi value for computing the bbox, defaults to
885 ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if
886 to match regions with a figure saved with a custom dpi value.
887 """
888 #return _unit_box
889 if not self.get_visible():
890 return Bbox.unit()
891 if dpi is not None:
892 dpi_orig = self.figure.dpi
893 self.figure.dpi = dpi
894 if self.get_text() == '':
895 tx, ty = self._get_xy_display()
896 return Bbox.from_bounds(tx, ty, 0, 0)
898 if renderer is not None:
899 self._renderer = renderer
900 if self._renderer is None:
901 self._renderer = self.figure._cachedRenderer
902 if self._renderer is None:
903 raise RuntimeError('Cannot get window extent w/o renderer')
905 bbox, info, descent = self._get_layout(self._renderer)
906 x, y = self.get_unitless_position()
907 x, y = self.get_transform().transform((x, y))
908 bbox = bbox.translated(x, y)
909 if dpi is not None:
910 self.figure.dpi = dpi_orig
911 return bbox
913 def set_backgroundcolor(self, color):
914 """
915 Set the background color of the text by updating the bbox.
917 Parameters
918 ----------
919 color : color
921 See Also
922 --------
923 .set_bbox : To change the position of the bounding box
924 """
925 if self._bbox_patch is None:
926 self.set_bbox(dict(facecolor=color, edgecolor=color))
927 else:
928 self._bbox_patch.update(dict(facecolor=color))
930 self._update_clip_properties()
931 self.stale = True
933 def set_color(self, color):
934 """
935 Set the foreground color of the text
937 Parameters
938 ----------
939 color : color
940 """
941 # Make sure it is hashable, or get_prop_tup will fail.
942 try:
943 hash(color)
944 except TypeError:
945 color = tuple(color)
946 self._color = color
947 self.stale = True
949 def set_horizontalalignment(self, align):
950 """
951 Set the horizontal alignment to one of
953 Parameters
954 ----------
955 align : {'center', 'right', 'left'}
956 """
957 cbook._check_in_list(['center', 'right', 'left'], align=align)
958 self._horizontalalignment = align
959 self.stale = True
961 def set_multialignment(self, align):
962 """
963 Set the alignment for multiple lines layout. The layout of the
964 bounding box of all the lines is determined by the horizontalalignment
965 and verticalalignment properties, but the multiline text within that
966 box can be
968 Parameters
969 ----------
970 align : {'left', 'right', 'center'}
971 """
972 cbook._check_in_list(['center', 'right', 'left'], align=align)
973 self._multialignment = align
974 self.stale = True
976 def set_linespacing(self, spacing):
977 """
978 Set the line spacing as a multiple of the font size.
979 Default is 1.2.
981 Parameters
982 ----------
983 spacing : float (multiple of font size)
984 """
985 self._linespacing = spacing
986 self.stale = True
988 def set_fontfamily(self, fontname):
989 """
990 Set the font family. May be either a single string, or a list of
991 strings in decreasing priority. Each string may be either a real font
992 name or a generic font class name. If the latter, the specific font
993 names will be looked up in the corresponding rcParams.
995 If a `Text` instance is constructed with ``fontfamily=None``, then the
996 font is set to :rc:`font.family`, and the
997 same is done when `set_fontfamily()` is called on an existing
998 `Text` instance.
1000 Parameters
1001 ----------
1002 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
1003'monospace'}
1005 See Also
1006 --------
1007 .font_manager.FontProperties.set_family
1008 """
1009 self._fontproperties.set_family(fontname)
1010 self.stale = True
1012 def set_fontvariant(self, variant):
1013 """
1014 Set the font variant, either 'normal' or 'small-caps'.
1016 Parameters
1017 ----------
1018 variant : {'normal', 'small-caps'}
1020 See Also
1021 --------
1022 .font_manager.FontProperties.set_variant
1023 """
1024 self._fontproperties.set_variant(variant)
1025 self.stale = True
1027 def set_fontstyle(self, fontstyle):
1028 """
1029 Set the font style.
1031 Parameters
1032 ----------
1033 fontstyle : {'normal', 'italic', 'oblique'}
1035 See Also
1036 --------
1037 .font_manager.FontProperties.set_style
1038 """
1039 self._fontproperties.set_style(fontstyle)
1040 self.stale = True
1042 def set_fontsize(self, fontsize):
1043 """
1044 Set the font size. May be either a size string, relative to
1045 the default font size, or an absolute font size in points.
1047 Parameters
1048 ----------
1049 fontsize : {size in points, 'xx-small', 'x-small', 'small', 'medium', \
1050'large', 'x-large', 'xx-large'}
1052 See Also
1053 --------
1054 .font_manager.FontProperties.set_size
1055 """
1056 self._fontproperties.set_size(fontsize)
1057 self.stale = True
1059 def set_fontweight(self, weight):
1060 """
1061 Set the font weight.
1063 Parameters
1064 ----------
1065 weight : {a numeric value in range 0-1000, 'ultralight', 'light', \
1066'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', \
1067'demi', 'bold', 'heavy', 'extra bold', 'black'}
1069 See Also
1070 --------
1071 .font_manager.FontProperties.set_weight
1072 """
1073 self._fontproperties.set_weight(weight)
1074 self.stale = True
1076 def set_fontstretch(self, stretch):
1077 """
1078 Set the font stretch (horizontal condensation or expansion).
1080 Parameters
1081 ----------
1082 stretch : {a numeric value in range 0-1000, 'ultra-condensed', \
1083'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', \
1084'expanded', 'extra-expanded', 'ultra-expanded'}
1086 See Also
1087 --------
1088 .font_manager.FontProperties.set_stretch
1089 """
1090 self._fontproperties.set_stretch(stretch)
1091 self.stale = True
1093 def set_position(self, xy):
1094 """
1095 Set the (*x*, *y*) position of the text.
1097 Parameters
1098 ----------
1099 xy : (float, float)
1100 """
1101 self.set_x(xy[0])
1102 self.set_y(xy[1])
1104 def set_x(self, x):
1105 """
1106 Set the *x* position of the text.
1108 Parameters
1109 ----------
1110 x : float
1111 """
1112 self._x = x
1113 self.stale = True
1115 def set_y(self, y):
1116 """
1117 Set the *y* position of the text.
1119 Parameters
1120 ----------
1121 y : float
1122 """
1123 self._y = y
1124 self.stale = True
1126 def set_rotation(self, s):
1127 """
1128 Set the rotation of the text.
1130 Parameters
1131 ----------
1132 s : {angle in degrees, 'vertical', 'horizontal'}
1133 """
1134 self._rotation = s
1135 self.stale = True
1137 def set_verticalalignment(self, align):
1138 """
1139 Set the vertical alignment
1141 Parameters
1142 ----------
1143 align : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
1144 """
1145 cbook._check_in_list(
1146 ['top', 'bottom', 'center', 'baseline', 'center_baseline'],
1147 align=align)
1148 self._verticalalignment = align
1149 self.stale = True
1151 def set_text(self, s):
1152 r"""
1153 Set the text string *s*.
1155 It may contain newlines (``\n``) or math in LaTeX syntax.
1157 Parameters
1158 ----------
1159 s : object
1160 Any object gets converted to its `str`, except ``None`` which
1161 becomes ``''``.
1162 """
1163 if s is None:
1164 s = ''
1165 if s != self._text:
1166 self._text = str(s)
1167 self.stale = True
1169 @staticmethod
1170 @cbook.deprecated("3.1")
1171 def is_math_text(s, usetex=None):
1172 """
1173 Returns a cleaned string and a boolean flag.
1174 The flag indicates if the given string *s* contains any mathtext,
1175 determined by counting unescaped dollar signs. If no mathtext
1176 is present, the cleaned string has its dollar signs unescaped.
1177 If usetex is on, the flag always has the value "TeX".
1178 """
1179 # Did we find an even number of non-escaped dollar signs?
1180 # If so, treat is as math text.
1181 if usetex is None:
1182 usetex = rcParams['text.usetex']
1183 if usetex:
1184 if s == ' ':
1185 s = r'\ '
1186 return s, 'TeX'
1188 if cbook.is_math_text(s):
1189 return s, True
1190 else:
1191 return s.replace(r'\$', '$'), False
1193 def _preprocess_math(self, s):
1194 """
1195 Return the string *s* after mathtext preprocessing, and the kind of
1196 mathtext support needed.
1198 - If *self* is configured to use TeX, return *s* unchanged except that
1199 a single space gets escaped, and the flag "TeX".
1200 - Otherwise, if *s* is mathtext (has an even number of unescaped dollar
1201 signs), return *s* and the flag True.
1202 - Otherwise, return *s* with dollar signs unescaped, and the flag
1203 False.
1204 """
1205 if self.get_usetex():
1206 if s == " ":
1207 s = r"\ "
1208 return s, "TeX"
1209 elif cbook.is_math_text(s):
1210 return s, True
1211 else:
1212 return s.replace(r"\$", "$"), False
1214 def set_fontproperties(self, fp):
1215 """
1216 Set the font properties that control the text.
1218 Parameters
1219 ----------
1220 fp : `.font_manager.FontProperties`
1221 """
1222 if isinstance(fp, str):
1223 fp = FontProperties(fp)
1224 self._fontproperties = fp.copy()
1225 self.stale = True
1227 def set_usetex(self, usetex):
1228 """
1229 Parameters
1230 ----------
1231 usetex : bool or None
1232 Whether to render using TeX, ``None`` means to use
1233 :rc:`text.usetex`.
1234 """
1235 if usetex is None:
1236 self._usetex = rcParams['text.usetex']
1237 else:
1238 self._usetex = bool(usetex)
1239 self.stale = True
1241 def get_usetex(self):
1242 """Return whether this `Text` object uses TeX for rendering."""
1243 return self._usetex
1245 def set_fontname(self, fontname):
1246 """
1247 Alias for `set_family`.
1249 One-way alias only: the getter differs.
1251 Parameters
1252 ----------
1253 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \
1254'monospace'}
1256 See Also
1257 --------
1258 .font_manager.FontProperties.set_family
1260 """
1261 return self.set_family(fontname)
1264docstring.interpd.update(Text=artist.kwdoc(Text))
1265docstring.dedent_interpd(Text.__init__)
1268@cbook.deprecated("3.1", alternative="Annotation")
1269class TextWithDash(Text):
1270 """
1271 This is basically a :class:`~matplotlib.text.Text` with a dash
1272 (drawn with a :class:`~matplotlib.lines.Line2D`) before/after
1273 it. It is intended to be a drop-in replacement for
1274 :class:`~matplotlib.text.Text`, and should behave identically to
1275 it when *dashlength* = 0.0.
1277 The dash always comes between the point specified by
1278 :meth:`~matplotlib.text.Text.set_position` and the text. When a
1279 dash exists, the text alignment arguments (*horizontalalignment*,
1280 *verticalalignment*) are ignored.
1282 *dashlength* is the length of the dash in canvas units.
1283 (default = 0.0).
1285 *dashdirection* is one of 0 or 1, where 0 draws the dash after the
1286 text and 1 before. (default = 0).
1288 *dashrotation* specifies the rotation of the dash, and should
1289 generally stay *None*. In this case
1290 :meth:`~matplotlib.text.TextWithDash.get_dashrotation` returns
1291 :meth:`~matplotlib.text.Text.get_rotation`. (i.e., the dash takes
1292 its rotation from the text's rotation). Because the text center is
1293 projected onto the dash, major deviations in the rotation cause
1294 what may be considered visually unappealing results.
1295 (default = *None*)
1297 *dashpad* is a padding length to add (or subtract) space
1298 between the text and the dash, in canvas units.
1299 (default = 3)
1301 *dashpush* "pushes" the dash and text away from the point
1302 specified by :meth:`~matplotlib.text.Text.set_position` by the
1303 amount in canvas units. (default = 0)
1305 .. note::
1307 The alignment of the two objects is based on the bounding box
1308 of the :class:`~matplotlib.text.Text`, as obtained by
1309 :meth:`~matplotlib.artist.Artist.get_window_extent`. This, in
1310 turn, appears to depend on the font metrics as given by the
1311 rendering backend. Hence the quality of the "centering" of the
1312 label text with respect to the dash varies depending on the
1313 backend used.
1315 .. note::
1317 I'm not sure that I got the
1318 :meth:`~matplotlib.text.TextWithDash.get_window_extent` right,
1319 or whether that's sufficient for providing the object bounding
1320 box.
1322 """
1323 __name__ = 'textwithdash'
1325 def __str__(self):
1326 return "TextWithDash(%g, %g, %r)" % (self._x, self._y, self._text)
1328 def __init__(self,
1329 x=0, y=0, text='',
1330 color=None, # defaults to rc params
1331 verticalalignment='center',
1332 horizontalalignment='center',
1333 multialignment=None,
1334 fontproperties=None, # defaults to FontProperties()
1335 rotation=None,
1336 linespacing=None,
1337 dashlength=0.0,
1338 dashdirection=0,
1339 dashrotation=None,
1340 dashpad=3,
1341 dashpush=0,
1342 ):
1344 Text.__init__(self, x=x, y=y, text=text, color=color,
1345 verticalalignment=verticalalignment,
1346 horizontalalignment=horizontalalignment,
1347 multialignment=multialignment,
1348 fontproperties=fontproperties,
1349 rotation=rotation,
1350 linespacing=linespacing,
1351 )
1353 # The position (x, y) values for text and dashline
1354 # are bogus as given in the instantiation; they will
1355 # be set correctly by update_coords() in draw()
1357 self.dashline = Line2D(xdata=(x, x),
1358 ydata=(y, y),
1359 color='k',
1360 linestyle='-')
1362 self._dashx = float(x)
1363 self._dashy = float(y)
1364 self._dashlength = dashlength
1365 self._dashdirection = dashdirection
1366 self._dashrotation = dashrotation
1367 self._dashpad = dashpad
1368 self._dashpush = dashpush
1370 #self.set_bbox(dict(pad=0))
1372 def get_unitless_position(self):
1373 "Return the unitless position of the text as a tuple (*x*, *y*)"
1374 # This will get the position with all unit information stripped away.
1375 # This is here for convenience since it is done in several locations.
1376 x = float(self.convert_xunits(self._dashx))
1377 y = float(self.convert_yunits(self._dashy))
1378 return x, y
1380 def get_position(self):
1381 "Return the position of the text as a tuple (*x*, *y*)"
1382 # This should return the same data (possibly unitized) as was
1383 # specified with set_x and set_y
1384 return self._dashx, self._dashy
1386 def get_prop_tup(self, renderer=None):
1387 """
1388 Return a hashable tuple of properties.
1390 Not intended to be human readable, but useful for backends who
1391 want to cache derived information about text (e.g., layouts) and
1392 need to know if the text has changed.
1393 """
1394 return (*Text.get_prop_tup(self, renderer=renderer),
1395 self._x, self._y, self._dashlength, self._dashdirection,
1396 self._dashrotation, self._dashpad, self._dashpush)
1398 def draw(self, renderer):
1399 """
1400 Draw the :class:`TextWithDash` object to the given *renderer*.
1401 """
1402 self.update_coords(renderer)
1403 Text.draw(self, renderer)
1404 if self.get_dashlength() > 0.0:
1405 self.dashline.draw(renderer)
1406 self.stale = False
1408 def update_coords(self, renderer):
1409 """
1410 Computes the actual *x*, *y* coordinates for text based on the
1411 input *x*, *y* and the *dashlength*. Since the rotation is
1412 with respect to the actual canvas's coordinates we need to map
1413 back and forth.
1414 """
1415 dashx, dashy = self.get_unitless_position()
1416 dashlength = self.get_dashlength()
1417 # Shortcircuit this process if we don't have a dash
1418 if dashlength == 0.0:
1419 self._x, self._y = dashx, dashy
1420 return
1422 dashrotation = self.get_dashrotation()
1423 dashdirection = self.get_dashdirection()
1424 dashpad = self.get_dashpad()
1425 dashpush = self.get_dashpush()
1427 angle = get_rotation(dashrotation)
1428 theta = np.pi * (angle / 180.0 + dashdirection - 1)
1429 cos_theta, sin_theta = np.cos(theta), np.sin(theta)
1431 transform = self.get_transform()
1433 # Compute the dash end points
1434 # The 'c' prefix is for canvas coordinates
1435 cxy = transform.transform((dashx, dashy))
1436 cd = np.array([cos_theta, sin_theta])
1437 c1 = cxy + dashpush * cd
1438 c2 = cxy + (dashpush + dashlength) * cd
1440 inverse = transform.inverted()
1441 (x1, y1), (x2, y2) = inverse.transform([c1, c2])
1442 self.dashline.set_data((x1, x2), (y1, y2))
1444 # We now need to extend this vector out to
1445 # the center of the text area.
1446 # The basic problem here is that we're "rotating"
1447 # two separate objects but want it to appear as
1448 # if they're rotated together.
1449 # This is made non-trivial because of the
1450 # interaction between text rotation and alignment -
1451 # text alignment is based on the bbox after rotation.
1452 # We reset/force both alignments to 'center'
1453 # so we can do something relatively reasonable.
1454 # There's probably a better way to do this by
1455 # embedding all this in the object's transformations,
1456 # but I don't grok the transformation stuff
1457 # well enough yet.
1458 we = Text.get_window_extent(self, renderer=renderer)
1459 w, h = we.width, we.height
1460 # Watch for zeros
1461 if sin_theta == 0.0:
1462 dx = w
1463 dy = 0.0
1464 elif cos_theta == 0.0:
1465 dx = 0.0
1466 dy = h
1467 else:
1468 tan_theta = sin_theta / cos_theta
1469 dx = w
1470 dy = w * tan_theta
1471 if dy > h or dy < -h:
1472 dy = h
1473 dx = h / tan_theta
1474 cwd = np.array([dx, dy]) / 2
1475 cwd *= 1 + dashpad / np.sqrt(np.dot(cwd, cwd))
1476 cw = c2 + (dashdirection * 2 - 1) * cwd
1478 self._x, self._y = inverse.transform(cw)
1480 # Now set the window extent
1481 # I'm not at all sure this is the right way to do this.
1482 we = Text.get_window_extent(self, renderer=renderer)
1483 self._twd_window_extent = we.frozen()
1484 self._twd_window_extent.update_from_data_xy(np.array([c1]), False)
1486 # Finally, make text align center
1487 Text.set_horizontalalignment(self, 'center')
1488 Text.set_verticalalignment(self, 'center')
1490 def get_window_extent(self, renderer=None):
1491 '''
1492 Return a :class:`~matplotlib.transforms.Bbox` object bounding
1493 the text, in display units.
1495 In addition to being used internally, this is useful for
1496 specifying clickable regions in a png file on a web page.
1498 *renderer* defaults to the _renderer attribute of the text
1499 object. This is not assigned until the first execution of
1500 :meth:`draw`, so you must use this kwarg if you want
1501 to call :meth:`get_window_extent` prior to the first
1502 :meth:`draw`. For getting web page regions, it is
1503 simpler to call the method after saving the figure.
1504 '''
1505 self.update_coords(renderer)
1506 if self.get_dashlength() == 0.0:
1507 return Text.get_window_extent(self, renderer=renderer)
1508 else:
1509 return self._twd_window_extent
1511 def get_dashlength(self):
1512 """
1513 Get the length of the dash.
1514 """
1515 return self._dashlength
1517 def set_dashlength(self, dl):
1518 """
1519 Set the length of the dash, in canvas units.
1521 Parameters
1522 ----------
1523 dl : float
1524 """
1525 self._dashlength = dl
1526 self.stale = True
1528 def get_dashdirection(self):
1529 """
1530 Get the direction dash. 1 is before the text and 0 is after.
1531 """
1532 return self._dashdirection
1534 def set_dashdirection(self, dd):
1535 """
1536 Set the direction of the dash following the text. 1 is before the text
1537 and 0 is after. The default is 0, which is what you'd want for the
1538 typical case of ticks below and on the left of the figure.
1540 Parameters
1541 ----------
1542 dd : int (1 is before, 0 is after)
1543 """
1544 self._dashdirection = dd
1545 self.stale = True
1547 def get_dashrotation(self):
1548 """
1549 Get the rotation of the dash in degrees.
1550 """
1551 if self._dashrotation is None:
1552 return self.get_rotation()
1553 else:
1554 return self._dashrotation
1556 def set_dashrotation(self, dr):
1557 """
1558 Set the rotation of the dash, in degrees.
1560 Parameters
1561 ----------
1562 dr : float
1563 """
1564 self._dashrotation = dr
1565 self.stale = True
1567 def get_dashpad(self):
1568 """
1569 Get the extra spacing between the dash and the text, in canvas units.
1570 """
1571 return self._dashpad
1573 def set_dashpad(self, dp):
1574 """
1575 Set the "pad" of the TextWithDash, which is the extra spacing
1576 between the dash and the text, in canvas units.
1578 Parameters
1579 ----------
1580 dp : float
1581 """
1582 self._dashpad = dp
1583 self.stale = True
1585 def get_dashpush(self):
1586 """
1587 Get the extra spacing between the dash and the specified text
1588 position, in canvas units.
1589 """
1590 return self._dashpush
1592 def set_dashpush(self, dp):
1593 """
1594 Set the "push" of the TextWithDash, which is the extra spacing between
1595 the beginning of the dash and the specified position.
1597 Parameters
1598 ----------
1599 dp : float
1600 """
1601 self._dashpush = dp
1602 self.stale = True
1604 def set_position(self, xy):
1605 """
1606 Set the (*x*, *y*) position of the :class:`TextWithDash`.
1608 Parameters
1609 ----------
1610 xy : (float, float)
1611 """
1612 self.set_x(xy[0])
1613 self.set_y(xy[1])
1615 def set_x(self, x):
1616 """
1617 Set the *x* position of the :class:`TextWithDash`.
1619 Parameters
1620 ----------
1621 x : float
1622 """
1623 self._dashx = float(x)
1624 self.stale = True
1626 def set_y(self, y):
1627 """
1628 Set the *y* position of the :class:`TextWithDash`.
1630 Parameters
1631 ----------
1632 y : float
1633 """
1634 self._dashy = float(y)
1635 self.stale = True
1637 def set_transform(self, t):
1638 """
1639 Set the :class:`matplotlib.transforms.Transform` instance used
1640 by this artist.
1642 Parameters
1643 ----------
1644 t : `~matplotlib.transforms.Transform`
1645 """
1646 Text.set_transform(self, t)
1647 self.dashline.set_transform(t)
1648 self.stale = True
1650 def get_figure(self):
1651 """Return the figure instance the artist belongs to."""
1652 return self.figure
1654 def set_figure(self, fig):
1655 """
1656 Set the figure instance the artist belongs to.
1658 Parameters
1659 ----------
1660 fig : `~matplotlib.figure.Figure`
1661 """
1662 Text.set_figure(self, fig)
1663 self.dashline.set_figure(fig)
1665docstring.interpd.update(TextWithDash=artist.kwdoc(TextWithDash))
1668class OffsetFrom:
1669 'Callable helper class for working with `Annotation`'
1670 def __init__(self, artist, ref_coord, unit="points"):
1671 '''
1672 Parameters
1673 ----------
1674 artist : `.Artist`, `.BboxBase`, or `.Transform`
1675 The object to compute the offset from.
1677 ref_coord : length 2 sequence
1678 If *artist* is an `.Artist` or `.BboxBase`, this values is
1679 the location to of the offset origin in fractions of the
1680 *artist* bounding box.
1682 If *artist* is a transform, the offset origin is the
1683 transform applied to this value.
1685 unit : {'points, 'pixels'}
1686 The screen units to use (pixels or points) for the offset
1687 input.
1689 '''
1690 self._artist = artist
1691 self._ref_coord = ref_coord
1692 self.set_unit(unit)
1694 def set_unit(self, unit):
1695 '''
1696 The unit for input to the transform used by ``__call__``
1698 Parameters
1699 ----------
1700 unit : {'points', 'pixels'}
1701 '''
1702 cbook._check_in_list(["points", "pixels"], unit=unit)
1703 self._unit = unit
1705 def get_unit(self):
1706 'The unit for input to the transform used by ``__call__``'
1707 return self._unit
1709 def _get_scale(self, renderer):
1710 unit = self.get_unit()
1711 if unit == "pixels":
1712 return 1.
1713 else:
1714 return renderer.points_to_pixels(1.)
1716 def __call__(self, renderer):
1717 '''
1718 Return the offset transform.
1720 Parameters
1721 ----------
1722 renderer : `RendererBase`
1723 The renderer to use to compute the offset
1725 Returns
1726 -------
1727 transform : `Transform`
1728 Maps (x, y) in pixel or point units to screen units
1729 relative to the given artist.
1730 '''
1731 if isinstance(self._artist, Artist):
1732 bbox = self._artist.get_window_extent(renderer)
1733 l, b, w, h = bbox.bounds
1734 xf, yf = self._ref_coord
1735 x, y = l + w * xf, b + h * yf
1736 elif isinstance(self._artist, BboxBase):
1737 l, b, w, h = self._artist.bounds
1738 xf, yf = self._ref_coord
1739 x, y = l + w * xf, b + h * yf
1740 elif isinstance(self._artist, Transform):
1741 x, y = self._artist.transform(self._ref_coord)
1742 else:
1743 raise RuntimeError("unknown type")
1745 sc = self._get_scale(renderer)
1746 tr = Affine2D().scale(sc).translate(x, y)
1748 return tr
1751class _AnnotationBase:
1752 def __init__(self,
1753 xy,
1754 xycoords='data',
1755 annotation_clip=None):
1757 self.xy = xy
1758 self.xycoords = xycoords
1759 self.set_annotation_clip(annotation_clip)
1761 self._draggable = None
1763 def _get_xy(self, renderer, x, y, s):
1764 if isinstance(s, tuple):
1765 s1, s2 = s
1766 else:
1767 s1, s2 = s, s
1768 if s1 == 'data':
1769 x = float(self.convert_xunits(x))
1770 if s2 == 'data':
1771 y = float(self.convert_yunits(y))
1772 return self._get_xy_transform(renderer, s).transform((x, y))
1774 def _get_xy_transform(self, renderer, s):
1776 if isinstance(s, tuple):
1777 s1, s2 = s
1778 from matplotlib.transforms import blended_transform_factory
1779 tr1 = self._get_xy_transform(renderer, s1)
1780 tr2 = self._get_xy_transform(renderer, s2)
1781 tr = blended_transform_factory(tr1, tr2)
1782 return tr
1783 elif callable(s):
1784 tr = s(renderer)
1785 if isinstance(tr, BboxBase):
1786 return BboxTransformTo(tr)
1787 elif isinstance(tr, Transform):
1788 return tr
1789 else:
1790 raise RuntimeError("unknown return type ...")
1791 elif isinstance(s, Artist):
1792 bbox = s.get_window_extent(renderer)
1793 return BboxTransformTo(bbox)
1794 elif isinstance(s, BboxBase):
1795 return BboxTransformTo(s)
1796 elif isinstance(s, Transform):
1797 return s
1798 elif not isinstance(s, str):
1799 raise RuntimeError("unknown coordinate type : %s" % s)
1801 if s == 'data':
1802 return self.axes.transData
1803 elif s == 'polar':
1804 from matplotlib.projections import PolarAxes
1805 tr = PolarAxes.PolarTransform()
1806 trans = tr + self.axes.transData
1807 return trans
1809 s_ = s.split()
1810 if len(s_) != 2:
1811 raise ValueError("%s is not a recognized coordinate" % s)
1813 bbox0, xy0 = None, None
1815 bbox_name, unit = s_
1816 # if unit is offset-like
1817 if bbox_name == "figure":
1818 bbox0 = self.figure.bbox
1819 elif bbox_name == "axes":
1820 bbox0 = self.axes.bbox
1821 # elif bbox_name == "bbox":
1822 # if bbox is None:
1823 # raise RuntimeError("bbox is specified as a coordinate but "
1824 # "never set")
1825 # bbox0 = self._get_bbox(renderer, bbox)
1827 if bbox0 is not None:
1828 xy0 = bbox0.bounds[:2]
1829 elif bbox_name == "offset":
1830 xy0 = self._get_ref_xy(renderer)
1832 if xy0 is not None:
1833 # reference x, y in display coordinate
1834 ref_x, ref_y = xy0
1835 from matplotlib.transforms import Affine2D
1836 if unit == "points":
1837 # dots per points
1838 dpp = self.figure.get_dpi() / 72.
1839 tr = Affine2D().scale(dpp)
1840 elif unit == "pixels":
1841 tr = Affine2D()
1842 elif unit == "fontsize":
1843 fontsize = self.get_size()
1844 dpp = fontsize * self.figure.get_dpi() / 72.
1845 tr = Affine2D().scale(dpp)
1846 elif unit == "fraction":
1847 w, h = bbox0.bounds[2:]
1848 tr = Affine2D().scale(w, h)
1849 else:
1850 raise ValueError("%s is not a recognized coordinate" % s)
1852 return tr.translate(ref_x, ref_y)
1854 else:
1855 raise ValueError("%s is not a recognized coordinate" % s)
1857 def _get_ref_xy(self, renderer):
1858 """
1859 return x, y (in display coordinate) that is to be used for a reference
1860 of any offset coordinate
1861 """
1862 def is_offset(s):
1863 return isinstance(s, str) and s.split()[0] == "offset"
1865 if isinstance(self.xycoords, tuple):
1866 if any(map(is_offset, self.xycoords)):
1867 raise ValueError("xycoords should not be an offset coordinate")
1868 elif is_offset(self.xycoords):
1869 raise ValueError("xycoords should not be an offset coordinate")
1870 x, y = self.xy
1871 return self._get_xy(renderer, x, y, self.xycoords)
1873 # def _get_bbox(self, renderer):
1874 # if hasattr(bbox, "bounds"):
1875 # return bbox
1876 # elif hasattr(bbox, "get_window_extent"):
1877 # bbox = bbox.get_window_extent()
1878 # return bbox
1879 # else:
1880 # raise ValueError("A bbox instance is expected but got %s" %
1881 # str(bbox))
1883 def set_annotation_clip(self, b):
1884 """
1885 set *annotation_clip* attribute.
1887 * True: the annotation will only be drawn when self.xy is inside
1888 the axes.
1889 * False: the annotation will always be drawn regardless of its
1890 position.
1891 * None: the self.xy will be checked only if *xycoords* is "data"
1892 """
1893 self._annotation_clip = b
1895 def get_annotation_clip(self):
1896 """
1897 Return *annotation_clip* attribute.
1898 See :meth:`set_annotation_clip` for the meaning of return values.
1899 """
1900 return self._annotation_clip
1902 def _get_position_xy(self, renderer):
1903 "Return the pixel position of the annotated point."
1904 x, y = self.xy
1905 return self._get_xy(renderer, x, y, self.xycoords)
1907 def _check_xy(self, renderer, xy_pixel):
1908 """
1909 given the xy pixel coordinate, check if the annotation need to
1910 be drawn.
1911 """
1913 b = self.get_annotation_clip()
1915 if b or (b is None and self.xycoords == "data"):
1916 # check if self.xy is inside the axes.
1917 if not self.axes.contains_point(xy_pixel):
1918 return False
1920 return True
1922 def draggable(self, state=None, use_blit=False):
1923 """
1924 Set the draggable state -- if state is
1926 * None : toggle the current state
1928 * True : turn draggable on
1930 * False : turn draggable off
1932 If draggable is on, you can drag the annotation on the canvas with
1933 the mouse. The DraggableAnnotation helper instance is returned if
1934 draggable is on.
1935 """
1936 from matplotlib.offsetbox import DraggableAnnotation
1937 is_draggable = self._draggable is not None
1939 # if state is None we'll toggle
1940 if state is None:
1941 state = not is_draggable
1943 if state:
1944 if self._draggable is None:
1945 self._draggable = DraggableAnnotation(self, use_blit)
1946 else:
1947 if self._draggable is not None:
1948 self._draggable.disconnect()
1949 self._draggable = None
1951 return self._draggable
1954class Annotation(Text, _AnnotationBase):
1955 """
1956 An `.Annotation` is a `.Text` that can refer to a specific position *xy*.
1957 Optionally an arrow pointing from the text to *xy* can be drawn.
1959 Attributes
1960 ----------
1961 xy
1962 The annotated position.
1963 xycoords
1964 The coordinate system for *xy*.
1965 arrow_patch
1966 A `.FancyArrowPatch` to point from *xytext* to *xy*.
1967 """
1969 def __str__(self):
1970 return "Annotation(%g, %g, %r)" % (self.xy[0], self.xy[1], self._text)
1972 @cbook._rename_parameter("3.1", "s", "text")
1973 def __init__(self, text, xy,
1974 xytext=None,
1975 xycoords='data',
1976 textcoords=None,
1977 arrowprops=None,
1978 annotation_clip=None,
1979 **kwargs):
1980 """
1981 Annotate the point *xy* with text *text*.
1983 In the simplest form, the text is placed at *xy*.
1985 Optionally, the text can be displayed in another position *xytext*.
1986 An arrow pointing from the text to the annotated point *xy* can then
1987 be added by defining *arrowprops*.
1989 Parameters
1990 ----------
1991 text : str
1992 The text of the annotation. *s* is a deprecated synonym for this
1993 parameter.
1995 xy : (float, float)
1996 The point *(x, y)* to annotate.
1998 xytext : (float, float), optional
1999 The position *(x, y)* to place the text at.
2000 If *None*, defaults to *xy*.
2002 xycoords : str, `.Artist`, `.Transform`, callable or tuple, optional
2004 The coordinate system that *xy* is given in. The following types
2005 of values are supported:
2007 - One of the following strings:
2009 ================= =============================================
2010 Value Description
2011 ================= =============================================
2012 'figure points' Points from the lower left of the figure
2013 'figure pixels' Pixels from the lower left of the figure
2014 'figure fraction' Fraction of figure from lower left
2015 'axes points' Points from lower left corner of axes
2016 'axes pixels' Pixels from lower left corner of axes
2017 'axes fraction' Fraction of axes from lower left
2018 'data' Use the coordinate system of the object being
2019 annotated (default)
2020 'polar' *(theta, r)* if not native 'data' coordinates
2021 ================= =============================================
2023 - An `.Artist`: *xy* is interpreted as a fraction of the artists
2024 `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower
2025 left corner of the bounding box and *(0.5, 1)* would be the
2026 center top of the bounding box.
2028 - A `.Transform` to transform *xy* to screen coordinates.
2030 - A function with one of the following signatures::
2032 def transform(renderer) -> Bbox
2033 def transform(renderer) -> Transform
2035 where *renderer* is a `.RendererBase` subclass.
2037 The result of the function is interpreted like the `.Artist` and
2038 `.Transform` cases above.
2040 - A tuple *(xcoords, ycoords)* specifying separate coordinate
2041 systems for *x* and *y*. *xcoords* and *ycoords* must each be
2042 of one of the above described types.
2044 See :ref:`plotting-guide-annotation` for more details.
2046 Defaults to 'data'.
2048 textcoords : str, `.Artist`, `.Transform`, callable or tuple, optional
2049 The coordinate system that *xytext* is given in.
2051 All *xycoords* values are valid as well as the following
2052 strings:
2054 ================= =========================================
2055 Value Description
2056 ================= =========================================
2057 'offset points' Offset (in points) from the *xy* value
2058 'offset pixels' Offset (in pixels) from the *xy* value
2059 ================= =========================================
2061 Defaults to the value of *xycoords*, i.e. use the same coordinate
2062 system for annotation point and text position.
2064 arrowprops : dict, optional
2065 The properties used to draw a
2066 `~matplotlib.patches.FancyArrowPatch` arrow between the
2067 positions *xy* and *xytext*.
2069 If *arrowprops* does not contain the key 'arrowstyle' the
2070 allowed keys are:
2072 ========== ======================================================
2073 Key Description
2074 ========== ======================================================
2075 width The width of the arrow in points
2076 headwidth The width of the base of the arrow head in points
2077 headlength The length of the arrow head in points
2078 shrink Fraction of total length to shrink from both ends
2079 ? Any key to :class:`matplotlib.patches.FancyArrowPatch`
2080 ========== ======================================================
2082 If *arrowprops* contains the key 'arrowstyle' the
2083 above keys are forbidden. The allowed values of
2084 ``'arrowstyle'`` are:
2086 ============ =============================================
2087 Name Attrs
2088 ============ =============================================
2089 ``'-'`` None
2090 ``'->'`` head_length=0.4,head_width=0.2
2091 ``'-['`` widthB=1.0,lengthB=0.2,angleB=None
2092 ``'|-|'`` widthA=1.0,widthB=1.0
2093 ``'-|>'`` head_length=0.4,head_width=0.2
2094 ``'<-'`` head_length=0.4,head_width=0.2
2095 ``'<->'`` head_length=0.4,head_width=0.2
2096 ``'<|-'`` head_length=0.4,head_width=0.2
2097 ``'<|-|>'`` head_length=0.4,head_width=0.2
2098 ``'fancy'`` head_length=0.4,head_width=0.4,tail_width=0.4
2099 ``'simple'`` head_length=0.5,head_width=0.5,tail_width=0.2
2100 ``'wedge'`` tail_width=0.3,shrink_factor=0.5
2101 ============ =============================================
2103 Valid keys for `~matplotlib.patches.FancyArrowPatch` are:
2105 =============== ==================================================
2106 Key Description
2107 =============== ==================================================
2108 arrowstyle the arrow style
2109 connectionstyle the connection style
2110 relpos default is (0.5, 0.5)
2111 patchA default is bounding box of the text
2112 patchB default is None
2113 shrinkA default is 2 points
2114 shrinkB default is 2 points
2115 mutation_scale default is text size (in points)
2116 mutation_aspect default is 1.
2117 ? any key for :class:`matplotlib.patches.PathPatch`
2118 =============== ==================================================
2120 Defaults to None, i.e. no arrow is drawn.
2122 annotation_clip : bool or None, optional
2123 Whether to draw the annotation when the annotation point *xy* is
2124 outside the axes area.
2126 - If *True*, the annotation will only be drawn when *xy* is
2127 within the axes.
2128 - If *False*, the annotation will always be drawn.
2129 - If *None*, the annotation will only be drawn when *xy* is
2130 within the axes and *xycoords* is 'data'.
2132 Defaults to *None*.
2134 **kwargs
2135 Additional kwargs are passed to `~matplotlib.text.Text`.
2137 Returns
2138 -------
2139 annotation : `.Annotation`
2141 See Also
2142 --------
2143 :ref:`plotting-guide-annotation`.
2145 """
2146 _AnnotationBase.__init__(self,
2147 xy,
2148 xycoords=xycoords,
2149 annotation_clip=annotation_clip)
2150 # warn about wonky input data
2151 if (xytext is None and
2152 textcoords is not None and
2153 textcoords != xycoords):
2154 cbook._warn_external("You have used the `textcoords` kwarg, but "
2155 "not the `xytext` kwarg. This can lead to "
2156 "surprising results.")
2158 # clean up textcoords and assign default
2159 if textcoords is None:
2160 textcoords = self.xycoords
2161 self._textcoords = textcoords
2163 # cleanup xytext defaults
2164 if xytext is None:
2165 xytext = self.xy
2166 x, y = xytext
2168 Text.__init__(self, x, y, text, **kwargs)
2170 self.arrowprops = arrowprops
2172 if arrowprops is not None:
2173 if "arrowstyle" in arrowprops:
2174 arrowprops = self.arrowprops.copy()
2175 self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
2176 else:
2177 # modified YAArrow API to be used with FancyArrowPatch
2178 shapekeys = ('width', 'headwidth', 'headlength',
2179 'shrink', 'frac')
2180 arrowprops = dict()
2181 for key, val in self.arrowprops.items():
2182 if key not in shapekeys:
2183 arrowprops[key] = val # basic Patch properties
2184 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
2185 **arrowprops)
2186 else:
2187 self.arrow_patch = None
2189 def contains(self, event):
2190 inside, info = self._default_contains(event)
2191 if inside is not None:
2192 return inside, info
2193 contains, tinfo = Text.contains(self, event)
2194 if self.arrow_patch is not None:
2195 in_patch, _ = self.arrow_patch.contains(event)
2196 contains = contains or in_patch
2197 return contains, tinfo
2199 @property
2200 def xyann(self):
2201 """
2202 The the text position.
2204 See also *xytext* in `.Annotation`.
2205 """
2206 return self.get_position()
2208 @xyann.setter
2209 def xyann(self, xytext):
2210 self.set_position(xytext)
2212 @property
2213 def anncoords(self):
2214 """The coordinate system to use for `.Annotation.xyann`."""
2215 return self._textcoords
2217 @anncoords.setter
2218 def anncoords(self, coords):
2219 self._textcoords = coords
2221 get_anncoords = anncoords.fget
2222 get_anncoords.__doc__ = """
2223 Return the coordinate system to use for `.Annotation.xyann`.
2225 See also *xycoords* in `.Annotation`.
2226 """
2228 set_anncoords = anncoords.fset
2229 set_anncoords.__doc__ = """
2230 Set the coordinate system to use for `.Annotation.xyann`.
2232 See also *xycoords* in `.Annotation`.
2233 """
2235 def set_figure(self, fig):
2236 if self.arrow_patch is not None:
2237 self.arrow_patch.set_figure(fig)
2238 Artist.set_figure(self, fig)
2240 def update_positions(self, renderer):
2241 """Update the pixel positions of the annotated point and the text."""
2242 xy_pixel = self._get_position_xy(renderer)
2243 self._update_position_xytext(renderer, xy_pixel)
2245 def _update_position_xytext(self, renderer, xy_pixel):
2246 """
2247 Update the pixel positions of the annotation text and the arrow patch.
2248 """
2249 # generate transformation,
2250 self.set_transform(self._get_xy_transform(renderer, self.anncoords))
2252 ox0, oy0 = self._get_xy_display()
2253 ox1, oy1 = xy_pixel
2255 if self.arrowprops is not None:
2256 x0, y0 = xy_pixel
2257 l, b, w, h = Text.get_window_extent(self, renderer).bounds
2258 r = l + w
2259 t = b + h
2260 xc = 0.5 * (l + r)
2261 yc = 0.5 * (b + t)
2263 d = self.arrowprops.copy()
2264 ms = d.pop("mutation_scale", self.get_size())
2265 self.arrow_patch.set_mutation_scale(ms)
2267 if "arrowstyle" not in d:
2268 # Approximately simulate the YAArrow.
2269 # Pop its kwargs:
2270 shrink = d.pop('shrink', 0.0)
2271 width = d.pop('width', 4)
2272 headwidth = d.pop('headwidth', 12)
2273 # Ignore frac--it is useless.
2274 frac = d.pop('frac', None)
2275 if frac is not None:
2276 cbook._warn_external(
2277 "'frac' option in 'arrowprops' is no longer supported;"
2278 " use 'headlength' to set the head length in points.")
2279 headlength = d.pop('headlength', 12)
2281 # NB: ms is in pts
2282 stylekw = dict(head_length=headlength / ms,
2283 head_width=headwidth / ms,
2284 tail_width=width / ms)
2286 self.arrow_patch.set_arrowstyle('simple', **stylekw)
2288 # using YAArrow style:
2289 # pick the (x, y) corner of the text bbox closest to point
2290 # annotated
2291 xpos = ((l, 0), (xc, 0.5), (r, 1))
2292 ypos = ((b, 0), (yc, 0.5), (t, 1))
2294 _, (x, relposx) = min((abs(val[0] - x0), val) for val in xpos)
2295 _, (y, relposy) = min((abs(val[0] - y0), val) for val in ypos)
2297 self._arrow_relpos = (relposx, relposy)
2299 r = np.hypot((y - y0), (x - x0))
2300 shrink_pts = shrink * r / renderer.points_to_pixels(1)
2301 self.arrow_patch.shrinkA = shrink_pts
2302 self.arrow_patch.shrinkB = shrink_pts
2304 # adjust the starting point of the arrow relative to
2305 # the textbox.
2306 # TODO : Rotation needs to be accounted.
2307 relpos = self._arrow_relpos
2308 bbox = Text.get_window_extent(self, renderer)
2309 ox0 = bbox.x0 + bbox.width * relpos[0]
2310 oy0 = bbox.y0 + bbox.height * relpos[1]
2312 # The arrow will be drawn from (ox0, oy0) to (ox1,
2313 # oy1). It will be first clipped by patchA and patchB.
2314 # Then it will be shrunk by shrinkA and shrinkB
2315 # (in points). If patch A is not set, self.bbox_patch
2316 # is used.
2318 self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
2320 if "patchA" in d:
2321 self.arrow_patch.set_patchA(d.pop("patchA"))
2322 else:
2323 if self._bbox_patch:
2324 self.arrow_patch.set_patchA(self._bbox_patch)
2325 else:
2326 pad = renderer.points_to_pixels(4)
2327 if self.get_text() == "":
2328 self.arrow_patch.set_patchA(None)
2329 return
2331 bbox = Text.get_window_extent(self, renderer)
2332 l, b, w, h = bbox.bounds
2333 l -= pad / 2.
2334 b -= pad / 2.
2335 w += pad
2336 h += pad
2337 r = Rectangle(xy=(l, b),
2338 width=w,
2339 height=h,
2340 )
2341 r.set_transform(IdentityTransform())
2342 r.set_clip_on(False)
2344 self.arrow_patch.set_patchA(r)
2346 @artist.allow_rasterization
2347 def draw(self, renderer):
2348 """
2349 Draw the :class:`Annotation` object to the given *renderer*.
2350 """
2352 if renderer is not None:
2353 self._renderer = renderer
2354 if not self.get_visible():
2355 return
2357 xy_pixel = self._get_position_xy(renderer)
2358 if not self._check_xy(renderer, xy_pixel):
2359 return
2361 self._update_position_xytext(renderer, xy_pixel)
2362 self.update_bbox_position_size(renderer)
2364 if self.arrow_patch is not None: # FancyArrowPatch
2365 if self.arrow_patch.figure is None and self.figure is not None:
2366 self.arrow_patch.figure = self.figure
2367 self.arrow_patch.draw(renderer)
2369 # Draw text, including FancyBboxPatch, after FancyArrowPatch.
2370 # Otherwise, a wedge arrowstyle can land partly on top of the Bbox.
2371 Text.draw(self, renderer)
2373 def get_window_extent(self, renderer=None):
2374 """
2375 Return the `.Bbox` bounding the text and arrow, in display units.
2377 Parameters
2378 ----------
2379 renderer : Renderer, optional
2380 A renderer is needed to compute the bounding box. If the artist
2381 has already been drawn, the renderer is cached; thus, it is only
2382 necessary to pass this argument when calling `get_window_extent`
2383 before the first `draw`. In practice, it is usually easier to
2384 trigger a draw first (e.g. by saving the figure).
2385 """
2386 # This block is the same as in Text.get_window_extent, but we need to
2387 # set the renderer before calling update_positions().
2388 if not self.get_visible():
2389 return Bbox.unit()
2390 if renderer is not None:
2391 self._renderer = renderer
2392 if self._renderer is None:
2393 self._renderer = self.figure._cachedRenderer
2394 if self._renderer is None:
2395 raise RuntimeError('Cannot get window extent w/o renderer')
2397 self.update_positions(self._renderer)
2399 text_bbox = Text.get_window_extent(self)
2400 bboxes = [text_bbox]
2402 if self.arrow_patch is not None:
2403 bboxes.append(self.arrow_patch.get_window_extent())
2405 return Bbox.union(bboxes)
2408docstring.interpd.update(Annotation=Annotation.__init__.__doc__)