Coverage for /usr/lib/python3/dist-packages/matplotlib/text.py: 48%

800 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2Classes for including text in a figure. 

3""" 

4 

5import functools 

6import logging 

7import math 

8from numbers import Real 

9import weakref 

10 

11import numpy as np 

12 

13import matplotlib as mpl 

14from . import _api, artist, cbook, _docstring 

15from .artist import Artist 

16from .font_manager import FontProperties 

17from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle 

18from .textpath import TextPath # noqa # Unused, but imported by others. 

19from .transforms import ( 

20 Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform) 

21 

22 

23_log = logging.getLogger(__name__) 

24 

25 

26@_api.deprecated("3.6") 

27def get_rotation(rotation): 

28 """ 

29 Return *rotation* normalized to an angle between 0 and 360 degrees. 

30 

31 Parameters 

32 ---------- 

33 rotation : float or {None, 'horizontal', 'vertical'} 

34 Rotation angle in degrees. *None* and 'horizontal' equal 0, 

35 'vertical' equals 90. 

36 

37 Returns 

38 ------- 

39 float 

40 """ 

41 try: 

42 return float(rotation) % 360 

43 except (ValueError, TypeError) as err: 

44 if cbook._str_equal(rotation, 'horizontal') or rotation is None: 

45 return 0. 

46 elif cbook._str_equal(rotation, 'vertical'): 

47 return 90. 

48 else: 

49 raise ValueError("rotation is {!r}; expected either 'horizontal', " 

50 "'vertical', numeric value, or None" 

51 .format(rotation)) from err 

52 

53 

54def _get_textbox(text, renderer): 

55 """ 

56 Calculate the bounding box of the text. 

57 

58 The bbox position takes text rotation into account, but the width and 

59 height are those of the unrotated box (unlike `.Text.get_window_extent`). 

60 """ 

61 # TODO : This function may move into the Text class as a method. As a 

62 # matter of fact, the information from the _get_textbox function 

63 # should be available during the Text._get_layout() call, which is 

64 # called within the _get_textbox. So, it would better to move this 

65 # function as a method with some refactoring of _get_layout method. 

66 

67 projected_xs = [] 

68 projected_ys = [] 

69 

70 theta = np.deg2rad(text.get_rotation()) 

71 tr = Affine2D().rotate(-theta) 

72 

73 _, parts, d = text._get_layout(renderer) 

74 

75 for t, wh, x, y in parts: 

76 w, h = wh 

77 

78 xt1, yt1 = tr.transform((x, y)) 

79 yt1 -= d 

80 xt2, yt2 = xt1 + w, yt1 + h 

81 

82 projected_xs.extend([xt1, xt2]) 

83 projected_ys.extend([yt1, yt2]) 

84 

85 xt_box, yt_box = min(projected_xs), min(projected_ys) 

86 w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box 

87 

88 x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box)) 

89 

90 return x_box, y_box, w_box, h_box 

91 

92 

93def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): 

94 """Call ``renderer.get_text_width_height_descent``, caching the results.""" 

95 # Cached based on a copy of fontprop so that later in-place mutations of 

96 # the passed-in argument do not mess up the cache. 

97 return _get_text_metrics_with_cache_impl( 

98 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi) 

99 

100 

101@functools.lru_cache(4096) 

102def _get_text_metrics_with_cache_impl( 

103 renderer_ref, text, fontprop, ismath, dpi): 

104 # dpi is unused, but participates in cache invalidation (via the renderer). 

105 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath) 

106 

107 

108@_docstring.interpd 

109@_api.define_aliases({ 

110 "color": ["c"], 

111 "fontfamily": ["family"], 

112 "fontproperties": ["font", "font_properties"], 

113 "horizontalalignment": ["ha"], 

114 "multialignment": ["ma"], 

115 "fontname": ["name"], 

116 "fontsize": ["size"], 

117 "fontstretch": ["stretch"], 

118 "fontstyle": ["style"], 

119 "fontvariant": ["variant"], 

120 "verticalalignment": ["va"], 

121 "fontweight": ["weight"], 

122}) 

123class Text(Artist): 

124 """Handle storing and drawing of text in window or data coordinates.""" 

125 

126 zorder = 3 

127 

128 def __repr__(self): 

129 return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text)) 

130 

131 @_api.make_keyword_only("3.6", name="color") 

132 def __init__(self, 

133 x=0, y=0, text='', 

134 color=None, # defaults to rc params 

135 verticalalignment='baseline', 

136 horizontalalignment='left', 

137 multialignment=None, 

138 fontproperties=None, # defaults to FontProperties() 

139 rotation=None, 

140 linespacing=None, 

141 rotation_mode=None, 

142 usetex=None, # defaults to rcParams['text.usetex'] 

143 wrap=False, 

144 transform_rotates_text=False, 

145 *, 

146 parse_math=None, # defaults to rcParams['text.parse_math'] 

147 **kwargs 

148 ): 

149 """ 

150 Create a `.Text` instance at *x*, *y* with string *text*. 

151 

152 The text is aligned relative to the anchor point (*x*, *y*) according 

153 to ``horizontalalignment`` (default: 'left') and ``verticalalignment`` 

154 (default: 'bottom'). See also 

155 :doc:`/gallery/text_labels_and_annotations/text_alignment`. 

156 

157 While Text accepts the 'label' keyword argument, by default it is not 

158 added to the handles of a legend. 

159 

160 Valid keyword arguments are: 

161 

162 %(Text:kwdoc)s 

163 """ 

164 super().__init__() 

165 self._x, self._y = x, y 

166 self._text = '' 

167 self.set_text(text) 

168 self.set_color( 

169 color if color is not None else mpl.rcParams["text.color"]) 

170 self.set_fontproperties(fontproperties) 

171 self.set_usetex(usetex) 

172 self.set_parse_math(parse_math if parse_math is not None else 

173 mpl.rcParams['text.parse_math']) 

174 self.set_wrap(wrap) 

175 self.set_verticalalignment(verticalalignment) 

176 self.set_horizontalalignment(horizontalalignment) 

177 self._multialignment = multialignment 

178 self.set_rotation(rotation) 

179 self._transform_rotates_text = transform_rotates_text 

180 self._bbox_patch = None # a FancyBboxPatch instance 

181 self._renderer = None 

182 if linespacing is None: 

183 linespacing = 1.2 # Maybe use rcParam later. 

184 self.set_linespacing(linespacing) 

185 self.set_rotation_mode(rotation_mode) 

186 self.update(kwargs) 

187 

188 def update(self, kwargs): 

189 # docstring inherited 

190 kwargs = cbook.normalize_kwargs(kwargs, Text) 

191 sentinel = object() # bbox can be None, so use another sentinel. 

192 # Update fontproperties first, as it has lowest priority. 

193 fontproperties = kwargs.pop("fontproperties", sentinel) 

194 if fontproperties is not sentinel: 

195 self.set_fontproperties(fontproperties) 

196 # Update bbox last, as it depends on font properties. 

197 bbox = kwargs.pop("bbox", sentinel) 

198 super().update(kwargs) 

199 if bbox is not sentinel: 

200 self.set_bbox(bbox) 

201 

202 def __getstate__(self): 

203 d = super().__getstate__() 

204 # remove the cached _renderer (if it exists) 

205 d['_renderer'] = None 

206 return d 

207 

208 def contains(self, mouseevent): 

209 """ 

210 Return whether the mouse event occurred inside the axis-aligned 

211 bounding-box of the text. 

212 """ 

213 inside, info = self._default_contains(mouseevent) 

214 if inside is not None: 

215 return inside, info 

216 

217 if not self.get_visible() or self._renderer is None: 

218 return False, {} 

219 

220 # Explicitly use Text.get_window_extent(self) and not 

221 # self.get_window_extent() so that Annotation.contains does not 

222 # accidentally cover the entire annotation bounding box. 

223 bbox = Text.get_window_extent(self) 

224 inside = (bbox.x0 <= mouseevent.x <= bbox.x1 

225 and bbox.y0 <= mouseevent.y <= bbox.y1) 

226 

227 cattr = {} 

228 # if the text has a surrounding patch, also check containment for it, 

229 # and merge the results with the results for the text. 

230 if self._bbox_patch: 

231 patch_inside, patch_cattr = self._bbox_patch.contains(mouseevent) 

232 inside = inside or patch_inside 

233 cattr["bbox_patch"] = patch_cattr 

234 

235 return inside, cattr 

236 

237 def _get_xy_display(self): 

238 """ 

239 Get the (possibly unit converted) transformed x, y in display coords. 

240 """ 

241 x, y = self.get_unitless_position() 

242 return self.get_transform().transform((x, y)) 

243 

244 def _get_multialignment(self): 

245 if self._multialignment is not None: 

246 return self._multialignment 

247 else: 

248 return self._horizontalalignment 

249 

250 def get_rotation(self): 

251 """Return the text angle in degrees between 0 and 360.""" 

252 if self.get_transform_rotates_text(): 

253 return self.get_transform().transform_angles( 

254 [self._rotation], [self.get_unitless_position()]).item(0) 

255 else: 

256 return self._rotation 

257 

258 def get_transform_rotates_text(self): 

259 """ 

260 Return whether rotations of the transform affect the text direction. 

261 """ 

262 return self._transform_rotates_text 

263 

264 def set_rotation_mode(self, m): 

265 """ 

266 Set text rotation mode. 

267 

268 Parameters 

269 ---------- 

270 m : {None, 'default', 'anchor'} 

271 If ``None`` or ``"default"``, the text will be first rotated, then 

272 aligned according to their horizontal and vertical alignments. If 

273 ``"anchor"``, then alignment occurs before rotation. 

274 """ 

275 _api.check_in_list(["anchor", "default", None], rotation_mode=m) 

276 self._rotation_mode = m 

277 self.stale = True 

278 

279 def get_rotation_mode(self): 

280 """Return the text rotation mode.""" 

281 return self._rotation_mode 

282 

283 def update_from(self, other): 

284 # docstring inherited 

285 super().update_from(other) 

286 self._color = other._color 

287 self._multialignment = other._multialignment 

288 self._verticalalignment = other._verticalalignment 

289 self._horizontalalignment = other._horizontalalignment 

290 self._fontproperties = other._fontproperties.copy() 

291 self._usetex = other._usetex 

292 self._rotation = other._rotation 

293 self._transform_rotates_text = other._transform_rotates_text 

294 self._picker = other._picker 

295 self._linespacing = other._linespacing 

296 self.stale = True 

297 

298 def _get_layout(self, renderer): 

299 """ 

300 Return the extent (bbox) of the text together with 

301 multiple-alignment information. Note that it returns an extent 

302 of a rotated text when necessary. 

303 """ 

304 thisx, thisy = 0.0, 0.0 

305 lines = self.get_text().split("\n") # Ensures lines is not empty. 

306 

307 ws = [] 

308 hs = [] 

309 xs = [] 

310 ys = [] 

311 

312 # Full vertical extent of font, including ascenders and descenders: 

313 _, lp_h, lp_d = _get_text_metrics_with_cache( 

314 renderer, "lp", self._fontproperties, 

315 ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi) 

316 min_dy = (lp_h - lp_d) * self._linespacing 

317 

318 for i, line in enumerate(lines): 

319 clean_line, ismath = self._preprocess_math(line) 

320 if clean_line: 

321 w, h, d = _get_text_metrics_with_cache( 

322 renderer, clean_line, self._fontproperties, 

323 ismath=ismath, dpi=self.figure.dpi) 

324 else: 

325 w = h = d = 0 

326 

327 # For multiline text, increase the line spacing when the text 

328 # net-height (excluding baseline) is larger than that of a "l" 

329 # (e.g., use of superscripts), which seems what TeX does. 

330 h = max(h, lp_h) 

331 d = max(d, lp_d) 

332 

333 ws.append(w) 

334 hs.append(h) 

335 

336 # Metrics of the last line that are needed later: 

337 baseline = (h - d) - thisy 

338 

339 if i == 0: 

340 # position at baseline 

341 thisy = -(h - d) 

342 else: 

343 # put baseline a good distance from bottom of previous line 

344 thisy -= max(min_dy, (h - d) * self._linespacing) 

345 

346 xs.append(thisx) # == 0. 

347 ys.append(thisy) 

348 

349 thisy -= d 

350 

351 # Metrics of the last line that are needed later: 

352 descent = d 

353 

354 # Bounding box definition: 

355 width = max(ws) 

356 xmin = 0 

357 xmax = width 

358 ymax = 0 

359 ymin = ys[-1] - descent # baseline of last line minus its descent 

360 

361 # get the rotation matrix 

362 M = Affine2D().rotate_deg(self.get_rotation()) 

363 

364 # now offset the individual text lines within the box 

365 malign = self._get_multialignment() 

366 if malign == 'left': 

367 offset_layout = [(x, y) for x, y in zip(xs, ys)] 

368 elif malign == 'center': 

369 offset_layout = [(x + width / 2 - w / 2, y) 

370 for x, y, w in zip(xs, ys, ws)] 

371 elif malign == 'right': 

372 offset_layout = [(x + width - w, y) 

373 for x, y, w in zip(xs, ys, ws)] 

374 

375 # the corners of the unrotated bounding box 

376 corners_horiz = np.array( 

377 [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]) 

378 

379 # now rotate the bbox 

380 corners_rotated = M.transform(corners_horiz) 

381 # compute the bounds of the rotated box 

382 xmin = corners_rotated[:, 0].min() 

383 xmax = corners_rotated[:, 0].max() 

384 ymin = corners_rotated[:, 1].min() 

385 ymax = corners_rotated[:, 1].max() 

386 width = xmax - xmin 

387 height = ymax - ymin 

388 

389 # Now move the box to the target position offset the display 

390 # bbox by alignment 

391 halign = self._horizontalalignment 

392 valign = self._verticalalignment 

393 

394 rotation_mode = self.get_rotation_mode() 

395 if rotation_mode != "anchor": 

396 # compute the text location in display coords and the offsets 

397 # necessary to align the bbox with that location 

398 if halign == 'center': 

399 offsetx = (xmin + xmax) / 2 

400 elif halign == 'right': 

401 offsetx = xmax 

402 else: 

403 offsetx = xmin 

404 

405 if valign == 'center': 

406 offsety = (ymin + ymax) / 2 

407 elif valign == 'top': 

408 offsety = ymax 

409 elif valign == 'baseline': 

410 offsety = ymin + descent 

411 elif valign == 'center_baseline': 

412 offsety = ymin + height - baseline / 2.0 

413 else: 

414 offsety = ymin 

415 else: 

416 xmin1, ymin1 = corners_horiz[0] 

417 xmax1, ymax1 = corners_horiz[2] 

418 

419 if halign == 'center': 

420 offsetx = (xmin1 + xmax1) / 2.0 

421 elif halign == 'right': 

422 offsetx = xmax1 

423 else: 

424 offsetx = xmin1 

425 

426 if valign == 'center': 

427 offsety = (ymin1 + ymax1) / 2.0 

428 elif valign == 'top': 

429 offsety = ymax1 

430 elif valign == 'baseline': 

431 offsety = ymax1 - baseline 

432 elif valign == 'center_baseline': 

433 offsety = ymax1 - baseline / 2.0 

434 else: 

435 offsety = ymin1 

436 

437 offsetx, offsety = M.transform((offsetx, offsety)) 

438 

439 xmin -= offsetx 

440 ymin -= offsety 

441 

442 bbox = Bbox.from_bounds(xmin, ymin, width, height) 

443 

444 # now rotate the positions around the first (x, y) position 

445 xys = M.transform(offset_layout) - (offsetx, offsety) 

446 

447 return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent 

448 

449 def set_bbox(self, rectprops): 

450 """ 

451 Draw a bounding box around self. 

452 

453 Parameters 

454 ---------- 

455 rectprops : dict with properties for `.patches.FancyBboxPatch` 

456 The default boxstyle is 'square'. The mutation 

457 scale of the `.patches.FancyBboxPatch` is set to the fontsize. 

458 

459 Examples 

460 -------- 

461 :: 

462 

463 t.set_bbox(dict(facecolor='red', alpha=0.5)) 

464 """ 

465 

466 if rectprops is not None: 

467 props = rectprops.copy() 

468 boxstyle = props.pop("boxstyle", None) 

469 pad = props.pop("pad", None) 

470 if boxstyle is None: 

471 boxstyle = "square" 

472 if pad is None: 

473 pad = 4 # points 

474 pad /= self.get_size() # to fraction of font size 

475 else: 

476 if pad is None: 

477 pad = 0.3 

478 # boxstyle could be a callable or a string 

479 if isinstance(boxstyle, str) and "pad" not in boxstyle: 

480 boxstyle += ",pad=%0.2f" % pad 

481 self._bbox_patch = FancyBboxPatch( 

482 (0, 0), 1, 1, 

483 boxstyle=boxstyle, transform=IdentityTransform(), **props) 

484 else: 

485 self._bbox_patch = None 

486 

487 self._update_clip_properties() 

488 

489 def get_bbox_patch(self): 

490 """ 

491 Return the bbox Patch, or None if the `.patches.FancyBboxPatch` 

492 is not made. 

493 """ 

494 return self._bbox_patch 

495 

496 def update_bbox_position_size(self, renderer): 

497 """ 

498 Update the location and the size of the bbox. 

499 

500 This method should be used when the position and size of the bbox needs 

501 to be updated before actually drawing the bbox. 

502 """ 

503 if self._bbox_patch: 

504 # don't use self.get_unitless_position here, which refers to text 

505 # position in Text: 

506 posx = float(self.convert_xunits(self._x)) 

507 posy = float(self.convert_yunits(self._y)) 

508 posx, posy = self.get_transform().transform((posx, posy)) 

509 

510 x_box, y_box, w_box, h_box = _get_textbox(self, renderer) 

511 self._bbox_patch.set_bounds(0., 0., w_box, h_box) 

512 self._bbox_patch.set_transform( 

513 Affine2D() 

514 .rotate_deg(self.get_rotation()) 

515 .translate(posx + x_box, posy + y_box)) 

516 fontsize_in_pixel = renderer.points_to_pixels(self.get_size()) 

517 self._bbox_patch.set_mutation_scale(fontsize_in_pixel) 

518 

519 def _update_clip_properties(self): 

520 clipprops = dict(clip_box=self.clipbox, 

521 clip_path=self._clippath, 

522 clip_on=self._clipon) 

523 if self._bbox_patch: 

524 self._bbox_patch.update(clipprops) 

525 

526 def set_clip_box(self, clipbox): 

527 # docstring inherited. 

528 super().set_clip_box(clipbox) 

529 self._update_clip_properties() 

530 

531 def set_clip_path(self, path, transform=None): 

532 # docstring inherited. 

533 super().set_clip_path(path, transform) 

534 self._update_clip_properties() 

535 

536 def set_clip_on(self, b): 

537 # docstring inherited. 

538 super().set_clip_on(b) 

539 self._update_clip_properties() 

540 

541 def get_wrap(self): 

542 """Return whether the text can be wrapped.""" 

543 return self._wrap 

544 

545 def set_wrap(self, wrap): 

546 """ 

547 Set whether the text can be wrapped. 

548 

549 Parameters 

550 ---------- 

551 wrap : bool 

552 

553 Notes 

554 ----- 

555 Wrapping does not work together with 

556 ``savefig(..., bbox_inches='tight')`` (which is also used internally 

557 by ``%matplotlib inline`` in IPython/Jupyter). The 'tight' setting 

558 rescales the canvas to accommodate all content and happens before 

559 wrapping. 

560 """ 

561 self._wrap = wrap 

562 

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() 

570 

571 # Calculate available width based on text alignment 

572 alignment = self.get_horizontalalignment() 

573 self.set_rotation_mode('anchor') 

574 rotation = self.get_rotation() 

575 

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) 

579 

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) 

586 

587 return line_width 

588 

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)) 

609 

610 return min(h1, h2) 

611 

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) 

621 

622 def _get_wrapped_text(self): 

623 """ 

624 Return a copy of the text string with new lines added so that the text 

625 is wrapped relative to the parent figure (if `get_wrap` is True). 

626 """ 

627 if not self.get_wrap(): 

628 return self.get_text() 

629 

630 # Not fit to handle breaking up latex syntax correctly, so 

631 # ignore latex for now. 

632 if self.get_usetex(): 

633 return self.get_text() 

634 

635 # Build the line incrementally, for a more accurate measure of length 

636 line_width = self._get_wrap_line_width() 

637 wrapped_lines = [] 

638 

639 # New lines in the user's text force a split 

640 unwrapped_lines = self.get_text().split('\n') 

641 

642 # Now wrap each individual unwrapped line 

643 for unwrapped_line in unwrapped_lines: 

644 

645 sub_words = unwrapped_line.split(' ') 

646 # Remove items from sub_words as we go, so stop when empty 

647 while len(sub_words) > 0: 

648 if len(sub_words) == 1: 

649 # Only one word, so just add it to the end 

650 wrapped_lines.append(sub_words.pop(0)) 

651 continue 

652 

653 for i in range(2, len(sub_words) + 1): 

654 # Get width of all words up to and including here 

655 line = ' '.join(sub_words[:i]) 

656 current_width = self._get_rendered_text_width(line) 

657 

658 # If all these words are too wide, append all not including 

659 # last word 

660 if current_width > line_width: 

661 wrapped_lines.append(' '.join(sub_words[:i - 1])) 

662 sub_words = sub_words[i - 1:] 

663 break 

664 

665 # Otherwise if all words fit in the width, append them all 

666 elif i == len(sub_words): 

667 wrapped_lines.append(' '.join(sub_words[:i])) 

668 sub_words = [] 

669 break 

670 

671 return '\n'.join(wrapped_lines) 

672 

673 @artist.allow_rasterization 

674 def draw(self, renderer): 

675 # docstring inherited 

676 

677 if renderer is not None: 

678 self._renderer = renderer 

679 if not self.get_visible(): 

680 return 

681 if self.get_text() == '': 

682 return 

683 

684 renderer.open_group('text', self.get_gid()) 

685 

686 with self._cm_set(text=self._get_wrapped_text()): 

687 bbox, info, descent = self._get_layout(renderer) 

688 trans = self.get_transform() 

689 

690 # don't use self.get_position here, which refers to text 

691 # position in Text: 

692 posx = float(self.convert_xunits(self._x)) 

693 posy = float(self.convert_yunits(self._y)) 

694 posx, posy = trans.transform((posx, posy)) 

695 if not np.isfinite(posx) or not np.isfinite(posy): 

696 _log.warning("posx and posy should be finite values") 

697 return 

698 canvasw, canvash = renderer.get_canvas_width_height() 

699 

700 # Update the location and size of the bbox 

701 # (`.patches.FancyBboxPatch`), and draw it. 

702 if self._bbox_patch: 

703 self.update_bbox_position_size(renderer) 

704 self._bbox_patch.draw(renderer) 

705 

706 gc = renderer.new_gc() 

707 gc.set_foreground(self.get_color()) 

708 gc.set_alpha(self.get_alpha()) 

709 gc.set_url(self._url) 

710 self._set_gc_clip(gc) 

711 

712 angle = self.get_rotation() 

713 

714 for line, wh, x, y in info: 

715 

716 mtext = self if len(info) == 1 else None 

717 x = x + posx 

718 y = y + posy 

719 if renderer.flipy(): 

720 y = canvash - y 

721 clean_line, ismath = self._preprocess_math(line) 

722 

723 if self.get_path_effects(): 

724 from matplotlib.patheffects import PathEffectRenderer 

725 textrenderer = PathEffectRenderer( 

726 self.get_path_effects(), renderer) 

727 else: 

728 textrenderer = renderer 

729 

730 if self.get_usetex(): 

731 textrenderer.draw_tex(gc, x, y, clean_line, 

732 self._fontproperties, angle, 

733 mtext=mtext) 

734 else: 

735 textrenderer.draw_text(gc, x, y, clean_line, 

736 self._fontproperties, angle, 

737 ismath=ismath, mtext=mtext) 

738 

739 gc.restore() 

740 renderer.close_group('text') 

741 self.stale = False 

742 

743 def get_color(self): 

744 """Return the color of the text.""" 

745 return self._color 

746 

747 def get_fontproperties(self): 

748 """Return the `.font_manager.FontProperties`.""" 

749 return self._fontproperties 

750 

751 def get_fontfamily(self): 

752 """ 

753 Return the list of font families used for font lookup. 

754 

755 See Also 

756 -------- 

757 .font_manager.FontProperties.get_family 

758 """ 

759 return self._fontproperties.get_family() 

760 

761 def get_fontname(self): 

762 """ 

763 Return the font name as a string. 

764 

765 See Also 

766 -------- 

767 .font_manager.FontProperties.get_name 

768 """ 

769 return self._fontproperties.get_name() 

770 

771 def get_fontstyle(self): 

772 """ 

773 Return the font style as a string. 

774 

775 See Also 

776 -------- 

777 .font_manager.FontProperties.get_style 

778 """ 

779 return self._fontproperties.get_style() 

780 

781 def get_fontsize(self): 

782 """ 

783 Return the font size as an integer. 

784 

785 See Also 

786 -------- 

787 .font_manager.FontProperties.get_size_in_points 

788 """ 

789 return self._fontproperties.get_size_in_points() 

790 

791 def get_fontvariant(self): 

792 """ 

793 Return the font variant as a string. 

794 

795 See Also 

796 -------- 

797 .font_manager.FontProperties.get_variant 

798 """ 

799 return self._fontproperties.get_variant() 

800 

801 def get_fontweight(self): 

802 """ 

803 Return the font weight as a string or a number. 

804 

805 See Also 

806 -------- 

807 .font_manager.FontProperties.get_weight 

808 """ 

809 return self._fontproperties.get_weight() 

810 

811 def get_stretch(self): 

812 """ 

813 Return the font stretch as a string or a number. 

814 

815 See Also 

816 -------- 

817 .font_manager.FontProperties.get_stretch 

818 """ 

819 return self._fontproperties.get_stretch() 

820 

821 def get_horizontalalignment(self): 

822 """ 

823 Return the horizontal alignment as a string. Will be one of 

824 'left', 'center' or 'right'. 

825 """ 

826 return self._horizontalalignment 

827 

828 def get_unitless_position(self): 

829 """Return the (x, y) unitless position of the text.""" 

830 # This will get the position with all unit information stripped away. 

831 # This is here for convenience since it is done in several locations. 

832 x = float(self.convert_xunits(self._x)) 

833 y = float(self.convert_yunits(self._y)) 

834 return x, y 

835 

836 def get_position(self): 

837 """Return the (x, y) position of the text.""" 

838 # This should return the same data (possible unitized) as was 

839 # specified with 'set_x' and 'set_y'. 

840 return self._x, self._y 

841 

842 # When removing, also remove the hash(color) check in set_color() 

843 @_api.deprecated("3.5") 

844 def get_prop_tup(self, renderer=None): 

845 """ 

846 Return a hashable tuple of properties. 

847 

848 Not intended to be human readable, but useful for backends who 

849 want to cache derived information about text (e.g., layouts) and 

850 need to know if the text has changed. 

851 """ 

852 x, y = self.get_unitless_position() 

853 renderer = renderer or self._renderer 

854 return (x, y, self.get_text(), self._color, 

855 self._verticalalignment, self._horizontalalignment, 

856 hash(self._fontproperties), 

857 self._rotation, self._rotation_mode, 

858 self._transform_rotates_text, 

859 self.figure.dpi, weakref.ref(renderer), 

860 self._linespacing 

861 ) 

862 

863 def get_text(self): 

864 """Return the text string.""" 

865 return self._text 

866 

867 def get_verticalalignment(self): 

868 """ 

869 Return the vertical alignment as a string. Will be one of 

870 'top', 'center', 'bottom', 'baseline' or 'center_baseline'. 

871 """ 

872 return self._verticalalignment 

873 

874 def get_window_extent(self, renderer=None, dpi=None): 

875 """ 

876 Return the `.Bbox` bounding the text, in display units. 

877 

878 In addition to being used internally, this is useful for specifying 

879 clickable regions in a png file on a web page. 

880 

881 Parameters 

882 ---------- 

883 renderer : Renderer, optional 

884 A renderer is needed to compute the bounding box. If the artist 

885 has already been drawn, the renderer is cached; thus, it is only 

886 necessary to pass this argument when calling `get_window_extent` 

887 before the first draw. In practice, it is usually easier to 

888 trigger a draw first, e.g. by calling 

889 `~.Figure.draw_without_rendering` or ``plt.show()``. 

890 

891 dpi : float, optional 

892 The dpi value for computing the bbox, defaults to 

893 ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if 

894 to match regions with a figure saved with a custom dpi value. 

895 """ 

896 if not self.get_visible(): 

897 return Bbox.unit() 

898 if dpi is None: 

899 dpi = self.figure.dpi 

900 if self.get_text() == '': 

901 with cbook._setattr_cm(self.figure, dpi=dpi): 

902 tx, ty = self._get_xy_display() 

903 return Bbox.from_bounds(tx, ty, 0, 0) 

904 

905 if renderer is not None: 

906 self._renderer = renderer 

907 if self._renderer is None: 

908 self._renderer = self.figure._get_renderer() 

909 if self._renderer is None: 

910 raise RuntimeError( 

911 "Cannot get window extent of text w/o renderer. You likely " 

912 "want to call 'figure.draw_without_rendering()' first.") 

913 

914 with cbook._setattr_cm(self.figure, dpi=dpi): 

915 bbox, info, descent = self._get_layout(self._renderer) 

916 x, y = self.get_unitless_position() 

917 x, y = self.get_transform().transform((x, y)) 

918 bbox = bbox.translated(x, y) 

919 return bbox 

920 

921 def set_backgroundcolor(self, color): 

922 """ 

923 Set the background color of the text by updating the bbox. 

924 

925 Parameters 

926 ---------- 

927 color : color 

928 

929 See Also 

930 -------- 

931 .set_bbox : To change the position of the bounding box 

932 """ 

933 if self._bbox_patch is None: 

934 self.set_bbox(dict(facecolor=color, edgecolor=color)) 

935 else: 

936 self._bbox_patch.update(dict(facecolor=color)) 

937 

938 self._update_clip_properties() 

939 self.stale = True 

940 

941 def set_color(self, color): 

942 """ 

943 Set the foreground color of the text 

944 

945 Parameters 

946 ---------- 

947 color : color 

948 """ 

949 # "auto" is only supported by axisartist, but we can just let it error 

950 # out at draw time for simplicity. 

951 if not cbook._str_equal(color, "auto"): 

952 mpl.colors._check_color_like(color=color) 

953 # Make sure it is hashable, or get_prop_tup will fail (remove this once 

954 # get_prop_tup is removed). 

955 try: 

956 hash(color) 

957 except TypeError: 

958 color = tuple(color) 

959 self._color = color 

960 self.stale = True 

961 

962 def set_horizontalalignment(self, align): 

963 """ 

964 Set the horizontal alignment relative to the anchor point. 

965 

966 See also :doc:`/gallery/text_labels_and_annotations/text_alignment`. 

967 

968 Parameters 

969 ---------- 

970 align : {'left', 'center', 'right'} 

971 """ 

972 _api.check_in_list(['center', 'right', 'left'], align=align) 

973 self._horizontalalignment = align 

974 self.stale = True 

975 

976 def set_multialignment(self, align): 

977 """ 

978 Set the text alignment for multiline texts. 

979 

980 The layout of the bounding box of all the lines is determined by the 

981 horizontalalignment and verticalalignment properties. This property 

982 controls the alignment of the text lines within that box. 

983 

984 Parameters 

985 ---------- 

986 align : {'left', 'right', 'center'} 

987 """ 

988 _api.check_in_list(['center', 'right', 'left'], align=align) 

989 self._multialignment = align 

990 self.stale = True 

991 

992 def set_linespacing(self, spacing): 

993 """ 

994 Set the line spacing as a multiple of the font size. 

995 

996 The default line spacing is 1.2. 

997 

998 Parameters 

999 ---------- 

1000 spacing : float (multiple of font size) 

1001 """ 

1002 _api.check_isinstance(Real, spacing=spacing) 

1003 self._linespacing = spacing 

1004 self.stale = True 

1005 

1006 def set_fontfamily(self, fontname): 

1007 """ 

1008 Set the font family. Can be either a single string, or a list of 

1009 strings in decreasing priority. Each string may be either a real font 

1010 name or a generic font class name. If the latter, the specific font 

1011 names will be looked up in the corresponding rcParams. 

1012 

1013 If a `Text` instance is constructed with ``fontfamily=None``, then the 

1014 font is set to :rc:`font.family`, and the 

1015 same is done when `set_fontfamily()` is called on an existing 

1016 `Text` instance. 

1017 

1018 Parameters 

1019 ---------- 

1020 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \ 

1021'monospace'} 

1022 

1023 See Also 

1024 -------- 

1025 .font_manager.FontProperties.set_family 

1026 """ 

1027 self._fontproperties.set_family(fontname) 

1028 self.stale = True 

1029 

1030 def set_fontvariant(self, variant): 

1031 """ 

1032 Set the font variant. 

1033 

1034 Parameters 

1035 ---------- 

1036 variant : {'normal', 'small-caps'} 

1037 

1038 See Also 

1039 -------- 

1040 .font_manager.FontProperties.set_variant 

1041 """ 

1042 self._fontproperties.set_variant(variant) 

1043 self.stale = True 

1044 

1045 def set_fontstyle(self, fontstyle): 

1046 """ 

1047 Set the font style. 

1048 

1049 Parameters 

1050 ---------- 

1051 fontstyle : {'normal', 'italic', 'oblique'} 

1052 

1053 See Also 

1054 -------- 

1055 .font_manager.FontProperties.set_style 

1056 """ 

1057 self._fontproperties.set_style(fontstyle) 

1058 self.stale = True 

1059 

1060 def set_fontsize(self, fontsize): 

1061 """ 

1062 Set the font size. 

1063 

1064 Parameters 

1065 ---------- 

1066 fontsize : float or {'xx-small', 'x-small', 'small', 'medium', \ 

1067'large', 'x-large', 'xx-large'} 

1068 If a float, the fontsize in points. The string values denote sizes 

1069 relative to the default font size. 

1070 

1071 See Also 

1072 -------- 

1073 .font_manager.FontProperties.set_size 

1074 """ 

1075 self._fontproperties.set_size(fontsize) 

1076 self.stale = True 

1077 

1078 def get_math_fontfamily(self): 

1079 """ 

1080 Return the font family name for math text rendered by Matplotlib. 

1081 

1082 The default value is :rc:`mathtext.fontset`. 

1083 

1084 See Also 

1085 -------- 

1086 set_math_fontfamily 

1087 """ 

1088 return self._fontproperties.get_math_fontfamily() 

1089 

1090 def set_math_fontfamily(self, fontfamily): 

1091 """ 

1092 Set the font family for math text rendered by Matplotlib. 

1093 

1094 This does only affect Matplotlib's own math renderer. It has no effect 

1095 when rendering with TeX (``usetex=True``). 

1096 

1097 Parameters 

1098 ---------- 

1099 fontfamily : str 

1100 The name of the font family. 

1101 

1102 Available font families are defined in the 

1103 :ref:`matplotlibrc.template file 

1104 <customizing-with-matplotlibrc-files>`. 

1105 

1106 See Also 

1107 -------- 

1108 get_math_fontfamily 

1109 """ 

1110 self._fontproperties.set_math_fontfamily(fontfamily) 

1111 

1112 def set_fontweight(self, weight): 

1113 """ 

1114 Set the font weight. 

1115 

1116 Parameters 

1117 ---------- 

1118 weight : {a numeric value in range 0-1000, 'ultralight', 'light', \ 

1119'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', \ 

1120'demi', 'bold', 'heavy', 'extra bold', 'black'} 

1121 

1122 See Also 

1123 -------- 

1124 .font_manager.FontProperties.set_weight 

1125 """ 

1126 self._fontproperties.set_weight(weight) 

1127 self.stale = True 

1128 

1129 def set_fontstretch(self, stretch): 

1130 """ 

1131 Set the font stretch (horizontal condensation or expansion). 

1132 

1133 Parameters 

1134 ---------- 

1135 stretch : {a numeric value in range 0-1000, 'ultra-condensed', \ 

1136'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', \ 

1137'expanded', 'extra-expanded', 'ultra-expanded'} 

1138 

1139 See Also 

1140 -------- 

1141 .font_manager.FontProperties.set_stretch 

1142 """ 

1143 self._fontproperties.set_stretch(stretch) 

1144 self.stale = True 

1145 

1146 def set_position(self, xy): 

1147 """ 

1148 Set the (*x*, *y*) position of the text. 

1149 

1150 Parameters 

1151 ---------- 

1152 xy : (float, float) 

1153 """ 

1154 self.set_x(xy[0]) 

1155 self.set_y(xy[1]) 

1156 

1157 def set_x(self, x): 

1158 """ 

1159 Set the *x* position of the text. 

1160 

1161 Parameters 

1162 ---------- 

1163 x : float 

1164 """ 

1165 self._x = x 

1166 self.stale = True 

1167 

1168 def set_y(self, y): 

1169 """ 

1170 Set the *y* position of the text. 

1171 

1172 Parameters 

1173 ---------- 

1174 y : float 

1175 """ 

1176 self._y = y 

1177 self.stale = True 

1178 

1179 def set_rotation(self, s): 

1180 """ 

1181 Set the rotation of the text. 

1182 

1183 Parameters 

1184 ---------- 

1185 s : float or {'vertical', 'horizontal'} 

1186 The rotation angle in degrees in mathematically positive direction 

1187 (counterclockwise). 'horizontal' equals 0, 'vertical' equals 90. 

1188 """ 

1189 if isinstance(s, Real): 

1190 self._rotation = float(s) % 360 

1191 elif cbook._str_equal(s, 'horizontal') or s is None: 

1192 self._rotation = 0. 

1193 elif cbook._str_equal(s, 'vertical'): 

1194 self._rotation = 90. 

1195 else: 

1196 raise ValueError("rotation must be 'vertical', 'horizontal' or " 

1197 f"a number, not {s}") 

1198 self.stale = True 

1199 

1200 def set_transform_rotates_text(self, t): 

1201 """ 

1202 Whether rotations of the transform affect the text direction. 

1203 

1204 Parameters 

1205 ---------- 

1206 t : bool 

1207 """ 

1208 self._transform_rotates_text = t 

1209 self.stale = True 

1210 

1211 def set_verticalalignment(self, align): 

1212 """ 

1213 Set the vertical alignment relative to the anchor point. 

1214 

1215 See also :doc:`/gallery/text_labels_and_annotations/text_alignment`. 

1216 

1217 Parameters 

1218 ---------- 

1219 align : {'bottom', 'baseline', 'center', 'center_baseline', 'top'} 

1220 """ 

1221 _api.check_in_list( 

1222 ['top', 'bottom', 'center', 'baseline', 'center_baseline'], 

1223 align=align) 

1224 self._verticalalignment = align 

1225 self.stale = True 

1226 

1227 def set_text(self, s): 

1228 r""" 

1229 Set the text string *s*. 

1230 

1231 It may contain newlines (``\n``) or math in LaTeX syntax. 

1232 

1233 Parameters 

1234 ---------- 

1235 s : object 

1236 Any object gets converted to its `str` representation, except for 

1237 ``None`` which is converted to an empty string. 

1238 """ 

1239 if s is None: 

1240 s = '' 

1241 if s != self._text: 

1242 self._text = str(s) 

1243 self.stale = True 

1244 

1245 def _preprocess_math(self, s): 

1246 """ 

1247 Return the string *s* after mathtext preprocessing, and the kind of 

1248 mathtext support needed. 

1249 

1250 - If *self* is configured to use TeX, return *s* unchanged except that 

1251 a single space gets escaped, and the flag "TeX". 

1252 - Otherwise, if *s* is mathtext (has an even number of unescaped dollar 

1253 signs) and ``parse_math`` is not set to False, return *s* and the 

1254 flag True. 

1255 - Otherwise, return *s* with dollar signs unescaped, and the flag 

1256 False. 

1257 """ 

1258 if self.get_usetex(): 

1259 if s == " ": 

1260 s = r"\ " 

1261 return s, "TeX" 

1262 elif not self.get_parse_math(): 

1263 return s, False 

1264 elif cbook.is_math_text(s): 

1265 return s, True 

1266 else: 

1267 return s.replace(r"\$", "$"), False 

1268 

1269 def set_fontproperties(self, fp): 

1270 """ 

1271 Set the font properties that control the text. 

1272 

1273 Parameters 

1274 ---------- 

1275 fp : `.font_manager.FontProperties` or `str` or `pathlib.Path` 

1276 If a `str`, it is interpreted as a fontconfig pattern parsed by 

1277 `.FontProperties`. If a `pathlib.Path`, it is interpreted as the 

1278 absolute path to a font file. 

1279 """ 

1280 self._fontproperties = FontProperties._from_any(fp).copy() 

1281 self.stale = True 

1282 

1283 def set_usetex(self, usetex): 

1284 """ 

1285 Parameters 

1286 ---------- 

1287 usetex : bool or None 

1288 Whether to render using TeX, ``None`` means to use 

1289 :rc:`text.usetex`. 

1290 """ 

1291 if usetex is None: 

1292 self._usetex = mpl.rcParams['text.usetex'] 

1293 else: 

1294 self._usetex = bool(usetex) 

1295 self.stale = True 

1296 

1297 def get_usetex(self): 

1298 """Return whether this `Text` object uses TeX for rendering.""" 

1299 return self._usetex 

1300 

1301 def set_parse_math(self, parse_math): 

1302 """ 

1303 Override switch to disable any mathtext parsing for this `Text`. 

1304 

1305 Parameters 

1306 ---------- 

1307 parse_math : bool 

1308 If False, this `Text` will never use mathtext. If True, mathtext 

1309 will be used if there is an even number of unescaped dollar signs. 

1310 """ 

1311 self._parse_math = bool(parse_math) 

1312 

1313 def get_parse_math(self): 

1314 """Return whether mathtext parsing is considered for this `Text`.""" 

1315 return self._parse_math 

1316 

1317 def set_fontname(self, fontname): 

1318 """ 

1319 Alias for `set_family`. 

1320 

1321 One-way alias only: the getter differs. 

1322 

1323 Parameters 

1324 ---------- 

1325 fontname : {FONTNAME, 'serif', 'sans-serif', 'cursive', 'fantasy', \ 

1326'monospace'} 

1327 

1328 See Also 

1329 -------- 

1330 .font_manager.FontProperties.set_family 

1331 

1332 """ 

1333 return self.set_family(fontname) 

1334 

1335 

1336class OffsetFrom: 

1337 """Callable helper class for working with `Annotation`.""" 

1338 

1339 def __init__(self, artist, ref_coord, unit="points"): 

1340 """ 

1341 Parameters 

1342 ---------- 

1343 artist : `.Artist` or `.BboxBase` or `.Transform` 

1344 The object to compute the offset from. 

1345 

1346 ref_coord : (float, float) 

1347 If *artist* is an `.Artist` or `.BboxBase`, this values is 

1348 the location to of the offset origin in fractions of the 

1349 *artist* bounding box. 

1350 

1351 If *artist* is a transform, the offset origin is the 

1352 transform applied to this value. 

1353 

1354 unit : {'points, 'pixels'}, default: 'points' 

1355 The screen units to use (pixels or points) for the offset input. 

1356 """ 

1357 self._artist = artist 

1358 self._ref_coord = ref_coord 

1359 self.set_unit(unit) 

1360 

1361 def set_unit(self, unit): 

1362 """ 

1363 Set the unit for input to the transform used by ``__call__``. 

1364 

1365 Parameters 

1366 ---------- 

1367 unit : {'points', 'pixels'} 

1368 """ 

1369 _api.check_in_list(["points", "pixels"], unit=unit) 

1370 self._unit = unit 

1371 

1372 def get_unit(self): 

1373 """Return the unit for input to the transform used by ``__call__``.""" 

1374 return self._unit 

1375 

1376 def _get_scale(self, renderer): 

1377 unit = self.get_unit() 

1378 if unit == "pixels": 

1379 return 1. 

1380 else: 

1381 return renderer.points_to_pixels(1.) 

1382 

1383 def __call__(self, renderer): 

1384 """ 

1385 Return the offset transform. 

1386 

1387 Parameters 

1388 ---------- 

1389 renderer : `RendererBase` 

1390 The renderer to use to compute the offset 

1391 

1392 Returns 

1393 ------- 

1394 `Transform` 

1395 Maps (x, y) in pixel or point units to screen units 

1396 relative to the given artist. 

1397 """ 

1398 if isinstance(self._artist, Artist): 

1399 bbox = self._artist.get_window_extent(renderer) 

1400 xf, yf = self._ref_coord 

1401 x = bbox.x0 + bbox.width * xf 

1402 y = bbox.y0 + bbox.height * yf 

1403 elif isinstance(self._artist, BboxBase): 

1404 bbox = self._artist 

1405 xf, yf = self._ref_coord 

1406 x = bbox.x0 + bbox.width * xf 

1407 y = bbox.y0 + bbox.height * yf 

1408 elif isinstance(self._artist, Transform): 

1409 x, y = self._artist.transform(self._ref_coord) 

1410 else: 

1411 raise RuntimeError("unknown type") 

1412 

1413 sc = self._get_scale(renderer) 

1414 tr = Affine2D().scale(sc).translate(x, y) 

1415 

1416 return tr 

1417 

1418 

1419class _AnnotationBase: 

1420 def __init__(self, 

1421 xy, 

1422 xycoords='data', 

1423 annotation_clip=None): 

1424 

1425 self.xy = xy 

1426 self.xycoords = xycoords 

1427 self.set_annotation_clip(annotation_clip) 

1428 

1429 self._draggable = None 

1430 

1431 def _get_xy(self, renderer, x, y, s): 

1432 if isinstance(s, tuple): 

1433 s1, s2 = s 

1434 else: 

1435 s1, s2 = s, s 

1436 if s1 == 'data': 

1437 x = float(self.convert_xunits(x)) 

1438 if s2 == 'data': 

1439 y = float(self.convert_yunits(y)) 

1440 return self._get_xy_transform(renderer, s).transform((x, y)) 

1441 

1442 def _get_xy_transform(self, renderer, s): 

1443 

1444 if isinstance(s, tuple): 

1445 s1, s2 = s 

1446 from matplotlib.transforms import blended_transform_factory 

1447 tr1 = self._get_xy_transform(renderer, s1) 

1448 tr2 = self._get_xy_transform(renderer, s2) 

1449 tr = blended_transform_factory(tr1, tr2) 

1450 return tr 

1451 elif callable(s): 

1452 tr = s(renderer) 

1453 if isinstance(tr, BboxBase): 

1454 return BboxTransformTo(tr) 

1455 elif isinstance(tr, Transform): 

1456 return tr 

1457 else: 

1458 raise RuntimeError("Unknown return type") 

1459 elif isinstance(s, Artist): 

1460 bbox = s.get_window_extent(renderer) 

1461 return BboxTransformTo(bbox) 

1462 elif isinstance(s, BboxBase): 

1463 return BboxTransformTo(s) 

1464 elif isinstance(s, Transform): 

1465 return s 

1466 elif not isinstance(s, str): 

1467 raise RuntimeError(f"Unknown coordinate type: {s!r}") 

1468 

1469 if s == 'data': 

1470 return self.axes.transData 

1471 elif s == 'polar': 

1472 from matplotlib.projections import PolarAxes 

1473 tr = PolarAxes.PolarTransform() 

1474 trans = tr + self.axes.transData 

1475 return trans 

1476 

1477 s_ = s.split() 

1478 if len(s_) != 2: 

1479 raise ValueError(f"{s!r} is not a recognized coordinate") 

1480 

1481 bbox0, xy0 = None, None 

1482 

1483 bbox_name, unit = s_ 

1484 # if unit is offset-like 

1485 if bbox_name == "figure": 

1486 bbox0 = self.figure.figbbox 

1487 elif bbox_name == "subfigure": 

1488 bbox0 = self.figure.bbox 

1489 elif bbox_name == "axes": 

1490 bbox0 = self.axes.bbox 

1491 # elif bbox_name == "bbox": 

1492 # if bbox is None: 

1493 # raise RuntimeError("bbox is specified as a coordinate but " 

1494 # "never set") 

1495 # bbox0 = self._get_bbox(renderer, bbox) 

1496 

1497 if bbox0 is not None: 

1498 xy0 = bbox0.p0 

1499 elif bbox_name == "offset": 

1500 xy0 = self._get_ref_xy(renderer) 

1501 

1502 if xy0 is not None: 

1503 # reference x, y in display coordinate 

1504 ref_x, ref_y = xy0 

1505 if unit == "points": 

1506 # dots per points 

1507 dpp = self.figure.dpi / 72 

1508 tr = Affine2D().scale(dpp) 

1509 elif unit == "pixels": 

1510 tr = Affine2D() 

1511 elif unit == "fontsize": 

1512 fontsize = self.get_size() 

1513 dpp = fontsize * self.figure.dpi / 72 

1514 tr = Affine2D().scale(dpp) 

1515 elif unit == "fraction": 

1516 w, h = bbox0.size 

1517 tr = Affine2D().scale(w, h) 

1518 else: 

1519 raise ValueError(f"{unit!r} is not a recognized unit") 

1520 

1521 return tr.translate(ref_x, ref_y) 

1522 

1523 else: 

1524 raise ValueError(f"{s!r} is not a recognized coordinate") 

1525 

1526 def _get_ref_xy(self, renderer): 

1527 """ 

1528 Return x, y (in display coordinates) that is to be used for a reference 

1529 of any offset coordinate. 

1530 """ 

1531 return self._get_xy(renderer, *self.xy, self.xycoords) 

1532 

1533 # def _get_bbox(self, renderer): 

1534 # if hasattr(bbox, "bounds"): 

1535 # return bbox 

1536 # elif hasattr(bbox, "get_window_extent"): 

1537 # bbox = bbox.get_window_extent() 

1538 # return bbox 

1539 # else: 

1540 # raise ValueError("A bbox instance is expected but got %s" % 

1541 # str(bbox)) 

1542 

1543 def set_annotation_clip(self, b): 

1544 """ 

1545 Set the annotation's clipping behavior. 

1546 

1547 Parameters 

1548 ---------- 

1549 b : bool or None 

1550 - True: The annotation will be clipped when ``self.xy`` is 

1551 outside the axes. 

1552 - False: The annotation will always be drawn. 

1553 - None: The annotation will be clipped when ``self.xy`` is 

1554 outside the axes and ``self.xycoords == "data"``. 

1555 """ 

1556 self._annotation_clip = b 

1557 

1558 def get_annotation_clip(self): 

1559 """ 

1560 Return the annotation's clipping behavior. 

1561 

1562 See `set_annotation_clip` for the meaning of return values. 

1563 """ 

1564 return self._annotation_clip 

1565 

1566 def _get_position_xy(self, renderer): 

1567 """Return the pixel position of the annotated point.""" 

1568 x, y = self.xy 

1569 return self._get_xy(renderer, x, y, self.xycoords) 

1570 

1571 def _check_xy(self, renderer=None): 

1572 """Check whether the annotation at *xy_pixel* should be drawn.""" 

1573 if renderer is None: 

1574 renderer = self.figure._get_renderer() 

1575 b = self.get_annotation_clip() 

1576 if b or (b is None and self.xycoords == "data"): 

1577 # check if self.xy is inside the axes. 

1578 xy_pixel = self._get_position_xy(renderer) 

1579 return self.axes.contains_point(xy_pixel) 

1580 return True 

1581 

1582 def draggable(self, state=None, use_blit=False): 

1583 """ 

1584 Set whether the annotation is draggable with the mouse. 

1585 

1586 Parameters 

1587 ---------- 

1588 state : bool or None 

1589 - True or False: set the draggability. 

1590 - None: toggle the draggability. 

1591 

1592 Returns 

1593 ------- 

1594 DraggableAnnotation or None 

1595 If the annotation is draggable, the corresponding 

1596 `.DraggableAnnotation` helper is returned. 

1597 """ 

1598 from matplotlib.offsetbox import DraggableAnnotation 

1599 is_draggable = self._draggable is not None 

1600 

1601 # if state is None we'll toggle 

1602 if state is None: 

1603 state = not is_draggable 

1604 

1605 if state: 

1606 if self._draggable is None: 

1607 self._draggable = DraggableAnnotation(self, use_blit) 

1608 else: 

1609 if self._draggable is not None: 

1610 self._draggable.disconnect() 

1611 self._draggable = None 

1612 

1613 return self._draggable 

1614 

1615 

1616class Annotation(Text, _AnnotationBase): 

1617 """ 

1618 An `.Annotation` is a `.Text` that can refer to a specific position *xy*. 

1619 Optionally an arrow pointing from the text to *xy* can be drawn. 

1620 

1621 Attributes 

1622 ---------- 

1623 xy 

1624 The annotated position. 

1625 xycoords 

1626 The coordinate system for *xy*. 

1627 arrow_patch 

1628 A `.FancyArrowPatch` to point from *xytext* to *xy*. 

1629 """ 

1630 

1631 def __str__(self): 

1632 return "Annotation(%g, %g, %r)" % (self.xy[0], self.xy[1], self._text) 

1633 

1634 def __init__(self, text, xy, 

1635 xytext=None, 

1636 xycoords='data', 

1637 textcoords=None, 

1638 arrowprops=None, 

1639 annotation_clip=None, 

1640 **kwargs): 

1641 """ 

1642 Annotate the point *xy* with text *text*. 

1643 

1644 In the simplest form, the text is placed at *xy*. 

1645 

1646 Optionally, the text can be displayed in another position *xytext*. 

1647 An arrow pointing from the text to the annotated point *xy* can then 

1648 be added by defining *arrowprops*. 

1649 

1650 Parameters 

1651 ---------- 

1652 text : str 

1653 The text of the annotation. 

1654 

1655 xy : (float, float) 

1656 The point *(x, y)* to annotate. The coordinate system is determined 

1657 by *xycoords*. 

1658 

1659 xytext : (float, float), default: *xy* 

1660 The position *(x, y)* to place the text at. The coordinate system 

1661 is determined by *textcoords*. 

1662 

1663 xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \ 

1664callable, default: 'data' 

1665 

1666 The coordinate system that *xy* is given in. The following types 

1667 of values are supported: 

1668 

1669 - One of the following strings: 

1670 

1671 ==================== ============================================ 

1672 Value Description 

1673 ==================== ============================================ 

1674 'figure points' Points from the lower left of the figure 

1675 'figure pixels' Pixels from the lower left of the figure 

1676 'figure fraction' Fraction of figure from lower left 

1677 'subfigure points' Points from the lower left of the subfigure 

1678 'subfigure pixels' Pixels from the lower left of the subfigure 

1679 'subfigure fraction' Fraction of subfigure from lower left 

1680 'axes points' Points from lower left corner of axes 

1681 'axes pixels' Pixels from lower left corner of axes 

1682 'axes fraction' Fraction of axes from lower left 

1683 'data' Use the coordinate system of the object 

1684 being annotated (default) 

1685 'polar' *(theta, r)* if not native 'data' 

1686 coordinates 

1687 ==================== ============================================ 

1688 

1689 Note that 'subfigure pixels' and 'figure pixels' are the same 

1690 for the parent figure, so users who want code that is usable in 

1691 a subfigure can use 'subfigure pixels'. 

1692 

1693 - An `.Artist`: *xy* is interpreted as a fraction of the artist's 

1694 `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower 

1695 left corner of the bounding box and *(0.5, 1)* would be the 

1696 center top of the bounding box. 

1697 

1698 - A `.Transform` to transform *xy* to screen coordinates. 

1699 

1700 - A function with one of the following signatures:: 

1701 

1702 def transform(renderer) -> Bbox 

1703 def transform(renderer) -> Transform 

1704 

1705 where *renderer* is a `.RendererBase` subclass. 

1706 

1707 The result of the function is interpreted like the `.Artist` and 

1708 `.Transform` cases above. 

1709 

1710 - A tuple *(xcoords, ycoords)* specifying separate coordinate 

1711 systems for *x* and *y*. *xcoords* and *ycoords* must each be 

1712 of one of the above described types. 

1713 

1714 See :ref:`plotting-guide-annotation` for more details. 

1715 

1716 textcoords : single or two-tuple of str or `.Artist` or `.Transform` \ 

1717or callable, default: value of *xycoords* 

1718 The coordinate system that *xytext* is given in. 

1719 

1720 All *xycoords* values are valid as well as the following 

1721 strings: 

1722 

1723 ================= ========================================= 

1724 Value Description 

1725 ================= ========================================= 

1726 'offset points' Offset (in points) from the *xy* value 

1727 'offset pixels' Offset (in pixels) from the *xy* value 

1728 ================= ========================================= 

1729 

1730 arrowprops : dict, optional 

1731 The properties used to draw a `.FancyArrowPatch` arrow between the 

1732 positions *xy* and *xytext*. Defaults to None, i.e. no arrow is 

1733 drawn. 

1734 

1735 For historical reasons there are two different ways to specify 

1736 arrows, "simple" and "fancy": 

1737 

1738 **Simple arrow:** 

1739 

1740 If *arrowprops* does not contain the key 'arrowstyle' the 

1741 allowed keys are: 

1742 

1743 ========== ====================================================== 

1744 Key Description 

1745 ========== ====================================================== 

1746 width The width of the arrow in points 

1747 headwidth The width of the base of the arrow head in points 

1748 headlength The length of the arrow head in points 

1749 shrink Fraction of total length to shrink from both ends 

1750 ? Any key to :class:`matplotlib.patches.FancyArrowPatch` 

1751 ========== ====================================================== 

1752 

1753 The arrow is attached to the edge of the text box, the exact 

1754 position (corners or centers) depending on where it's pointing to. 

1755 

1756 **Fancy arrow:** 

1757 

1758 This is used if 'arrowstyle' is provided in the *arrowprops*. 

1759 

1760 Valid keys are the following `~matplotlib.patches.FancyArrowPatch` 

1761 parameters: 

1762 

1763 =============== ================================================== 

1764 Key Description 

1765 =============== ================================================== 

1766 arrowstyle the arrow style 

1767 connectionstyle the connection style 

1768 relpos see below; default is (0.5, 0.5) 

1769 patchA default is bounding box of the text 

1770 patchB default is None 

1771 shrinkA default is 2 points 

1772 shrinkB default is 2 points 

1773 mutation_scale default is text size (in points) 

1774 mutation_aspect default is 1. 

1775 ? any key for :class:`matplotlib.patches.PathPatch` 

1776 =============== ================================================== 

1777 

1778 The exact starting point position of the arrow is defined by 

1779 *relpos*. It's a tuple of relative coordinates of the text box, 

1780 where (0, 0) is the lower left corner and (1, 1) is the upper 

1781 right corner. Values <0 and >1 are supported and specify points 

1782 outside the text box. By default (0.5, 0.5), so the starting point 

1783 is centered in the text box. 

1784 

1785 annotation_clip : bool or None, default: None 

1786 Whether to clip (i.e. not draw) the annotation when the annotation 

1787 point *xy* is outside the axes area. 

1788 

1789 - If *True*, the annotation will be clipped when *xy* is outside 

1790 the axes. 

1791 - If *False*, the annotation will always be drawn. 

1792 - If *None*, the annotation will be clipped when *xy* is outside 

1793 the axes and *xycoords* is 'data'. 

1794 

1795 **kwargs 

1796 Additional kwargs are passed to `~matplotlib.text.Text`. 

1797 

1798 Returns 

1799 ------- 

1800 `.Annotation` 

1801 

1802 See Also 

1803 -------- 

1804 :ref:`plotting-guide-annotation` 

1805 

1806 """ 

1807 _AnnotationBase.__init__(self, 

1808 xy, 

1809 xycoords=xycoords, 

1810 annotation_clip=annotation_clip) 

1811 # warn about wonky input data 

1812 if (xytext is None and 

1813 textcoords is not None and 

1814 textcoords != xycoords): 

1815 _api.warn_external("You have used the `textcoords` kwarg, but " 

1816 "not the `xytext` kwarg. This can lead to " 

1817 "surprising results.") 

1818 

1819 # clean up textcoords and assign default 

1820 if textcoords is None: 

1821 textcoords = self.xycoords 

1822 self._textcoords = textcoords 

1823 

1824 # cleanup xytext defaults 

1825 if xytext is None: 

1826 xytext = self.xy 

1827 x, y = xytext 

1828 

1829 self.arrowprops = arrowprops 

1830 if arrowprops is not None: 

1831 arrowprops = arrowprops.copy() 

1832 if "arrowstyle" in arrowprops: 

1833 self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5)) 

1834 else: 

1835 # modified YAArrow API to be used with FancyArrowPatch 

1836 for key in [ 

1837 'width', 'headwidth', 'headlength', 'shrink', 'frac']: 

1838 arrowprops.pop(key, None) 

1839 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops) 

1840 else: 

1841 self.arrow_patch = None 

1842 

1843 # Must come last, as some kwargs may be propagated to arrow_patch. 

1844 Text.__init__(self, x, y, text, **kwargs) 

1845 

1846 def contains(self, event): 

1847 inside, info = self._default_contains(event) 

1848 if inside is not None: 

1849 return inside, info 

1850 contains, tinfo = Text.contains(self, event) 

1851 if self.arrow_patch is not None: 

1852 in_patch, _ = self.arrow_patch.contains(event) 

1853 contains = contains or in_patch 

1854 return contains, tinfo 

1855 

1856 @property 

1857 def xycoords(self): 

1858 return self._xycoords 

1859 

1860 @xycoords.setter 

1861 def xycoords(self, xycoords): 

1862 def is_offset(s): 

1863 return isinstance(s, str) and s.startswith("offset") 

1864 

1865 if (isinstance(xycoords, tuple) and any(map(is_offset, xycoords)) 

1866 or is_offset(xycoords)): 

1867 raise ValueError("xycoords cannot be an offset coordinate") 

1868 self._xycoords = xycoords 

1869 

1870 @property 

1871 def xyann(self): 

1872 """ 

1873 The text position. 

1874 

1875 See also *xytext* in `.Annotation`. 

1876 """ 

1877 return self.get_position() 

1878 

1879 @xyann.setter 

1880 def xyann(self, xytext): 

1881 self.set_position(xytext) 

1882 

1883 def get_anncoords(self): 

1884 """ 

1885 Return the coordinate system to use for `.Annotation.xyann`. 

1886 

1887 See also *xycoords* in `.Annotation`. 

1888 """ 

1889 return self._textcoords 

1890 

1891 def set_anncoords(self, coords): 

1892 """ 

1893 Set the coordinate system to use for `.Annotation.xyann`. 

1894 

1895 See also *xycoords* in `.Annotation`. 

1896 """ 

1897 self._textcoords = coords 

1898 

1899 anncoords = property(get_anncoords, set_anncoords, doc=""" 

1900 The coordinate system to use for `.Annotation.xyann`.""") 

1901 

1902 def set_figure(self, fig): 

1903 # docstring inherited 

1904 if self.arrow_patch is not None: 

1905 self.arrow_patch.set_figure(fig) 

1906 Artist.set_figure(self, fig) 

1907 

1908 def update_positions(self, renderer): 

1909 """ 

1910 Update the pixel positions of the annotation text and the arrow patch. 

1911 """ 

1912 # generate transformation 

1913 self.set_transform(self._get_xy_transform(renderer, self.anncoords)) 

1914 

1915 arrowprops = self.arrowprops 

1916 if arrowprops is None: 

1917 return 

1918 

1919 bbox = Text.get_window_extent(self, renderer) 

1920 

1921 arrow_end = x1, y1 = self._get_position_xy(renderer) # Annotated pos. 

1922 

1923 ms = arrowprops.get("mutation_scale", self.get_size()) 

1924 self.arrow_patch.set_mutation_scale(ms) 

1925 

1926 if "arrowstyle" not in arrowprops: 

1927 # Approximately simulate the YAArrow. 

1928 shrink = arrowprops.get('shrink', 0.0) 

1929 width = arrowprops.get('width', 4) 

1930 headwidth = arrowprops.get('headwidth', 12) 

1931 if 'frac' in arrowprops: 

1932 _api.warn_external( 

1933 "'frac' option in 'arrowprops' is no longer supported;" 

1934 " use 'headlength' to set the head length in points.") 

1935 headlength = arrowprops.get('headlength', 12) 

1936 

1937 # NB: ms is in pts 

1938 stylekw = dict(head_length=headlength / ms, 

1939 head_width=headwidth / ms, 

1940 tail_width=width / ms) 

1941 

1942 self.arrow_patch.set_arrowstyle('simple', **stylekw) 

1943 

1944 # using YAArrow style: 

1945 # pick the corner of the text bbox closest to annotated point. 

1946 xpos = [(bbox.x0, 0), ((bbox.x0 + bbox.x1) / 2, 0.5), (bbox.x1, 1)] 

1947 ypos = [(bbox.y0, 0), ((bbox.y0 + bbox.y1) / 2, 0.5), (bbox.y1, 1)] 

1948 x, relposx = min(xpos, key=lambda v: abs(v[0] - x1)) 

1949 y, relposy = min(ypos, key=lambda v: abs(v[0] - y1)) 

1950 self._arrow_relpos = (relposx, relposy) 

1951 r = np.hypot(y - y1, x - x1) 

1952 shrink_pts = shrink * r / renderer.points_to_pixels(1) 

1953 self.arrow_patch.shrinkA = self.arrow_patch.shrinkB = shrink_pts 

1954 

1955 # adjust the starting point of the arrow relative to the textbox. 

1956 # TODO : Rotation needs to be accounted. 

1957 arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos 

1958 # The arrow is drawn from arrow_begin to arrow_end. It will be first 

1959 # clipped by patchA and patchB. Then it will be shrunk by shrinkA and 

1960 # shrinkB (in points). If patchA is not set, self.bbox_patch is used. 

1961 self.arrow_patch.set_positions(arrow_begin, arrow_end) 

1962 

1963 if "patchA" in arrowprops: 

1964 patchA = arrowprops["patchA"] 

1965 elif self._bbox_patch: 

1966 patchA = self._bbox_patch 

1967 elif self.get_text() == "": 

1968 patchA = None 

1969 else: 

1970 pad = renderer.points_to_pixels(4) 

1971 patchA = Rectangle( 

1972 xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), 

1973 width=bbox.width + pad, height=bbox.height + pad, 

1974 transform=IdentityTransform(), clip_on=False) 

1975 self.arrow_patch.set_patchA(patchA) 

1976 

1977 @artist.allow_rasterization 

1978 def draw(self, renderer): 

1979 # docstring inherited 

1980 if renderer is not None: 

1981 self._renderer = renderer 

1982 if not self.get_visible() or not self._check_xy(renderer): 

1983 return 

1984 # Update text positions before `Text.draw` would, so that the 

1985 # FancyArrowPatch is correctly positioned. 

1986 self.update_positions(renderer) 

1987 self.update_bbox_position_size(renderer) 

1988 if self.arrow_patch is not None: # FancyArrowPatch 

1989 if self.arrow_patch.figure is None and self.figure is not None: 

1990 self.arrow_patch.figure = self.figure 

1991 self.arrow_patch.draw(renderer) 

1992 # Draw text, including FancyBboxPatch, after FancyArrowPatch. 

1993 # Otherwise, a wedge arrowstyle can land partly on top of the Bbox. 

1994 Text.draw(self, renderer) 

1995 

1996 def get_window_extent(self, renderer=None): 

1997 # docstring inherited 

1998 # This block is the same as in Text.get_window_extent, but we need to 

1999 # set the renderer before calling update_positions(). 

2000 if not self.get_visible() or not self._check_xy(renderer): 

2001 return Bbox.unit() 

2002 if renderer is not None: 

2003 self._renderer = renderer 

2004 if self._renderer is None: 

2005 self._renderer = self.figure._get_renderer() 

2006 if self._renderer is None: 

2007 raise RuntimeError('Cannot get window extent w/o renderer') 

2008 

2009 self.update_positions(self._renderer) 

2010 

2011 text_bbox = Text.get_window_extent(self) 

2012 bboxes = [text_bbox] 

2013 

2014 if self.arrow_patch is not None: 

2015 bboxes.append(self.arrow_patch.get_window_extent()) 

2016 

2017 return Bbox.union(bboxes) 

2018 

2019 def get_tightbbox(self, renderer=None): 

2020 # docstring inherited 

2021 if not self._check_xy(renderer): 

2022 return Bbox.null() 

2023 return super().get_tightbbox(renderer) 

2024 

2025 

2026_docstring.interpd.update(Annotation=Annotation.__init__.__doc__)