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