Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/text.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

796 statements  

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, TextToPath # noqa # Logically located here 

19from .transforms import ( 

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

21 

22 

23_log = logging.getLogger(__name__) 

24 

25 

26def _get_textbox(text, renderer): 

27 """ 

28 Calculate the bounding box of the text. 

29 

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

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

32 """ 

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

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

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

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

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

38 

39 projected_xs = [] 

40 projected_ys = [] 

41 

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

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

44 

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

46 

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

48 w, h = wh 

49 

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

51 yt1 -= d 

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

53 

54 projected_xs.extend([xt1, xt2]) 

55 projected_ys.extend([yt1, yt2]) 

56 

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

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

59 

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

61 

62 return x_box, y_box, w_box, h_box 

63 

64 

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

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

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

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

69 return _get_text_metrics_with_cache_impl( 

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

71 

72 

73@functools.lru_cache(4096) 

74def _get_text_metrics_with_cache_impl( 

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

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

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

78 

79 

80@_docstring.interpd 

81@_api.define_aliases({ 

82 "color": ["c"], 

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

84 "fontfamily": ["family"], 

85 "fontname": ["name"], 

86 "fontsize": ["size"], 

87 "fontstretch": ["stretch"], 

88 "fontstyle": ["style"], 

89 "fontvariant": ["variant"], 

90 "fontweight": ["weight"], 

91 "horizontalalignment": ["ha"], 

92 "verticalalignment": ["va"], 

93 "multialignment": ["ma"], 

94}) 

95class Text(Artist): 

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

97 

98 zorder = 3 

99 _charsize_cache = dict() 

100 

101 def __repr__(self): 

102 return f"Text({self._x}, {self._y}, {self._text!r})" 

103 

104 def __init__(self, 

105 x=0, y=0, text='', *, 

106 color=None, # defaults to rc params 

107 verticalalignment='baseline', 

108 horizontalalignment='left', 

109 multialignment=None, 

110 fontproperties=None, # defaults to FontProperties() 

111 rotation=None, 

112 linespacing=None, 

113 rotation_mode=None, 

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

115 wrap=False, 

116 transform_rotates_text=False, 

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

118 antialiased=None, # defaults to rcParams['text.antialiased'] 

119 **kwargs 

120 ): 

121 """ 

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

123 

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

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

126 (default: 'baseline'). See also 

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

128 

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

130 added to the handles of a legend. 

131 

132 Valid keyword arguments are: 

133 

134 %(Text:kwdoc)s 

135 """ 

136 super().__init__() 

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

138 self._text = '' 

139 self._reset_visual_defaults( 

140 text=text, 

141 color=color, 

142 fontproperties=fontproperties, 

143 usetex=usetex, 

144 parse_math=parse_math, 

145 wrap=wrap, 

146 verticalalignment=verticalalignment, 

147 horizontalalignment=horizontalalignment, 

148 multialignment=multialignment, 

149 rotation=rotation, 

150 transform_rotates_text=transform_rotates_text, 

151 linespacing=linespacing, 

152 rotation_mode=rotation_mode, 

153 antialiased=antialiased 

154 ) 

155 self.update(kwargs) 

156 

157 def _reset_visual_defaults( 

158 self, 

159 text='', 

160 color=None, 

161 fontproperties=None, 

162 usetex=None, 

163 parse_math=None, 

164 wrap=False, 

165 verticalalignment='baseline', 

166 horizontalalignment='left', 

167 multialignment=None, 

168 rotation=None, 

169 transform_rotates_text=False, 

170 linespacing=None, 

171 rotation_mode=None, 

172 antialiased=None 

173 ): 

174 self.set_text(text) 

175 self.set_color(mpl._val_or_rc(color, "text.color")) 

176 self.set_fontproperties(fontproperties) 

177 self.set_usetex(usetex) 

178 self.set_parse_math(mpl._val_or_rc(parse_math, 'text.parse_math')) 

179 self.set_wrap(wrap) 

180 self.set_verticalalignment(verticalalignment) 

181 self.set_horizontalalignment(horizontalalignment) 

182 self._multialignment = multialignment 

183 self.set_rotation(rotation) 

184 self._transform_rotates_text = transform_rotates_text 

185 self._bbox_patch = None # a FancyBboxPatch instance 

186 self._renderer = None 

187 if linespacing is None: 

188 linespacing = 1.2 # Maybe use rcParam later. 

189 self.set_linespacing(linespacing) 

190 self.set_rotation_mode(rotation_mode) 

191 self.set_antialiased(antialiased if antialiased is not None else 

192 mpl.rcParams['text.antialiased']) 

193 

194 def update(self, kwargs): 

195 # docstring inherited 

196 ret = [] 

197 kwargs = cbook.normalize_kwargs(kwargs, Text) 

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

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

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

201 if fontproperties is not sentinel: 

202 ret.append(self.set_fontproperties(fontproperties)) 

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

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

205 ret.extend(super().update(kwargs)) 

206 if bbox is not sentinel: 

207 ret.append(self.set_bbox(bbox)) 

208 return ret 

209 

210 def __getstate__(self): 

211 d = super().__getstate__() 

212 # remove the cached _renderer (if it exists) 

213 d['_renderer'] = None 

214 return d 

215 

216 def contains(self, mouseevent): 

217 """ 

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

219 bounding-box of the text. 

220 """ 

221 if (self._different_canvas(mouseevent) or not self.get_visible() 

222 or self._renderer is None): 

223 return False, {} 

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

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

226 # accidentally cover the entire annotation bounding box. 

227 bbox = Text.get_window_extent(self) 

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

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

230 cattr = {} 

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

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

233 if self._bbox_patch: 

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

235 inside = inside or patch_inside 

236 cattr["bbox_patch"] = patch_cattr 

237 return inside, cattr 

238 

239 def _get_xy_display(self): 

240 """ 

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

242 """ 

243 x, y = self.get_unitless_position() 

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

245 

246 def _get_multialignment(self): 

247 if self._multialignment is not None: 

248 return self._multialignment 

249 else: 

250 return self._horizontalalignment 

251 

252 def _char_index_at(self, x): 

253 """ 

254 Calculate the index closest to the coordinate x in display space. 

255 

256 The position of text[index] is assumed to be the sum of the widths 

257 of all preceding characters text[:index]. 

258 

259 This works only on single line texts. 

260 """ 

261 if not self._text: 

262 return 0 

263 

264 text = self._text 

265 

266 fontproperties = str(self._fontproperties) 

267 if fontproperties not in Text._charsize_cache: 

268 Text._charsize_cache[fontproperties] = dict() 

269 

270 charsize_cache = Text._charsize_cache[fontproperties] 

271 for char in set(text): 

272 if char not in charsize_cache: 

273 self.set_text(char) 

274 bb = self.get_window_extent() 

275 charsize_cache[char] = bb.x1 - bb.x0 

276 

277 self.set_text(text) 

278 bb = self.get_window_extent() 

279 

280 size_accum = np.cumsum([0] + [charsize_cache[x] for x in text]) 

281 std_x = x - bb.x0 

282 return (np.abs(size_accum - std_x)).argmin() 

283 

284 def get_rotation(self): 

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

286 if self.get_transform_rotates_text(): 

287 return self.get_transform().transform_angles( 

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

289 else: 

290 return self._rotation 

291 

292 def get_transform_rotates_text(self): 

293 """ 

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

295 """ 

296 return self._transform_rotates_text 

297 

298 def set_rotation_mode(self, m): 

299 """ 

300 Set text rotation mode. 

301 

302 Parameters 

303 ---------- 

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

305 If ``"default"``, the text will be first rotated, then aligned according 

306 to their horizontal and vertical alignments. If ``"anchor"``, then 

307 alignment occurs before rotation. Passing ``None`` will set the rotation 

308 mode to ``"default"``. 

309 """ 

310 if m is None: 

311 m = "default" 

312 else: 

313 _api.check_in_list(("anchor", "default"), rotation_mode=m) 

314 self._rotation_mode = m 

315 self.stale = True 

316 

317 def get_rotation_mode(self): 

318 """Return the text rotation mode.""" 

319 return self._rotation_mode 

320 

321 def set_antialiased(self, antialiased): 

322 """ 

323 Set whether to use antialiased rendering. 

324 

325 Parameters 

326 ---------- 

327 antialiased : bool 

328 

329 Notes 

330 ----- 

331 Antialiasing will be determined by :rc:`text.antialiased` 

332 and the parameter *antialiased* will have no effect if the text contains 

333 math expressions. 

334 """ 

335 self._antialiased = antialiased 

336 self.stale = True 

337 

338 def get_antialiased(self): 

339 """Return whether antialiased rendering is used.""" 

340 return self._antialiased 

341 

342 def update_from(self, other): 

343 # docstring inherited 

344 super().update_from(other) 

345 self._color = other._color 

346 self._multialignment = other._multialignment 

347 self._verticalalignment = other._verticalalignment 

348 self._horizontalalignment = other._horizontalalignment 

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

350 self._usetex = other._usetex 

351 self._rotation = other._rotation 

352 self._transform_rotates_text = other._transform_rotates_text 

353 self._picker = other._picker 

354 self._linespacing = other._linespacing 

355 self._antialiased = other._antialiased 

356 self.stale = True 

357 

358 def _get_layout(self, renderer): 

359 """ 

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

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

362 of a rotated text when necessary. 

363 """ 

364 thisx, thisy = 0.0, 0.0 

365 lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty. 

366 

367 ws = [] 

368 hs = [] 

369 xs = [] 

370 ys = [] 

371 

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

373 _, lp_h, lp_d = _get_text_metrics_with_cache( 

374 renderer, "lp", self._fontproperties, 

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

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

377 

378 for i, line in enumerate(lines): 

379 clean_line, ismath = self._preprocess_math(line) 

380 if clean_line: 

381 w, h, d = _get_text_metrics_with_cache( 

382 renderer, clean_line, self._fontproperties, 

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

384 else: 

385 w = h = d = 0 

386 

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

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

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

390 h = max(h, lp_h) 

391 d = max(d, lp_d) 

392 

393 ws.append(w) 

394 hs.append(h) 

395 

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

397 baseline = (h - d) - thisy 

398 

399 if i == 0: 

400 # position at baseline 

401 thisy = -(h - d) 

402 else: 

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

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

405 

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

407 ys.append(thisy) 

408 

409 thisy -= d 

410 

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

412 descent = d 

413 

414 # Bounding box definition: 

415 width = max(ws) 

416 xmin = 0 

417 xmax = width 

418 ymax = 0 

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

420 

421 # get the rotation matrix 

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

423 

424 # now offset the individual text lines within the box 

425 malign = self._get_multialignment() 

426 if malign == 'left': 

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

428 elif malign == 'center': 

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

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

431 elif malign == 'right': 

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

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

434 

435 # the corners of the unrotated bounding box 

436 corners_horiz = np.array( 

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

438 

439 # now rotate the bbox 

440 corners_rotated = M.transform(corners_horiz) 

441 # compute the bounds of the rotated box 

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

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

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

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

446 width = xmax - xmin 

447 height = ymax - ymin 

448 

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

450 # bbox by alignment 

451 halign = self._horizontalalignment 

452 valign = self._verticalalignment 

453 

454 rotation_mode = self.get_rotation_mode() 

455 if rotation_mode != "anchor": 

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

457 # necessary to align the bbox with that location 

458 if halign == 'center': 

459 offsetx = (xmin + xmax) / 2 

460 elif halign == 'right': 

461 offsetx = xmax 

462 else: 

463 offsetx = xmin 

464 

465 if valign == 'center': 

466 offsety = (ymin + ymax) / 2 

467 elif valign == 'top': 

468 offsety = ymax 

469 elif valign == 'baseline': 

470 offsety = ymin + descent 

471 elif valign == 'center_baseline': 

472 offsety = ymin + height - baseline / 2.0 

473 else: 

474 offsety = ymin 

475 else: 

476 xmin1, ymin1 = corners_horiz[0] 

477 xmax1, ymax1 = corners_horiz[2] 

478 

479 if halign == 'center': 

480 offsetx = (xmin1 + xmax1) / 2.0 

481 elif halign == 'right': 

482 offsetx = xmax1 

483 else: 

484 offsetx = xmin1 

485 

486 if valign == 'center': 

487 offsety = (ymin1 + ymax1) / 2.0 

488 elif valign == 'top': 

489 offsety = ymax1 

490 elif valign == 'baseline': 

491 offsety = ymax1 - baseline 

492 elif valign == 'center_baseline': 

493 offsety = ymax1 - baseline / 2.0 

494 else: 

495 offsety = ymin1 

496 

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

498 

499 xmin -= offsetx 

500 ymin -= offsety 

501 

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

503 

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

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

506 

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

508 

509 def set_bbox(self, rectprops): 

510 """ 

511 Draw a bounding box around self. 

512 

513 Parameters 

514 ---------- 

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

516 The default boxstyle is 'square'. The mutation 

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

518 

519 Examples 

520 -------- 

521 :: 

522 

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

524 """ 

525 

526 if rectprops is not None: 

527 props = rectprops.copy() 

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

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

530 if boxstyle is None: 

531 boxstyle = "square" 

532 if pad is None: 

533 pad = 4 # points 

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

535 else: 

536 if pad is None: 

537 pad = 0.3 

538 # boxstyle could be a callable or a string 

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

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

541 self._bbox_patch = FancyBboxPatch( 

542 (0, 0), 1, 1, 

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

544 else: 

545 self._bbox_patch = None 

546 

547 self._update_clip_properties() 

548 

549 def get_bbox_patch(self): 

550 """ 

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

552 is not made. 

553 """ 

554 return self._bbox_patch 

555 

556 def update_bbox_position_size(self, renderer): 

557 """ 

558 Update the location and the size of the bbox. 

559 

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

561 to be updated before actually drawing the bbox. 

562 """ 

563 if self._bbox_patch: 

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

565 # position in Text: 

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

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

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

569 

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

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

572 self._bbox_patch.set_transform( 

573 Affine2D() 

574 .rotate_deg(self.get_rotation()) 

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

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

577 self._bbox_patch.set_mutation_scale(fontsize_in_pixel) 

578 

579 def _update_clip_properties(self): 

580 if self._bbox_patch: 

581 clipprops = dict(clip_box=self.clipbox, 

582 clip_path=self._clippath, 

583 clip_on=self._clipon) 

584 self._bbox_patch.update(clipprops) 

585 

586 def set_clip_box(self, clipbox): 

587 # docstring inherited. 

588 super().set_clip_box(clipbox) 

589 self._update_clip_properties() 

590 

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

592 # docstring inherited. 

593 super().set_clip_path(path, transform) 

594 self._update_clip_properties() 

595 

596 def set_clip_on(self, b): 

597 # docstring inherited. 

598 super().set_clip_on(b) 

599 self._update_clip_properties() 

600 

601 def get_wrap(self): 

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

603 return self._wrap 

604 

605 def set_wrap(self, wrap): 

606 """ 

607 Set whether the text can be wrapped. 

608 

609 Wrapping makes sure the text is confined to the (sub)figure box. It 

610 does not take into account any other artists. 

611 

612 Parameters 

613 ---------- 

614 wrap : bool 

615 

616 Notes 

617 ----- 

618 Wrapping does not work together with 

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

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

621 rescales the canvas to accommodate all content and happens before 

622 wrapping. 

623 """ 

624 self._wrap = wrap 

625 

626 def _get_wrap_line_width(self): 

627 """ 

628 Return the maximum line width for wrapping text based on the current 

629 orientation. 

630 """ 

631 x0, y0 = self.get_transform().transform(self.get_position()) 

632 figure_box = self.get_figure().get_window_extent() 

633 

634 # Calculate available width based on text alignment 

635 alignment = self.get_horizontalalignment() 

636 self.set_rotation_mode('anchor') 

637 rotation = self.get_rotation() 

638 

639 left = self._get_dist_to_box(rotation, x0, y0, figure_box) 

640 right = self._get_dist_to_box( 

641 (180 + rotation) % 360, x0, y0, figure_box) 

642 

643 if alignment == 'left': 

644 line_width = left 

645 elif alignment == 'right': 

646 line_width = right 

647 else: 

648 line_width = 2 * min(left, right) 

649 

650 return line_width 

651 

652 def _get_dist_to_box(self, rotation, x0, y0, figure_box): 

653 """ 

654 Return the distance from the given points to the boundaries of a 

655 rotated box, in pixels. 

656 """ 

657 if rotation > 270: 

658 quad = rotation - 270 

659 h1 = (y0 - figure_box.y0) / math.cos(math.radians(quad)) 

660 h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad)) 

661 elif rotation > 180: 

662 quad = rotation - 180 

663 h1 = (x0 - figure_box.x0) / math.cos(math.radians(quad)) 

664 h2 = (y0 - figure_box.y0) / math.cos(math.radians(90 - quad)) 

665 elif rotation > 90: 

666 quad = rotation - 90 

667 h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad)) 

668 h2 = (x0 - figure_box.x0) / math.cos(math.radians(90 - quad)) 

669 else: 

670 h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation)) 

671 h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation)) 

672 

673 return min(h1, h2) 

674 

675 def _get_rendered_text_width(self, text): 

676 """ 

677 Return the width of a given text string, in pixels. 

678 """ 

679 

680 w, h, d = self._renderer.get_text_width_height_descent( 

681 text, 

682 self.get_fontproperties(), 

683 cbook.is_math_text(text)) 

684 return math.ceil(w) 

685 

686 def _get_wrapped_text(self): 

687 """ 

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

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

690 """ 

691 if not self.get_wrap(): 

692 return self.get_text() 

693 

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

695 # ignore latex for now. 

696 if self.get_usetex(): 

697 return self.get_text() 

698 

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

700 line_width = self._get_wrap_line_width() 

701 wrapped_lines = [] 

702 

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

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

705 

706 # Now wrap each individual unwrapped line 

707 for unwrapped_line in unwrapped_lines: 

708 

709 sub_words = unwrapped_line.split(' ') 

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

711 while len(sub_words) > 0: 

712 if len(sub_words) == 1: 

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

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

715 continue 

716 

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

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

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

720 current_width = self._get_rendered_text_width(line) 

721 

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

723 # last word 

724 if current_width > line_width: 

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

726 sub_words = sub_words[i - 1:] 

727 break 

728 

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

730 elif i == len(sub_words): 

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

732 sub_words = [] 

733 break 

734 

735 return '\n'.join(wrapped_lines) 

736 

737 @artist.allow_rasterization 

738 def draw(self, renderer): 

739 # docstring inherited 

740 

741 if renderer is not None: 

742 self._renderer = renderer 

743 if not self.get_visible(): 

744 return 

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

746 return 

747 

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

749 

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

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

752 trans = self.get_transform() 

753 

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

755 # position in Text: 

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

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

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

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

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

761 return 

762 canvasw, canvash = renderer.get_canvas_width_height() 

763 

764 # Update the location and size of the bbox 

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

766 if self._bbox_patch: 

767 self.update_bbox_position_size(renderer) 

768 self._bbox_patch.draw(renderer) 

769 

770 gc = renderer.new_gc() 

771 gc.set_foreground(self.get_color()) 

772 gc.set_alpha(self.get_alpha()) 

773 gc.set_url(self._url) 

774 gc.set_antialiased(self._antialiased) 

775 self._set_gc_clip(gc) 

776 

777 angle = self.get_rotation() 

778 

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

780 

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

782 x = x + posx 

783 y = y + posy 

784 if renderer.flipy(): 

785 y = canvash - y 

786 clean_line, ismath = self._preprocess_math(line) 

787 

788 if self.get_path_effects(): 

789 from matplotlib.patheffects import PathEffectRenderer 

790 textrenderer = PathEffectRenderer( 

791 self.get_path_effects(), renderer) 

792 else: 

793 textrenderer = renderer 

794 

795 if self.get_usetex(): 

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

797 self._fontproperties, angle, 

798 mtext=mtext) 

799 else: 

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

801 self._fontproperties, angle, 

802 ismath=ismath, mtext=mtext) 

803 

804 gc.restore() 

805 renderer.close_group('text') 

806 self.stale = False 

807 

808 def get_color(self): 

809 """Return the color of the text.""" 

810 return self._color 

811 

812 def get_fontproperties(self): 

813 """Return the `.font_manager.FontProperties`.""" 

814 return self._fontproperties 

815 

816 def get_fontfamily(self): 

817 """ 

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

819 

820 See Also 

821 -------- 

822 .font_manager.FontProperties.get_family 

823 """ 

824 return self._fontproperties.get_family() 

825 

826 def get_fontname(self): 

827 """ 

828 Return the font name as a string. 

829 

830 See Also 

831 -------- 

832 .font_manager.FontProperties.get_name 

833 """ 

834 return self._fontproperties.get_name() 

835 

836 def get_fontstyle(self): 

837 """ 

838 Return the font style as a string. 

839 

840 See Also 

841 -------- 

842 .font_manager.FontProperties.get_style 

843 """ 

844 return self._fontproperties.get_style() 

845 

846 def get_fontsize(self): 

847 """ 

848 Return the font size as an integer. 

849 

850 See Also 

851 -------- 

852 .font_manager.FontProperties.get_size_in_points 

853 """ 

854 return self._fontproperties.get_size_in_points() 

855 

856 def get_fontvariant(self): 

857 """ 

858 Return the font variant as a string. 

859 

860 See Also 

861 -------- 

862 .font_manager.FontProperties.get_variant 

863 """ 

864 return self._fontproperties.get_variant() 

865 

866 def get_fontweight(self): 

867 """ 

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

869 

870 See Also 

871 -------- 

872 .font_manager.FontProperties.get_weight 

873 """ 

874 return self._fontproperties.get_weight() 

875 

876 def get_stretch(self): 

877 """ 

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

879 

880 See Also 

881 -------- 

882 .font_manager.FontProperties.get_stretch 

883 """ 

884 return self._fontproperties.get_stretch() 

885 

886 def get_horizontalalignment(self): 

887 """ 

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

889 'left', 'center' or 'right'. 

890 """ 

891 return self._horizontalalignment 

892 

893 def get_unitless_position(self): 

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

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

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

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

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

899 return x, y 

900 

901 def get_position(self): 

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

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

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

905 return self._x, self._y 

906 

907 def get_text(self): 

908 """Return the text string.""" 

909 return self._text 

910 

911 def get_verticalalignment(self): 

912 """ 

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

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

915 """ 

916 return self._verticalalignment 

917 

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

919 """ 

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

921 

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

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

924 

925 Parameters 

926 ---------- 

927 renderer : Renderer, optional 

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

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

930 necessary to pass this argument when calling `get_window_extent` 

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

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

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

934 

935 dpi : float, optional 

936 The dpi value for computing the bbox, defaults to 

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

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

939 """ 

940 if not self.get_visible(): 

941 return Bbox.unit() 

942 if dpi is None: 

943 dpi = self.figure.dpi 

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

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

946 tx, ty = self._get_xy_display() 

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

948 

949 if renderer is not None: 

950 self._renderer = renderer 

951 if self._renderer is None: 

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

953 if self._renderer is None: 

954 raise RuntimeError( 

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

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

957 

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

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

960 x, y = self.get_unitless_position() 

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

962 bbox = bbox.translated(x, y) 

963 return bbox 

964 

965 def set_backgroundcolor(self, color): 

966 """ 

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

968 

969 Parameters 

970 ---------- 

971 color : :mpltype:`color` 

972 

973 See Also 

974 -------- 

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

976 """ 

977 if self._bbox_patch is None: 

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

979 else: 

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

981 

982 self._update_clip_properties() 

983 self.stale = True 

984 

985 def set_color(self, color): 

986 """ 

987 Set the foreground color of the text 

988 

989 Parameters 

990 ---------- 

991 color : :mpltype:`color` 

992 """ 

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

994 # out at draw time for simplicity. 

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

996 mpl.colors._check_color_like(color=color) 

997 self._color = color 

998 self.stale = True 

999 

1000 def set_horizontalalignment(self, align): 

1001 """ 

1002 Set the horizontal alignment relative to the anchor point. 

1003 

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

1005 

1006 Parameters 

1007 ---------- 

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

1009 """ 

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

1011 self._horizontalalignment = align 

1012 self.stale = True 

1013 

1014 def set_multialignment(self, align): 

1015 """ 

1016 Set the text alignment for multiline texts. 

1017 

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

1019 horizontalalignment and verticalalignment properties. This property 

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

1021 

1022 Parameters 

1023 ---------- 

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

1025 """ 

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

1027 self._multialignment = align 

1028 self.stale = True 

1029 

1030 def set_linespacing(self, spacing): 

1031 """ 

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

1033 

1034 The default line spacing is 1.2. 

1035 

1036 Parameters 

1037 ---------- 

1038 spacing : float (multiple of font size) 

1039 """ 

1040 _api.check_isinstance(Real, spacing=spacing) 

1041 self._linespacing = spacing 

1042 self.stale = True 

1043 

1044 def set_fontfamily(self, fontname): 

1045 """ 

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

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

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

1049 names will be looked up in the corresponding rcParams. 

1050 

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

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

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

1054 `Text` instance. 

1055 

1056 Parameters 

1057 ---------- 

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

1059'monospace'} 

1060 

1061 See Also 

1062 -------- 

1063 .font_manager.FontProperties.set_family 

1064 """ 

1065 self._fontproperties.set_family(fontname) 

1066 self.stale = True 

1067 

1068 def set_fontvariant(self, variant): 

1069 """ 

1070 Set the font variant. 

1071 

1072 Parameters 

1073 ---------- 

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

1075 

1076 See Also 

1077 -------- 

1078 .font_manager.FontProperties.set_variant 

1079 """ 

1080 self._fontproperties.set_variant(variant) 

1081 self.stale = True 

1082 

1083 def set_fontstyle(self, fontstyle): 

1084 """ 

1085 Set the font style. 

1086 

1087 Parameters 

1088 ---------- 

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

1090 

1091 See Also 

1092 -------- 

1093 .font_manager.FontProperties.set_style 

1094 """ 

1095 self._fontproperties.set_style(fontstyle) 

1096 self.stale = True 

1097 

1098 def set_fontsize(self, fontsize): 

1099 """ 

1100 Set the font size. 

1101 

1102 Parameters 

1103 ---------- 

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

1105'large', 'x-large', 'xx-large'} 

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

1107 relative to the default font size. 

1108 

1109 See Also 

1110 -------- 

1111 .font_manager.FontProperties.set_size 

1112 """ 

1113 self._fontproperties.set_size(fontsize) 

1114 self.stale = True 

1115 

1116 def get_math_fontfamily(self): 

1117 """ 

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

1119 

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

1121 

1122 See Also 

1123 -------- 

1124 set_math_fontfamily 

1125 """ 

1126 return self._fontproperties.get_math_fontfamily() 

1127 

1128 def set_math_fontfamily(self, fontfamily): 

1129 """ 

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

1131 

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

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

1134 

1135 Parameters 

1136 ---------- 

1137 fontfamily : str 

1138 The name of the font family. 

1139 

1140 Available font families are defined in the 

1141 :ref:`default matplotlibrc file 

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

1143 

1144 See Also 

1145 -------- 

1146 get_math_fontfamily 

1147 """ 

1148 self._fontproperties.set_math_fontfamily(fontfamily) 

1149 

1150 def set_fontweight(self, weight): 

1151 """ 

1152 Set the font weight. 

1153 

1154 Parameters 

1155 ---------- 

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

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

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

1159 

1160 See Also 

1161 -------- 

1162 .font_manager.FontProperties.set_weight 

1163 """ 

1164 self._fontproperties.set_weight(weight) 

1165 self.stale = True 

1166 

1167 def set_fontstretch(self, stretch): 

1168 """ 

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

1170 

1171 Parameters 

1172 ---------- 

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

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

1175'expanded', 'extra-expanded', 'ultra-expanded'} 

1176 

1177 See Also 

1178 -------- 

1179 .font_manager.FontProperties.set_stretch 

1180 """ 

1181 self._fontproperties.set_stretch(stretch) 

1182 self.stale = True 

1183 

1184 def set_position(self, xy): 

1185 """ 

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

1187 

1188 Parameters 

1189 ---------- 

1190 xy : (float, float) 

1191 """ 

1192 self.set_x(xy[0]) 

1193 self.set_y(xy[1]) 

1194 

1195 def set_x(self, x): 

1196 """ 

1197 Set the *x* position of the text. 

1198 

1199 Parameters 

1200 ---------- 

1201 x : float 

1202 """ 

1203 self._x = x 

1204 self.stale = True 

1205 

1206 def set_y(self, y): 

1207 """ 

1208 Set the *y* position of the text. 

1209 

1210 Parameters 

1211 ---------- 

1212 y : float 

1213 """ 

1214 self._y = y 

1215 self.stale = True 

1216 

1217 def set_rotation(self, s): 

1218 """ 

1219 Set the rotation of the text. 

1220 

1221 Parameters 

1222 ---------- 

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

1224 The rotation angle in degrees in mathematically positive direction 

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

1226 """ 

1227 if isinstance(s, Real): 

1228 self._rotation = float(s) % 360 

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

1230 self._rotation = 0. 

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

1232 self._rotation = 90. 

1233 else: 

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

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

1236 self.stale = True 

1237 

1238 def set_transform_rotates_text(self, t): 

1239 """ 

1240 Whether rotations of the transform affect the text direction. 

1241 

1242 Parameters 

1243 ---------- 

1244 t : bool 

1245 """ 

1246 self._transform_rotates_text = t 

1247 self.stale = True 

1248 

1249 def set_verticalalignment(self, align): 

1250 """ 

1251 Set the vertical alignment relative to the anchor point. 

1252 

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

1254 

1255 Parameters 

1256 ---------- 

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

1258 """ 

1259 _api.check_in_list( 

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

1261 align=align) 

1262 self._verticalalignment = align 

1263 self.stale = True 

1264 

1265 def set_text(self, s): 

1266 r""" 

1267 Set the text string *s*. 

1268 

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

1270 

1271 Parameters 

1272 ---------- 

1273 s : object 

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

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

1276 """ 

1277 s = '' if s is None else str(s) 

1278 if s != self._text: 

1279 self._text = s 

1280 self.stale = True 

1281 

1282 def _preprocess_math(self, s): 

1283 """ 

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

1285 mathtext support needed. 

1286 

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

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

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

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

1291 flag True. 

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

1293 False. 

1294 """ 

1295 if self.get_usetex(): 

1296 if s == " ": 

1297 s = r"\ " 

1298 return s, "TeX" 

1299 elif not self.get_parse_math(): 

1300 return s, False 

1301 elif cbook.is_math_text(s): 

1302 return s, True 

1303 else: 

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

1305 

1306 def set_fontproperties(self, fp): 

1307 """ 

1308 Set the font properties that control the text. 

1309 

1310 Parameters 

1311 ---------- 

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

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

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

1315 absolute path to a font file. 

1316 """ 

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

1318 self.stale = True 

1319 

1320 @_docstring.kwarg_doc("bool, default: :rc:`text.usetex`") 

1321 def set_usetex(self, usetex): 

1322 """ 

1323 Parameters 

1324 ---------- 

1325 usetex : bool or None 

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

1327 :rc:`text.usetex`. 

1328 """ 

1329 if usetex is None: 

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

1331 else: 

1332 self._usetex = bool(usetex) 

1333 self.stale = True 

1334 

1335 def get_usetex(self): 

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

1337 return self._usetex 

1338 

1339 def set_parse_math(self, parse_math): 

1340 """ 

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

1342 

1343 Parameters 

1344 ---------- 

1345 parse_math : bool 

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

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

1348 """ 

1349 self._parse_math = bool(parse_math) 

1350 

1351 def get_parse_math(self): 

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

1353 return self._parse_math 

1354 

1355 def set_fontname(self, fontname): 

1356 """ 

1357 Alias for `set_fontfamily`. 

1358 

1359 One-way alias only: the getter differs. 

1360 

1361 Parameters 

1362 ---------- 

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

1364'monospace'} 

1365 

1366 See Also 

1367 -------- 

1368 .font_manager.FontProperties.set_family 

1369 

1370 """ 

1371 self.set_fontfamily(fontname) 

1372 

1373 

1374class OffsetFrom: 

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

1376 

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

1378 """ 

1379 Parameters 

1380 ---------- 

1381 artist : `~matplotlib.artist.Artist` or `.BboxBase` or `.Transform` 

1382 The object to compute the offset from. 

1383 

1384 ref_coord : (float, float) 

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

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

1387 *artist* bounding box. 

1388 

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

1390 transform applied to this value. 

1391 

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

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

1394 """ 

1395 self._artist = artist 

1396 x, y = ref_coord # Make copy when ref_coord is an array (and check the shape). 

1397 self._ref_coord = x, y 

1398 self.set_unit(unit) 

1399 

1400 def set_unit(self, unit): 

1401 """ 

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

1403 

1404 Parameters 

1405 ---------- 

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

1407 """ 

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

1409 self._unit = unit 

1410 

1411 def get_unit(self): 

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

1413 return self._unit 

1414 

1415 def __call__(self, renderer): 

1416 """ 

1417 Return the offset transform. 

1418 

1419 Parameters 

1420 ---------- 

1421 renderer : `RendererBase` 

1422 The renderer to use to compute the offset 

1423 

1424 Returns 

1425 ------- 

1426 `Transform` 

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

1428 relative to the given artist. 

1429 """ 

1430 if isinstance(self._artist, Artist): 

1431 bbox = self._artist.get_window_extent(renderer) 

1432 xf, yf = self._ref_coord 

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

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

1435 elif isinstance(self._artist, BboxBase): 

1436 bbox = self._artist 

1437 xf, yf = self._ref_coord 

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

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

1440 elif isinstance(self._artist, Transform): 

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

1442 else: 

1443 _api.check_isinstance((Artist, BboxBase, Transform), artist=self._artist) 

1444 scale = 1 if self._unit == "pixels" else renderer.points_to_pixels(1) 

1445 return Affine2D().scale(scale).translate(x, y) 

1446 

1447 

1448class _AnnotationBase: 

1449 def __init__(self, 

1450 xy, 

1451 xycoords='data', 

1452 annotation_clip=None): 

1453 

1454 x, y = xy # Make copy when xy is an array (and check the shape). 

1455 self.xy = x, y 

1456 self.xycoords = xycoords 

1457 self.set_annotation_clip(annotation_clip) 

1458 

1459 self._draggable = None 

1460 

1461 def _get_xy(self, renderer, xy, coords): 

1462 x, y = xy 

1463 xcoord, ycoord = coords if isinstance(coords, tuple) else (coords, coords) 

1464 if xcoord == 'data': 

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

1466 if ycoord == 'data': 

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

1468 return self._get_xy_transform(renderer, coords).transform((x, y)) 

1469 

1470 def _get_xy_transform(self, renderer, coords): 

1471 

1472 if isinstance(coords, tuple): 

1473 xcoord, ycoord = coords 

1474 from matplotlib.transforms import blended_transform_factory 

1475 tr1 = self._get_xy_transform(renderer, xcoord) 

1476 tr2 = self._get_xy_transform(renderer, ycoord) 

1477 return blended_transform_factory(tr1, tr2) 

1478 elif callable(coords): 

1479 tr = coords(renderer) 

1480 if isinstance(tr, BboxBase): 

1481 return BboxTransformTo(tr) 

1482 elif isinstance(tr, Transform): 

1483 return tr 

1484 else: 

1485 raise TypeError( 

1486 f"xycoords callable must return a BboxBase or Transform, not a " 

1487 f"{type(tr).__name__}") 

1488 elif isinstance(coords, Artist): 

1489 bbox = coords.get_window_extent(renderer) 

1490 return BboxTransformTo(bbox) 

1491 elif isinstance(coords, BboxBase): 

1492 return BboxTransformTo(coords) 

1493 elif isinstance(coords, Transform): 

1494 return coords 

1495 elif not isinstance(coords, str): 

1496 raise TypeError( 

1497 f"'xycoords' must be an instance of str, tuple[str, str], Artist, " 

1498 f"Transform, or Callable, not a {type(coords).__name__}") 

1499 

1500 if coords == 'data': 

1501 return self.axes.transData 

1502 elif coords == 'polar': 

1503 from matplotlib.projections import PolarAxes 

1504 tr = PolarAxes.PolarTransform(apply_theta_transforms=False) 

1505 trans = tr + self.axes.transData 

1506 return trans 

1507 

1508 try: 

1509 bbox_name, unit = coords.split() 

1510 except ValueError: # i.e. len(coords.split()) != 2. 

1511 raise ValueError(f"{coords!r} is not a valid coordinate") from None 

1512 

1513 bbox0, xy0 = None, None 

1514 

1515 # if unit is offset-like 

1516 if bbox_name == "figure": 

1517 bbox0 = self.figure.figbbox 

1518 elif bbox_name == "subfigure": 

1519 bbox0 = self.figure.bbox 

1520 elif bbox_name == "axes": 

1521 bbox0 = self.axes.bbox 

1522 

1523 # reference x, y in display coordinate 

1524 if bbox0 is not None: 

1525 xy0 = bbox0.p0 

1526 elif bbox_name == "offset": 

1527 xy0 = self._get_position_xy(renderer) 

1528 else: 

1529 raise ValueError(f"{coords!r} is not a valid coordinate") 

1530 

1531 if unit == "points": 

1532 tr = Affine2D().scale(self.figure.dpi / 72) # dpi/72 dots per point 

1533 elif unit == "pixels": 

1534 tr = Affine2D() 

1535 elif unit == "fontsize": 

1536 tr = Affine2D().scale(self.get_size() * self.figure.dpi / 72) 

1537 elif unit == "fraction": 

1538 tr = Affine2D().scale(*bbox0.size) 

1539 else: 

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

1541 

1542 return tr.translate(*xy0) 

1543 

1544 def set_annotation_clip(self, b): 

1545 """ 

1546 Set the annotation's clipping behavior. 

1547 

1548 Parameters 

1549 ---------- 

1550 b : bool or None 

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

1552 outside the Axes. 

1553 - False: The annotation will always be drawn. 

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

1555 outside the Axes and ``self.xycoords == "data"``. 

1556 """ 

1557 self._annotation_clip = b 

1558 

1559 def get_annotation_clip(self): 

1560 """ 

1561 Return the annotation's clipping behavior. 

1562 

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

1564 """ 

1565 return self._annotation_clip 

1566 

1567 def _get_position_xy(self, renderer): 

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

1569 return self._get_xy(renderer, self.xy, self.xycoords) 

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 use_blit : bool, default: False 

1592 Use blitting for faster image composition. For details see 

1593 :ref:`func-animation`. 

1594 

1595 Returns 

1596 ------- 

1597 DraggableAnnotation or None 

1598 If the annotation is draggable, the corresponding 

1599 `.DraggableAnnotation` helper is returned. 

1600 """ 

1601 from matplotlib.offsetbox import DraggableAnnotation 

1602 is_draggable = self._draggable is not None 

1603 

1604 # if state is None we'll toggle 

1605 if state is None: 

1606 state = not is_draggable 

1607 

1608 if state: 

1609 if self._draggable is None: 

1610 self._draggable = DraggableAnnotation(self, use_blit) 

1611 else: 

1612 if self._draggable is not None: 

1613 self._draggable.disconnect() 

1614 self._draggable = None 

1615 

1616 return self._draggable 

1617 

1618 

1619class Annotation(Text, _AnnotationBase): 

1620 """ 

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

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

1623 

1624 Attributes 

1625 ---------- 

1626 xy 

1627 The annotated position. 

1628 xycoords 

1629 The coordinate system for *xy*. 

1630 arrow_patch 

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

1632 """ 

1633 

1634 def __str__(self): 

1635 return f"Annotation({self.xy[0]:g}, {self.xy[1]:g}, {self._text!r})" 

1636 

1637 def __init__(self, text, xy, 

1638 xytext=None, 

1639 xycoords='data', 

1640 textcoords=None, 

1641 arrowprops=None, 

1642 annotation_clip=None, 

1643 **kwargs): 

1644 """ 

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

1646 

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

1648 

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

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

1651 be added by defining *arrowprops*. 

1652 

1653 Parameters 

1654 ---------- 

1655 text : str 

1656 The text of the annotation. 

1657 

1658 xy : (float, float) 

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

1660 by *xycoords*. 

1661 

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

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

1664 is determined by *textcoords*. 

1665 

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

1667callable, default: 'data' 

1668 

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

1670 of values are supported: 

1671 

1672 - One of the following strings: 

1673 

1674 ==================== ============================================ 

1675 Value Description 

1676 ==================== ============================================ 

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

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

1679 'figure fraction' Fraction of figure from lower left 

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

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

1682 'subfigure fraction' Fraction of subfigure from lower left 

1683 'axes points' Points from lower left corner of the Axes 

1684 'axes pixels' Pixels from lower left corner of the Axes 

1685 'axes fraction' Fraction of Axes from lower left 

1686 'data' Use the coordinate system of the object 

1687 being annotated (default) 

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

1689 coordinates 

1690 ==================== ============================================ 

1691 

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

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

1694 a subfigure can use 'subfigure pixels'. 

1695 

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

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

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

1699 center top of the bounding box. 

1700 

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

1702 

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

1704 

1705 def transform(renderer) -> Bbox 

1706 def transform(renderer) -> Transform 

1707 

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

1709 

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

1711 `.Transform` cases above. 

1712 

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

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

1715 of one of the above described types. 

1716 

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

1718 

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

1720or callable, default: value of *xycoords* 

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

1722 

1723 All *xycoords* values are valid as well as the following strings: 

1724 

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

1726 Value Description 

1727 ================= ================================================= 

1728 'offset points' Offset, in points, from the *xy* value 

1729 'offset pixels' Offset, in pixels, from the *xy* value 

1730 'offset fontsize' Offset, relative to fontsize, from the *xy* value 

1731 ================= ================================================= 

1732 

1733 arrowprops : dict, optional 

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

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

1736 drawn. 

1737 

1738 For historical reasons there are two different ways to specify 

1739 arrows, "simple" and "fancy": 

1740 

1741 **Simple arrow:** 

1742 

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

1744 allowed keys are: 

1745 

1746 ========== ================================================= 

1747 Key Description 

1748 ========== ================================================= 

1749 width The width of the arrow in points 

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

1751 headlength The length of the arrow head in points 

1752 shrink Fraction of total length to shrink from both ends 

1753 ? Any `.FancyArrowPatch` property 

1754 ========== ================================================= 

1755 

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

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

1758 

1759 **Fancy arrow:** 

1760 

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

1762 

1763 Valid keys are the following `.FancyArrowPatch` parameters: 

1764 

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

1766 Key Description 

1767 =============== =================================== 

1768 arrowstyle The arrow style 

1769 connectionstyle The connection style 

1770 relpos See below; default is (0.5, 0.5) 

1771 patchA Default is bounding box of the text 

1772 patchB Default is None 

1773 shrinkA In points. Default is 2 points 

1774 shrinkB In points. Default is 2 points 

1775 mutation_scale Default is text size (in points) 

1776 mutation_aspect Default is 1 

1777 ? Any `.FancyArrowPatch` property 

1778 =============== =================================== 

1779 

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

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

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

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

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

1785 is centered in the text box. 

1786 

1787 annotation_clip : bool or None, default: None 

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

1789 point *xy* is outside the Axes area. 

1790 

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

1792 the Axes. 

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

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

1795 the Axes and *xycoords* is 'data'. 

1796 

1797 **kwargs 

1798 Additional kwargs are passed to `.Text`. 

1799 

1800 Returns 

1801 ------- 

1802 `.Annotation` 

1803 

1804 See Also 

1805 -------- 

1806 :ref:`annotations` 

1807 

1808 """ 

1809 _AnnotationBase.__init__(self, 

1810 xy, 

1811 xycoords=xycoords, 

1812 annotation_clip=annotation_clip) 

1813 # warn about wonky input data 

1814 if (xytext is None and 

1815 textcoords is not None and 

1816 textcoords != xycoords): 

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

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

1819 "surprising results.") 

1820 

1821 # clean up textcoords and assign default 

1822 if textcoords is None: 

1823 textcoords = self.xycoords 

1824 self._textcoords = textcoords 

1825 

1826 # cleanup xytext defaults 

1827 if xytext is None: 

1828 xytext = self.xy 

1829 x, y = xytext 

1830 

1831 self.arrowprops = arrowprops 

1832 if arrowprops is not None: 

1833 arrowprops = arrowprops.copy() 

1834 if "arrowstyle" in arrowprops: 

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

1836 else: 

1837 # modified YAArrow API to be used with FancyArrowPatch 

1838 for key in ['width', 'headwidth', 'headlength', 'shrink']: 

1839 arrowprops.pop(key, None) 

1840 if 'frac' in arrowprops: 

1841 _api.warn_deprecated( 

1842 "3.8", name="the (unused) 'frac' key in 'arrowprops'") 

1843 arrowprops.pop("frac") 

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

1845 else: 

1846 self.arrow_patch = None 

1847 

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

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

1850 

1851 @_api.rename_parameter("3.8", "event", "mouseevent") 

1852 def contains(self, mouseevent): 

1853 if self._different_canvas(mouseevent): 

1854 return False, {} 

1855 contains, tinfo = Text.contains(self, mouseevent) 

1856 if self.arrow_patch is not None: 

1857 in_patch, _ = self.arrow_patch.contains(mouseevent) 

1858 contains = contains or in_patch 

1859 return contains, tinfo 

1860 

1861 @property 

1862 def xycoords(self): 

1863 return self._xycoords 

1864 

1865 @xycoords.setter 

1866 def xycoords(self, xycoords): 

1867 def is_offset(s): 

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

1869 

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

1871 or is_offset(xycoords)): 

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

1873 self._xycoords = xycoords 

1874 

1875 @property 

1876 def xyann(self): 

1877 """ 

1878 The text position. 

1879 

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

1881 """ 

1882 return self.get_position() 

1883 

1884 @xyann.setter 

1885 def xyann(self, xytext): 

1886 self.set_position(xytext) 

1887 

1888 def get_anncoords(self): 

1889 """ 

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

1891 

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

1893 """ 

1894 return self._textcoords 

1895 

1896 def set_anncoords(self, coords): 

1897 """ 

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

1899 

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

1901 """ 

1902 self._textcoords = coords 

1903 

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

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

1906 

1907 def set_figure(self, fig): 

1908 # docstring inherited 

1909 if self.arrow_patch is not None: 

1910 self.arrow_patch.set_figure(fig) 

1911 Artist.set_figure(self, fig) 

1912 

1913 def update_positions(self, renderer): 

1914 """ 

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

1916 """ 

1917 # generate transformation 

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

1919 

1920 arrowprops = self.arrowprops 

1921 if arrowprops is None: 

1922 return 

1923 

1924 bbox = Text.get_window_extent(self, renderer) 

1925 

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

1927 

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

1929 self.arrow_patch.set_mutation_scale(ms) 

1930 

1931 if "arrowstyle" not in arrowprops: 

1932 # Approximately simulate the YAArrow. 

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

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

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

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

1937 

1938 # NB: ms is in pts 

1939 stylekw = dict(head_length=headlength / ms, 

1940 head_width=headwidth / ms, 

1941 tail_width=width / ms) 

1942 

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

1944 

1945 # using YAArrow style: 

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

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

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

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

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

1951 self._arrow_relpos = (relposx, relposy) 

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

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

1954 self.arrow_patch.shrinkA = self.arrow_patch.shrinkB = shrink_pts 

1955 

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

1957 # TODO : Rotation needs to be accounted. 

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

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

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

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

1962 self.arrow_patch.set_positions(arrow_begin, arrow_end) 

1963 

1964 if "patchA" in arrowprops: 

1965 patchA = arrowprops["patchA"] 

1966 elif self._bbox_patch: 

1967 patchA = self._bbox_patch 

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

1969 patchA = None 

1970 else: 

1971 pad = renderer.points_to_pixels(4) 

1972 patchA = Rectangle( 

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

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

1975 transform=IdentityTransform(), clip_on=False) 

1976 self.arrow_patch.set_patchA(patchA) 

1977 

1978 @artist.allow_rasterization 

1979 def draw(self, renderer): 

1980 # docstring inherited 

1981 if renderer is not None: 

1982 self._renderer = renderer 

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

1984 return 

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

1986 # FancyArrowPatch is correctly positioned. 

1987 self.update_positions(renderer) 

1988 self.update_bbox_position_size(renderer) 

1989 if self.arrow_patch is not None: # FancyArrowPatch 

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

1991 self.arrow_patch.figure = self.figure 

1992 self.arrow_patch.draw(renderer) 

1993 # Draw text, including FancyBboxPatch, after FancyArrowPatch. 

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

1995 Text.draw(self, renderer) 

1996 

1997 def get_window_extent(self, renderer=None): 

1998 # docstring inherited 

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

2000 # set the renderer before calling update_positions(). 

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

2002 return Bbox.unit() 

2003 if renderer is not None: 

2004 self._renderer = renderer 

2005 if self._renderer is None: 

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

2007 if self._renderer is None: 

2008 raise RuntimeError('Cannot get window extent without renderer') 

2009 

2010 self.update_positions(self._renderer) 

2011 

2012 text_bbox = Text.get_window_extent(self) 

2013 bboxes = [text_bbox] 

2014 

2015 if self.arrow_patch is not None: 

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

2017 

2018 return Bbox.union(bboxes) 

2019 

2020 def get_tightbbox(self, renderer=None): 

2021 # docstring inherited 

2022 if not self._check_xy(renderer): 

2023 return Bbox.null() 

2024 return super().get_tightbbox(renderer) 

2025 

2026 

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