Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Classes for including text in a figure. 

3""" 

4 

5import contextlib 

6import logging 

7import math 

8import weakref 

9 

10import numpy as np 

11 

12from . import artist, cbook, docstring, rcParams 

13from .artist import Artist 

14from .font_manager import FontProperties 

15from .lines import Line2D 

16from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle 

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

18from .transforms import ( 

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

20 

21 

22_log = logging.getLogger(__name__) 

23 

24 

25@contextlib.contextmanager 

26def _wrap_text(textobj): 

27 """Temporarily inserts newlines to the text if the wrap option is enabled. 

28 """ 

29 if textobj.get_wrap(): 

30 old_text = textobj.get_text() 

31 try: 

32 textobj.set_text(textobj._get_wrapped_text()) 

33 yield textobj 

34 finally: 

35 textobj.set_text(old_text) 

36 else: 

37 yield textobj 

38 

39 

40# Extracted from Text's method to serve as a function 

41def get_rotation(rotation): 

42 """ 

43 Return the text angle as float between 0 and 360 degrees. 

44 

45 *rotation* may be 'horizontal', 'vertical', or a numeric value in degrees. 

46 """ 

47 try: 

48 return float(rotation) % 360 

49 except (ValueError, TypeError): 

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

51 return 0. 

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

53 return 90. 

54 else: 

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

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

57 .format(rotation)) 

58 

59 

60def _get_textbox(text, renderer): 

61 """ 

62 Calculate the bounding box of the text. Unlike 

63 :meth:`matplotlib.text.Text.get_extents` method, The bbox size of 

64 the text before the rotation is calculated. 

65 """ 

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

67 # matter of fact, The information from the _get_textbox function 

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

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

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

71 

72 projected_xs = [] 

73 projected_ys = [] 

74 

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

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

77 

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

79 

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

81 w, h = wh 

82 

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

84 yt1 -= d 

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

86 

87 projected_xs.extend([xt1, xt2]) 

88 projected_ys.extend([yt1, yt2]) 

89 

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

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

92 

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

94 

95 return x_box, y_box, w_box, h_box 

96 

97 

98@cbook._define_aliases({ 

99 "color": ["c"], 

100 "fontfamily": ["family"], 

101 "fontproperties": ["font_properties"], 

102 "horizontalalignment": ["ha"], 

103 "multialignment": ["ma"], 

104 "fontname": ["name"], 

105 "fontsize": ["size"], 

106 "fontstretch": ["stretch"], 

107 "fontstyle": ["style"], 

108 "fontvariant": ["variant"], 

109 "verticalalignment": ["va"], 

110 "fontweight": ["weight"], 

111}) 

112class Text(Artist): 

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

114 

115 zorder = 3 

116 _cached = cbook.maxdict(50) 

117 

118 def __repr__(self): 

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

120 

121 def __init__(self, 

122 x=0, y=0, text='', 

123 color=None, # defaults to rc params 

124 verticalalignment='baseline', 

125 horizontalalignment='left', 

126 multialignment=None, 

127 fontproperties=None, # defaults to FontProperties() 

128 rotation=None, 

129 linespacing=None, 

130 rotation_mode=None, 

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

132 wrap=False, 

133 **kwargs 

134 ): 

135 """ 

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

137 

138 Valid keyword arguments are: 

139 

140 %(Text)s 

141 """ 

142 Artist.__init__(self) 

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

144 

145 if color is None: 

146 color = rcParams['text.color'] 

147 if fontproperties is None: 

148 fontproperties = FontProperties() 

149 elif isinstance(fontproperties, str): 

150 fontproperties = FontProperties(fontproperties) 

151 

152 self._text = '' 

153 self.set_text(text) 

154 self.set_color(color) 

155 self.set_usetex(usetex) 

156 self.set_wrap(wrap) 

157 self.set_verticalalignment(verticalalignment) 

158 self.set_horizontalalignment(horizontalalignment) 

159 self._multialignment = multialignment 

160 self._rotation = rotation 

161 self._fontproperties = fontproperties 

162 self._bbox_patch = None # a FancyBboxPatch instance 

163 self._renderer = None 

164 if linespacing is None: 

165 linespacing = 1.2 # Maybe use rcParam later. 

166 self._linespacing = linespacing 

167 self.set_rotation_mode(rotation_mode) 

168 self.update(kwargs) 

169 

170 def update(self, kwargs): 

171 """ 

172 Update properties from a dictionary. 

173 """ 

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

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

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

177 super().update(kwargs) 

178 if bbox is not sentinel: 

179 self.set_bbox(bbox) 

180 

181 def __getstate__(self): 

182 d = super().__getstate__() 

183 # remove the cached _renderer (if it exists) 

184 d['_renderer'] = None 

185 return d 

186 

187 def contains(self, mouseevent): 

188 """Test whether the mouse event occurred in the patch. 

189 

190 In the case of text, a hit is true anywhere in the 

191 axis-aligned bounding-box containing the text. 

192 

193 Returns 

194 ------- 

195 bool : bool 

196 """ 

197 inside, info = self._default_contains(mouseevent) 

198 if inside is not None: 

199 return inside, info 

200 

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

202 return False, {} 

203 

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

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

206 # accidentally cover the entire annotation bounding box. 

207 l, b, w, h = Text.get_window_extent(self).bounds 

208 r, t = l + w, b + h 

209 

210 x, y = mouseevent.x, mouseevent.y 

211 inside = (l <= x <= r and b <= y <= t) 

212 cattr = {} 

213 

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

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

216 if self._bbox_patch: 

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

218 inside = inside or patch_inside 

219 cattr["bbox_patch"] = patch_cattr 

220 

221 return inside, cattr 

222 

223 def _get_xy_display(self): 

224 """ 

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

226 """ 

227 x, y = self.get_unitless_position() 

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

229 

230 def _get_multialignment(self): 

231 if self._multialignment is not None: 

232 return self._multialignment 

233 else: 

234 return self._horizontalalignment 

235 

236 def get_rotation(self): 

237 """Return the text angle as float in degrees.""" 

238 return get_rotation(self._rotation) # string_or_number -> number 

239 

240 def set_rotation_mode(self, m): 

241 """ 

242 Set text rotation mode. 

243 

244 Parameters 

245 ---------- 

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

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

248 aligned according to their horizontal and vertical alignments. If 

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

250 """ 

251 cbook._check_in_list(["anchor", "default", None], rotation_mode=m) 

252 self._rotation_mode = m 

253 self.stale = True 

254 

255 def get_rotation_mode(self): 

256 """Get the text rotation mode.""" 

257 return self._rotation_mode 

258 

259 def update_from(self, other): 

260 """Copy properties from other to self.""" 

261 Artist.update_from(self, other) 

262 self._color = other._color 

263 self._multialignment = other._multialignment 

264 self._verticalalignment = other._verticalalignment 

265 self._horizontalalignment = other._horizontalalignment 

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

267 self._rotation = other._rotation 

268 self._picker = other._picker 

269 self._linespacing = other._linespacing 

270 self.stale = True 

271 

272 def _get_layout(self, renderer): 

273 """ 

274 return the extent (bbox) of the text together with 

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

276 of a rotated text when necessary. 

277 """ 

278 key = self.get_prop_tup(renderer=renderer) 

279 if key in self._cached: 

280 return self._cached[key] 

281 

282 thisx, thisy = 0.0, 0.0 

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

284 

285 ws = [] 

286 hs = [] 

287 xs = [] 

288 ys = [] 

289 

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

291 _, lp_h, lp_d = renderer.get_text_width_height_descent( 

292 "lp", self._fontproperties, 

293 ismath="TeX" if self.get_usetex() else False) 

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

295 

296 for i, line in enumerate(lines): 

297 clean_line, ismath = self._preprocess_math(line) 

298 if clean_line: 

299 w, h, d = renderer.get_text_width_height_descent( 

300 clean_line, self._fontproperties, ismath=ismath) 

301 else: 

302 w = h = d = 0 

303 

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

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

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

307 h = max(h, lp_h) 

308 d = max(d, lp_d) 

309 

310 ws.append(w) 

311 hs.append(h) 

312 

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

314 baseline = (h - d) - thisy 

315 

316 if i == 0: 

317 # position at baseline 

318 thisy = -(h - d) 

319 else: 

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

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

322 

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

324 ys.append(thisy) 

325 

326 thisy -= d 

327 

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

329 descent = d 

330 

331 # Bounding box definition: 

332 width = max(ws) 

333 xmin = 0 

334 xmax = width 

335 ymax = 0 

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

337 height = ymax - ymin 

338 

339 # get the rotation matrix 

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

341 

342 # now offset the individual text lines within the box 

343 malign = self._get_multialignment() 

344 if malign == 'left': 

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

346 elif malign == 'center': 

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

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

349 elif malign == 'right': 

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

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

352 

353 # the corners of the unrotated bounding box 

354 corners_horiz = np.array( 

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

356 

357 # now rotate the bbox 

358 corners_rotated = M.transform(corners_horiz) 

359 # compute the bounds of the rotated box 

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

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

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

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

364 width = xmax - xmin 

365 height = ymax - ymin 

366 

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

368 # bbox by alignment 

369 halign = self._horizontalalignment 

370 valign = self._verticalalignment 

371 

372 rotation_mode = self.get_rotation_mode() 

373 if rotation_mode != "anchor": 

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

375 # necessary to align the bbox with that location 

376 if halign == 'center': 

377 offsetx = (xmin + xmax) / 2 

378 elif halign == 'right': 

379 offsetx = xmax 

380 else: 

381 offsetx = xmin 

382 

383 if valign == 'center': 

384 offsety = (ymin + ymax) / 2 

385 elif valign == 'top': 

386 offsety = ymax 

387 elif valign == 'baseline': 

388 offsety = ymin + descent 

389 elif valign == 'center_baseline': 

390 offsety = ymin + height - baseline / 2.0 

391 else: 

392 offsety = ymin 

393 else: 

394 xmin1, ymin1 = corners_horiz[0] 

395 xmax1, ymax1 = corners_horiz[2] 

396 

397 if halign == 'center': 

398 offsetx = (xmin1 + xmax1) / 2.0 

399 elif halign == 'right': 

400 offsetx = xmax1 

401 else: 

402 offsetx = xmin1 

403 

404 if valign == 'center': 

405 offsety = (ymin1 + ymax1) / 2.0 

406 elif valign == 'top': 

407 offsety = ymax1 

408 elif valign == 'baseline': 

409 offsety = ymax1 - baseline 

410 elif valign == 'center_baseline': 

411 offsety = ymax1 - baseline / 2.0 

412 else: 

413 offsety = ymin1 

414 

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

416 

417 xmin -= offsetx 

418 ymin -= offsety 

419 

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

421 

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

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

424 

425 ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent 

426 self._cached[key] = ret 

427 return ret 

428 

429 def set_bbox(self, rectprops): 

430 """ 

431 Draw a bounding box around self. 

432 

433 Parameters 

434 ---------- 

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

436 The default boxstyle is 'square'. The mutation 

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

438 

439 Examples 

440 -------- 

441 :: 

442 

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

444 """ 

445 

446 if rectprops is not None: 

447 props = rectprops.copy() 

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

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

450 if boxstyle is None: 

451 boxstyle = "square" 

452 if pad is None: 

453 pad = 4 # points 

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

455 else: 

456 if pad is None: 

457 pad = 0.3 

458 

459 # boxstyle could be a callable or a string 

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

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

462 

463 bbox_transmuter = props.pop("bbox_transmuter", None) 

464 

465 self._bbox_patch = FancyBboxPatch( 

466 (0., 0.), 

467 1., 1., 

468 boxstyle=boxstyle, 

469 bbox_transmuter=bbox_transmuter, 

470 transform=IdentityTransform(), 

471 **props) 

472 else: 

473 self._bbox_patch = None 

474 

475 self._update_clip_properties() 

476 

477 def get_bbox_patch(self): 

478 """ 

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

480 is not made. 

481 """ 

482 return self._bbox_patch 

483 

484 def update_bbox_position_size(self, renderer): 

485 """ 

486 Update the location and the size of the bbox. 

487 

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

489 to be updated before actually drawing the bbox. 

490 """ 

491 

492 if self._bbox_patch: 

493 

494 trans = self.get_transform() 

495 

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

497 # position in Text, and dash position in TextWithDash: 

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

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

500 

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

502 

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

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

505 theta = np.deg2rad(self.get_rotation()) 

506 tr = Affine2D().rotate(theta) 

507 tr = tr.translate(posx + x_box, posy + y_box) 

508 self._bbox_patch.set_transform(tr) 

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

510 self._bbox_patch.set_mutation_scale(fontsize_in_pixel) 

511 

512 def _draw_bbox(self, renderer, posx, posy): 

513 """ 

514 Update the location and size of the bbox (`.patches.FancyBboxPatch`), 

515 and draw. 

516 """ 

517 

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

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

520 theta = np.deg2rad(self.get_rotation()) 

521 tr = Affine2D().rotate(theta) 

522 tr = tr.translate(posx + x_box, posy + y_box) 

523 self._bbox_patch.set_transform(tr) 

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

525 self._bbox_patch.set_mutation_scale(fontsize_in_pixel) 

526 self._bbox_patch.draw(renderer) 

527 

528 def _update_clip_properties(self): 

529 clipprops = dict(clip_box=self.clipbox, 

530 clip_path=self._clippath, 

531 clip_on=self._clipon) 

532 if self._bbox_patch: 

533 self._bbox_patch.update(clipprops) 

534 

535 def set_clip_box(self, clipbox): 

536 # docstring inherited. 

537 super().set_clip_box(clipbox) 

538 self._update_clip_properties() 

539 

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

541 # docstring inherited. 

542 super().set_clip_path(path, transform) 

543 self._update_clip_properties() 

544 

545 def set_clip_on(self, b): 

546 # docstring inherited. 

547 super().set_clip_on(b) 

548 self._update_clip_properties() 

549 

550 def get_wrap(self): 

551 """Return the wrapping state for the text.""" 

552 return self._wrap 

553 

554 def set_wrap(self, wrap): 

555 """Set the wrapping state for the text. 

556 

557 Parameters 

558 ---------- 

559 wrap : bool 

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 with new lines added, so that 

625 the text is wrapped relative to the parent figure. 

626 """ 

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

628 # ignore latex for now. 

629 if self.get_usetex(): 

630 return self.get_text() 

631 

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

633 line_width = self._get_wrap_line_width() 

634 wrapped_lines = [] 

635 

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

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

638 

639 # Now wrap each individual unwrapped line 

640 for unwrapped_line in unwrapped_lines: 

641 

642 sub_words = unwrapped_line.split(' ') 

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

644 while len(sub_words) > 0: 

645 if len(sub_words) == 1: 

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

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

648 continue 

649 

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

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

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

653 current_width = self._get_rendered_text_width(line) 

654 

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

656 # last word 

657 if current_width > line_width: 

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

659 sub_words = sub_words[i - 1:] 

660 break 

661 

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

663 elif i == len(sub_words): 

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

665 sub_words = [] 

666 break 

667 

668 return '\n'.join(wrapped_lines) 

669 

670 @artist.allow_rasterization 

671 def draw(self, renderer): 

672 """ 

673 Draws the `.Text` object to the given *renderer*. 

674 """ 

675 if renderer is not None: 

676 self._renderer = renderer 

677 if not self.get_visible(): 

678 return 

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

680 return 

681 

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

683 

684 with _wrap_text(self) as textobj: 

685 bbox, info, descent = textobj._get_layout(renderer) 

686 trans = textobj.get_transform() 

687 

688 # don't use textobj.get_position here, which refers to text 

689 # position in Text, and dash position in TextWithDash: 

690 posx = float(textobj.convert_xunits(textobj._x)) 

691 posy = float(textobj.convert_yunits(textobj._y)) 

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

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

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

695 return 

696 canvasw, canvash = renderer.get_canvas_width_height() 

697 

698 # draw the FancyBboxPatch 

699 if textobj._bbox_patch: 

700 textobj._draw_bbox(renderer, posx, posy) 

701 

702 gc = renderer.new_gc() 

703 gc.set_foreground(textobj.get_color()) 

704 gc.set_alpha(textobj.get_alpha()) 

705 gc.set_url(textobj._url) 

706 textobj._set_gc_clip(gc) 

707 

708 angle = textobj.get_rotation() 

709 

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

711 

712 mtext = textobj if len(info) == 1 else None 

713 x = x + posx 

714 y = y + posy 

715 if renderer.flipy(): 

716 y = canvash - y 

717 clean_line, ismath = textobj._preprocess_math(line) 

718 

719 if textobj.get_path_effects(): 

720 from matplotlib.patheffects import PathEffectRenderer 

721 textrenderer = PathEffectRenderer( 

722 textobj.get_path_effects(), renderer) 

723 else: 

724 textrenderer = renderer 

725 

726 if textobj.get_usetex(): 

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

728 textobj._fontproperties, angle, 

729 mtext=mtext) 

730 else: 

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

732 textobj._fontproperties, angle, 

733 ismath=ismath, mtext=mtext) 

734 

735 gc.restore() 

736 renderer.close_group('text') 

737 self.stale = False 

738 

739 def get_color(self): 

740 "Return the color of the text" 

741 return self._color 

742 

743 def get_fontproperties(self): 

744 "Return the `.font_manager.FontProperties` object" 

745 return self._fontproperties 

746 

747 def get_fontfamily(self): 

748 """ 

749 Return the list of font families used for font lookup 

750 

751 See Also 

752 -------- 

753 .font_manager.FontProperties.get_family 

754 """ 

755 return self._fontproperties.get_family() 

756 

757 def get_fontname(self): 

758 """ 

759 Return the font name as string 

760 

761 See Also 

762 -------- 

763 .font_manager.FontProperties.get_name 

764 """ 

765 return self._fontproperties.get_name() 

766 

767 def get_fontstyle(self): 

768 """ 

769 Return the font style as string 

770 

771 See Also 

772 -------- 

773 .font_manager.FontProperties.get_style 

774 """ 

775 return self._fontproperties.get_style() 

776 

777 def get_fontsize(self): 

778 """ 

779 Return the font size as integer 

780 

781 See Also 

782 -------- 

783 .font_manager.FontProperties.get_size_in_points 

784 """ 

785 return self._fontproperties.get_size_in_points() 

786 

787 def get_fontvariant(self): 

788 """ 

789 Return the font variant as a string 

790 

791 See Also 

792 -------- 

793 .font_manager.FontProperties.get_variant 

794 """ 

795 return self._fontproperties.get_variant() 

796 

797 def get_fontweight(self): 

798 """ 

799 Get the font weight as string or number 

800 

801 See Also 

802 -------- 

803 .font_manager.FontProperties.get_weight 

804 """ 

805 return self._fontproperties.get_weight() 

806 

807 def get_stretch(self): 

808 """ 

809 Get the font stretch as a string or number 

810 

811 See Also 

812 -------- 

813 .font_manager.FontProperties.get_stretch 

814 """ 

815 return self._fontproperties.get_stretch() 

816 

817 def get_horizontalalignment(self): 

818 """ 

819 Return the horizontal alignment as string. Will be one of 

820 'left', 'center' or 'right'. 

821 """ 

822 return self._horizontalalignment 

823 

824 def get_unitless_position(self): 

825 "Return the unitless position of the text as a tuple (*x*, *y*)" 

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

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

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

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

830 return x, y 

831 

832 def get_position(self): 

833 "Return the position of the text as a tuple (*x*, *y*)" 

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

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

836 return self._x, self._y 

837 

838 def get_prop_tup(self, renderer=None): 

839 """ 

840 Return a hashable tuple of properties. 

841 

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

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

844 need to know if the text has changed. 

845 """ 

846 x, y = self.get_unitless_position() 

847 renderer = renderer or self._renderer 

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

849 self._verticalalignment, self._horizontalalignment, 

850 hash(self._fontproperties), 

851 self._rotation, self._rotation_mode, 

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

853 self._linespacing 

854 ) 

855 

856 def get_text(self): 

857 "Get the text as string" 

858 return self._text 

859 

860 def get_verticalalignment(self): 

861 """ 

862 Return the vertical alignment as string. Will be one of 

863 'top', 'center', 'bottom' or 'baseline'. 

864 """ 

865 return self._verticalalignment 

866 

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

868 """ 

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

870 

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

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

873 

874 Parameters 

875 ---------- 

876 renderer : Renderer, optional 

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

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

879 necessary to pass this argument when calling `get_window_extent` 

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

881 trigger a draw first (e.g. by saving the figure). 

882 

883 dpi : float, optional 

884 The dpi value for computing the bbox, defaults to 

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

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

887 """ 

888 #return _unit_box 

889 if not self.get_visible(): 

890 return Bbox.unit() 

891 if dpi is not None: 

892 dpi_orig = self.figure.dpi 

893 self.figure.dpi = dpi 

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

895 tx, ty = self._get_xy_display() 

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

897 

898 if renderer is not None: 

899 self._renderer = renderer 

900 if self._renderer is None: 

901 self._renderer = self.figure._cachedRenderer 

902 if self._renderer is None: 

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

904 

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

906 x, y = self.get_unitless_position() 

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

908 bbox = bbox.translated(x, y) 

909 if dpi is not None: 

910 self.figure.dpi = dpi_orig 

911 return bbox 

912 

913 def set_backgroundcolor(self, color): 

914 """ 

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

916 

917 Parameters 

918 ---------- 

919 color : color 

920 

921 See Also 

922 -------- 

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

924 """ 

925 if self._bbox_patch is None: 

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

927 else: 

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

929 

930 self._update_clip_properties() 

931 self.stale = True 

932 

933 def set_color(self, color): 

934 """ 

935 Set the foreground color of the text 

936 

937 Parameters 

938 ---------- 

939 color : color 

940 """ 

941 # Make sure it is hashable, or get_prop_tup will fail. 

942 try: 

943 hash(color) 

944 except TypeError: 

945 color = tuple(color) 

946 self._color = color 

947 self.stale = True 

948 

949 def set_horizontalalignment(self, align): 

950 """ 

951 Set the horizontal alignment to one of 

952 

953 Parameters 

954 ---------- 

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

956 """ 

957 cbook._check_in_list(['center', 'right', 'left'], align=align) 

958 self._horizontalalignment = align 

959 self.stale = True 

960 

961 def set_multialignment(self, align): 

962 """ 

963 Set the alignment for multiple lines layout. The layout of the 

964 bounding box of all the lines is determined by the horizontalalignment 

965 and verticalalignment properties, but the multiline text within that 

966 box can be 

967 

968 Parameters 

969 ---------- 

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

971 """ 

972 cbook._check_in_list(['center', 'right', 'left'], align=align) 

973 self._multialignment = align 

974 self.stale = True 

975 

976 def set_linespacing(self, spacing): 

977 """ 

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

979 Default is 1.2. 

980 

981 Parameters 

982 ---------- 

983 spacing : float (multiple of font size) 

984 """ 

985 self._linespacing = spacing 

986 self.stale = True 

987 

988 def set_fontfamily(self, fontname): 

989 """ 

990 Set the font family. May be either a single string, or a list of 

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

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

993 names will be looked up in the corresponding rcParams. 

994 

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

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

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

998 `Text` instance. 

999 

1000 Parameters 

1001 ---------- 

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

1003'monospace'} 

1004 

1005 See Also 

1006 -------- 

1007 .font_manager.FontProperties.set_family 

1008 """ 

1009 self._fontproperties.set_family(fontname) 

1010 self.stale = True 

1011 

1012 def set_fontvariant(self, variant): 

1013 """ 

1014 Set the font variant, either 'normal' or 'small-caps'. 

1015 

1016 Parameters 

1017 ---------- 

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

1019 

1020 See Also 

1021 -------- 

1022 .font_manager.FontProperties.set_variant 

1023 """ 

1024 self._fontproperties.set_variant(variant) 

1025 self.stale = True 

1026 

1027 def set_fontstyle(self, fontstyle): 

1028 """ 

1029 Set the font style. 

1030 

1031 Parameters 

1032 ---------- 

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

1034 

1035 See Also 

1036 -------- 

1037 .font_manager.FontProperties.set_style 

1038 """ 

1039 self._fontproperties.set_style(fontstyle) 

1040 self.stale = True 

1041 

1042 def set_fontsize(self, fontsize): 

1043 """ 

1044 Set the font size. May be either a size string, relative to 

1045 the default font size, or an absolute font size in points. 

1046 

1047 Parameters 

1048 ---------- 

1049 fontsize : {size in points, 'xx-small', 'x-small', 'small', 'medium', \ 

1050'large', 'x-large', 'xx-large'} 

1051 

1052 See Also 

1053 -------- 

1054 .font_manager.FontProperties.set_size 

1055 """ 

1056 self._fontproperties.set_size(fontsize) 

1057 self.stale = True 

1058 

1059 def set_fontweight(self, weight): 

1060 """ 

1061 Set the font weight. 

1062 

1063 Parameters 

1064 ---------- 

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

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

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

1068 

1069 See Also 

1070 -------- 

1071 .font_manager.FontProperties.set_weight 

1072 """ 

1073 self._fontproperties.set_weight(weight) 

1074 self.stale = True 

1075 

1076 def set_fontstretch(self, stretch): 

1077 """ 

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

1079 

1080 Parameters 

1081 ---------- 

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

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

1084'expanded', 'extra-expanded', 'ultra-expanded'} 

1085 

1086 See Also 

1087 -------- 

1088 .font_manager.FontProperties.set_stretch 

1089 """ 

1090 self._fontproperties.set_stretch(stretch) 

1091 self.stale = True 

1092 

1093 def set_position(self, xy): 

1094 """ 

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

1096 

1097 Parameters 

1098 ---------- 

1099 xy : (float, float) 

1100 """ 

1101 self.set_x(xy[0]) 

1102 self.set_y(xy[1]) 

1103 

1104 def set_x(self, x): 

1105 """ 

1106 Set the *x* position of the text. 

1107 

1108 Parameters 

1109 ---------- 

1110 x : float 

1111 """ 

1112 self._x = x 

1113 self.stale = True 

1114 

1115 def set_y(self, y): 

1116 """ 

1117 Set the *y* position of the text. 

1118 

1119 Parameters 

1120 ---------- 

1121 y : float 

1122 """ 

1123 self._y = y 

1124 self.stale = True 

1125 

1126 def set_rotation(self, s): 

1127 """ 

1128 Set the rotation of the text. 

1129 

1130 Parameters 

1131 ---------- 

1132 s : {angle in degrees, 'vertical', 'horizontal'} 

1133 """ 

1134 self._rotation = s 

1135 self.stale = True 

1136 

1137 def set_verticalalignment(self, align): 

1138 """ 

1139 Set the vertical alignment 

1140 

1141 Parameters 

1142 ---------- 

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

1144 """ 

1145 cbook._check_in_list( 

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

1147 align=align) 

1148 self._verticalalignment = align 

1149 self.stale = True 

1150 

1151 def set_text(self, s): 

1152 r""" 

1153 Set the text string *s*. 

1154 

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

1156 

1157 Parameters 

1158 ---------- 

1159 s : object 

1160 Any object gets converted to its `str`, except ``None`` which 

1161 becomes ``''``. 

1162 """ 

1163 if s is None: 

1164 s = '' 

1165 if s != self._text: 

1166 self._text = str(s) 

1167 self.stale = True 

1168 

1169 @staticmethod 

1170 @cbook.deprecated("3.1") 

1171 def is_math_text(s, usetex=None): 

1172 """ 

1173 Returns a cleaned string and a boolean flag. 

1174 The flag indicates if the given string *s* contains any mathtext, 

1175 determined by counting unescaped dollar signs. If no mathtext 

1176 is present, the cleaned string has its dollar signs unescaped. 

1177 If usetex is on, the flag always has the value "TeX". 

1178 """ 

1179 # Did we find an even number of non-escaped dollar signs? 

1180 # If so, treat is as math text. 

1181 if usetex is None: 

1182 usetex = rcParams['text.usetex'] 

1183 if usetex: 

1184 if s == ' ': 

1185 s = r'\ ' 

1186 return s, 'TeX' 

1187 

1188 if cbook.is_math_text(s): 

1189 return s, True 

1190 else: 

1191 return s.replace(r'\$', '$'), False 

1192 

1193 def _preprocess_math(self, s): 

1194 """ 

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

1196 mathtext support needed. 

1197 

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

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

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

1201 signs), return *s* and the flag True. 

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

1203 False. 

1204 """ 

1205 if self.get_usetex(): 

1206 if s == " ": 

1207 s = r"\ " 

1208 return s, "TeX" 

1209 elif cbook.is_math_text(s): 

1210 return s, True 

1211 else: 

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

1213 

1214 def set_fontproperties(self, fp): 

1215 """ 

1216 Set the font properties that control the text. 

1217 

1218 Parameters 

1219 ---------- 

1220 fp : `.font_manager.FontProperties` 

1221 """ 

1222 if isinstance(fp, str): 

1223 fp = FontProperties(fp) 

1224 self._fontproperties = fp.copy() 

1225 self.stale = True 

1226 

1227 def set_usetex(self, usetex): 

1228 """ 

1229 Parameters 

1230 ---------- 

1231 usetex : bool or None 

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

1233 :rc:`text.usetex`. 

1234 """ 

1235 if usetex is None: 

1236 self._usetex = rcParams['text.usetex'] 

1237 else: 

1238 self._usetex = bool(usetex) 

1239 self.stale = True 

1240 

1241 def get_usetex(self): 

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

1243 return self._usetex 

1244 

1245 def set_fontname(self, fontname): 

1246 """ 

1247 Alias for `set_family`. 

1248 

1249 One-way alias only: the getter differs. 

1250 

1251 Parameters 

1252 ---------- 

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

1254'monospace'} 

1255 

1256 See Also 

1257 -------- 

1258 .font_manager.FontProperties.set_family 

1259 

1260 """ 

1261 return self.set_family(fontname) 

1262 

1263 

1264docstring.interpd.update(Text=artist.kwdoc(Text)) 

1265docstring.dedent_interpd(Text.__init__) 

1266 

1267 

1268@cbook.deprecated("3.1", alternative="Annotation") 

1269class TextWithDash(Text): 

1270 """ 

1271 This is basically a :class:`~matplotlib.text.Text` with a dash 

1272 (drawn with a :class:`~matplotlib.lines.Line2D`) before/after 

1273 it. It is intended to be a drop-in replacement for 

1274 :class:`~matplotlib.text.Text`, and should behave identically to 

1275 it when *dashlength* = 0.0. 

1276 

1277 The dash always comes between the point specified by 

1278 :meth:`~matplotlib.text.Text.set_position` and the text. When a 

1279 dash exists, the text alignment arguments (*horizontalalignment*, 

1280 *verticalalignment*) are ignored. 

1281 

1282 *dashlength* is the length of the dash in canvas units. 

1283 (default = 0.0). 

1284 

1285 *dashdirection* is one of 0 or 1, where 0 draws the dash after the 

1286 text and 1 before. (default = 0). 

1287 

1288 *dashrotation* specifies the rotation of the dash, and should 

1289 generally stay *None*. In this case 

1290 :meth:`~matplotlib.text.TextWithDash.get_dashrotation` returns 

1291 :meth:`~matplotlib.text.Text.get_rotation`. (i.e., the dash takes 

1292 its rotation from the text's rotation). Because the text center is 

1293 projected onto the dash, major deviations in the rotation cause 

1294 what may be considered visually unappealing results. 

1295 (default = *None*) 

1296 

1297 *dashpad* is a padding length to add (or subtract) space 

1298 between the text and the dash, in canvas units. 

1299 (default = 3) 

1300 

1301 *dashpush* "pushes" the dash and text away from the point 

1302 specified by :meth:`~matplotlib.text.Text.set_position` by the 

1303 amount in canvas units. (default = 0) 

1304 

1305 .. note:: 

1306 

1307 The alignment of the two objects is based on the bounding box 

1308 of the :class:`~matplotlib.text.Text`, as obtained by 

1309 :meth:`~matplotlib.artist.Artist.get_window_extent`. This, in 

1310 turn, appears to depend on the font metrics as given by the 

1311 rendering backend. Hence the quality of the "centering" of the 

1312 label text with respect to the dash varies depending on the 

1313 backend used. 

1314 

1315 .. note:: 

1316 

1317 I'm not sure that I got the 

1318 :meth:`~matplotlib.text.TextWithDash.get_window_extent` right, 

1319 or whether that's sufficient for providing the object bounding 

1320 box. 

1321 

1322 """ 

1323 __name__ = 'textwithdash' 

1324 

1325 def __str__(self): 

1326 return "TextWithDash(%g, %g, %r)" % (self._x, self._y, self._text) 

1327 

1328 def __init__(self, 

1329 x=0, y=0, text='', 

1330 color=None, # defaults to rc params 

1331 verticalalignment='center', 

1332 horizontalalignment='center', 

1333 multialignment=None, 

1334 fontproperties=None, # defaults to FontProperties() 

1335 rotation=None, 

1336 linespacing=None, 

1337 dashlength=0.0, 

1338 dashdirection=0, 

1339 dashrotation=None, 

1340 dashpad=3, 

1341 dashpush=0, 

1342 ): 

1343 

1344 Text.__init__(self, x=x, y=y, text=text, color=color, 

1345 verticalalignment=verticalalignment, 

1346 horizontalalignment=horizontalalignment, 

1347 multialignment=multialignment, 

1348 fontproperties=fontproperties, 

1349 rotation=rotation, 

1350 linespacing=linespacing, 

1351 ) 

1352 

1353 # The position (x, y) values for text and dashline 

1354 # are bogus as given in the instantiation; they will 

1355 # be set correctly by update_coords() in draw() 

1356 

1357 self.dashline = Line2D(xdata=(x, x), 

1358 ydata=(y, y), 

1359 color='k', 

1360 linestyle='-') 

1361 

1362 self._dashx = float(x) 

1363 self._dashy = float(y) 

1364 self._dashlength = dashlength 

1365 self._dashdirection = dashdirection 

1366 self._dashrotation = dashrotation 

1367 self._dashpad = dashpad 

1368 self._dashpush = dashpush 

1369 

1370 #self.set_bbox(dict(pad=0)) 

1371 

1372 def get_unitless_position(self): 

1373 "Return the unitless position of the text as a tuple (*x*, *y*)" 

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

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

1376 x = float(self.convert_xunits(self._dashx)) 

1377 y = float(self.convert_yunits(self._dashy)) 

1378 return x, y 

1379 

1380 def get_position(self): 

1381 "Return the position of the text as a tuple (*x*, *y*)" 

1382 # This should return the same data (possibly unitized) as was 

1383 # specified with set_x and set_y 

1384 return self._dashx, self._dashy 

1385 

1386 def get_prop_tup(self, renderer=None): 

1387 """ 

1388 Return a hashable tuple of properties. 

1389 

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

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

1392 need to know if the text has changed. 

1393 """ 

1394 return (*Text.get_prop_tup(self, renderer=renderer), 

1395 self._x, self._y, self._dashlength, self._dashdirection, 

1396 self._dashrotation, self._dashpad, self._dashpush) 

1397 

1398 def draw(self, renderer): 

1399 """ 

1400 Draw the :class:`TextWithDash` object to the given *renderer*. 

1401 """ 

1402 self.update_coords(renderer) 

1403 Text.draw(self, renderer) 

1404 if self.get_dashlength() > 0.0: 

1405 self.dashline.draw(renderer) 

1406 self.stale = False 

1407 

1408 def update_coords(self, renderer): 

1409 """ 

1410 Computes the actual *x*, *y* coordinates for text based on the 

1411 input *x*, *y* and the *dashlength*. Since the rotation is 

1412 with respect to the actual canvas's coordinates we need to map 

1413 back and forth. 

1414 """ 

1415 dashx, dashy = self.get_unitless_position() 

1416 dashlength = self.get_dashlength() 

1417 # Shortcircuit this process if we don't have a dash 

1418 if dashlength == 0.0: 

1419 self._x, self._y = dashx, dashy 

1420 return 

1421 

1422 dashrotation = self.get_dashrotation() 

1423 dashdirection = self.get_dashdirection() 

1424 dashpad = self.get_dashpad() 

1425 dashpush = self.get_dashpush() 

1426 

1427 angle = get_rotation(dashrotation) 

1428 theta = np.pi * (angle / 180.0 + dashdirection - 1) 

1429 cos_theta, sin_theta = np.cos(theta), np.sin(theta) 

1430 

1431 transform = self.get_transform() 

1432 

1433 # Compute the dash end points 

1434 # The 'c' prefix is for canvas coordinates 

1435 cxy = transform.transform((dashx, dashy)) 

1436 cd = np.array([cos_theta, sin_theta]) 

1437 c1 = cxy + dashpush * cd 

1438 c2 = cxy + (dashpush + dashlength) * cd 

1439 

1440 inverse = transform.inverted() 

1441 (x1, y1), (x2, y2) = inverse.transform([c1, c2]) 

1442 self.dashline.set_data((x1, x2), (y1, y2)) 

1443 

1444 # We now need to extend this vector out to 

1445 # the center of the text area. 

1446 # The basic problem here is that we're "rotating" 

1447 # two separate objects but want it to appear as 

1448 # if they're rotated together. 

1449 # This is made non-trivial because of the 

1450 # interaction between text rotation and alignment - 

1451 # text alignment is based on the bbox after rotation. 

1452 # We reset/force both alignments to 'center' 

1453 # so we can do something relatively reasonable. 

1454 # There's probably a better way to do this by 

1455 # embedding all this in the object's transformations, 

1456 # but I don't grok the transformation stuff 

1457 # well enough yet. 

1458 we = Text.get_window_extent(self, renderer=renderer) 

1459 w, h = we.width, we.height 

1460 # Watch for zeros 

1461 if sin_theta == 0.0: 

1462 dx = w 

1463 dy = 0.0 

1464 elif cos_theta == 0.0: 

1465 dx = 0.0 

1466 dy = h 

1467 else: 

1468 tan_theta = sin_theta / cos_theta 

1469 dx = w 

1470 dy = w * tan_theta 

1471 if dy > h or dy < -h: 

1472 dy = h 

1473 dx = h / tan_theta 

1474 cwd = np.array([dx, dy]) / 2 

1475 cwd *= 1 + dashpad / np.sqrt(np.dot(cwd, cwd)) 

1476 cw = c2 + (dashdirection * 2 - 1) * cwd 

1477 

1478 self._x, self._y = inverse.transform(cw) 

1479 

1480 # Now set the window extent 

1481 # I'm not at all sure this is the right way to do this. 

1482 we = Text.get_window_extent(self, renderer=renderer) 

1483 self._twd_window_extent = we.frozen() 

1484 self._twd_window_extent.update_from_data_xy(np.array([c1]), False) 

1485 

1486 # Finally, make text align center 

1487 Text.set_horizontalalignment(self, 'center') 

1488 Text.set_verticalalignment(self, 'center') 

1489 

1490 def get_window_extent(self, renderer=None): 

1491 ''' 

1492 Return a :class:`~matplotlib.transforms.Bbox` object bounding 

1493 the text, in display units. 

1494 

1495 In addition to being used internally, this is useful for 

1496 specifying clickable regions in a png file on a web page. 

1497 

1498 *renderer* defaults to the _renderer attribute of the text 

1499 object. This is not assigned until the first execution of 

1500 :meth:`draw`, so you must use this kwarg if you want 

1501 to call :meth:`get_window_extent` prior to the first 

1502 :meth:`draw`. For getting web page regions, it is 

1503 simpler to call the method after saving the figure. 

1504 ''' 

1505 self.update_coords(renderer) 

1506 if self.get_dashlength() == 0.0: 

1507 return Text.get_window_extent(self, renderer=renderer) 

1508 else: 

1509 return self._twd_window_extent 

1510 

1511 def get_dashlength(self): 

1512 """ 

1513 Get the length of the dash. 

1514 """ 

1515 return self._dashlength 

1516 

1517 def set_dashlength(self, dl): 

1518 """ 

1519 Set the length of the dash, in canvas units. 

1520 

1521 Parameters 

1522 ---------- 

1523 dl : float 

1524 """ 

1525 self._dashlength = dl 

1526 self.stale = True 

1527 

1528 def get_dashdirection(self): 

1529 """ 

1530 Get the direction dash. 1 is before the text and 0 is after. 

1531 """ 

1532 return self._dashdirection 

1533 

1534 def set_dashdirection(self, dd): 

1535 """ 

1536 Set the direction of the dash following the text. 1 is before the text 

1537 and 0 is after. The default is 0, which is what you'd want for the 

1538 typical case of ticks below and on the left of the figure. 

1539 

1540 Parameters 

1541 ---------- 

1542 dd : int (1 is before, 0 is after) 

1543 """ 

1544 self._dashdirection = dd 

1545 self.stale = True 

1546 

1547 def get_dashrotation(self): 

1548 """ 

1549 Get the rotation of the dash in degrees. 

1550 """ 

1551 if self._dashrotation is None: 

1552 return self.get_rotation() 

1553 else: 

1554 return self._dashrotation 

1555 

1556 def set_dashrotation(self, dr): 

1557 """ 

1558 Set the rotation of the dash, in degrees. 

1559 

1560 Parameters 

1561 ---------- 

1562 dr : float 

1563 """ 

1564 self._dashrotation = dr 

1565 self.stale = True 

1566 

1567 def get_dashpad(self): 

1568 """ 

1569 Get the extra spacing between the dash and the text, in canvas units. 

1570 """ 

1571 return self._dashpad 

1572 

1573 def set_dashpad(self, dp): 

1574 """ 

1575 Set the "pad" of the TextWithDash, which is the extra spacing 

1576 between the dash and the text, in canvas units. 

1577 

1578 Parameters 

1579 ---------- 

1580 dp : float 

1581 """ 

1582 self._dashpad = dp 

1583 self.stale = True 

1584 

1585 def get_dashpush(self): 

1586 """ 

1587 Get the extra spacing between the dash and the specified text 

1588 position, in canvas units. 

1589 """ 

1590 return self._dashpush 

1591 

1592 def set_dashpush(self, dp): 

1593 """ 

1594 Set the "push" of the TextWithDash, which is the extra spacing between 

1595 the beginning of the dash and the specified position. 

1596 

1597 Parameters 

1598 ---------- 

1599 dp : float 

1600 """ 

1601 self._dashpush = dp 

1602 self.stale = True 

1603 

1604 def set_position(self, xy): 

1605 """ 

1606 Set the (*x*, *y*) position of the :class:`TextWithDash`. 

1607 

1608 Parameters 

1609 ---------- 

1610 xy : (float, float) 

1611 """ 

1612 self.set_x(xy[0]) 

1613 self.set_y(xy[1]) 

1614 

1615 def set_x(self, x): 

1616 """ 

1617 Set the *x* position of the :class:`TextWithDash`. 

1618 

1619 Parameters 

1620 ---------- 

1621 x : float 

1622 """ 

1623 self._dashx = float(x) 

1624 self.stale = True 

1625 

1626 def set_y(self, y): 

1627 """ 

1628 Set the *y* position of the :class:`TextWithDash`. 

1629 

1630 Parameters 

1631 ---------- 

1632 y : float 

1633 """ 

1634 self._dashy = float(y) 

1635 self.stale = True 

1636 

1637 def set_transform(self, t): 

1638 """ 

1639 Set the :class:`matplotlib.transforms.Transform` instance used 

1640 by this artist. 

1641 

1642 Parameters 

1643 ---------- 

1644 t : `~matplotlib.transforms.Transform` 

1645 """ 

1646 Text.set_transform(self, t) 

1647 self.dashline.set_transform(t) 

1648 self.stale = True 

1649 

1650 def get_figure(self): 

1651 """Return the figure instance the artist belongs to.""" 

1652 return self.figure 

1653 

1654 def set_figure(self, fig): 

1655 """ 

1656 Set the figure instance the artist belongs to. 

1657 

1658 Parameters 

1659 ---------- 

1660 fig : `~matplotlib.figure.Figure` 

1661 """ 

1662 Text.set_figure(self, fig) 

1663 self.dashline.set_figure(fig) 

1664 

1665docstring.interpd.update(TextWithDash=artist.kwdoc(TextWithDash)) 

1666 

1667 

1668class OffsetFrom: 

1669 'Callable helper class for working with `Annotation`' 

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

1671 ''' 

1672 Parameters 

1673 ---------- 

1674 artist : `.Artist`, `.BboxBase`, or `.Transform` 

1675 The object to compute the offset from. 

1676 

1677 ref_coord : length 2 sequence 

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

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

1680 *artist* bounding box. 

1681 

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

1683 transform applied to this value. 

1684 

1685 unit : {'points, 'pixels'} 

1686 The screen units to use (pixels or points) for the offset 

1687 input. 

1688 

1689 ''' 

1690 self._artist = artist 

1691 self._ref_coord = ref_coord 

1692 self.set_unit(unit) 

1693 

1694 def set_unit(self, unit): 

1695 ''' 

1696 The unit for input to the transform used by ``__call__`` 

1697 

1698 Parameters 

1699 ---------- 

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

1701 ''' 

1702 cbook._check_in_list(["points", "pixels"], unit=unit) 

1703 self._unit = unit 

1704 

1705 def get_unit(self): 

1706 'The unit for input to the transform used by ``__call__``' 

1707 return self._unit 

1708 

1709 def _get_scale(self, renderer): 

1710 unit = self.get_unit() 

1711 if unit == "pixels": 

1712 return 1. 

1713 else: 

1714 return renderer.points_to_pixels(1.) 

1715 

1716 def __call__(self, renderer): 

1717 ''' 

1718 Return the offset transform. 

1719 

1720 Parameters 

1721 ---------- 

1722 renderer : `RendererBase` 

1723 The renderer to use to compute the offset 

1724 

1725 Returns 

1726 ------- 

1727 transform : `Transform` 

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

1729 relative to the given artist. 

1730 ''' 

1731 if isinstance(self._artist, Artist): 

1732 bbox = self._artist.get_window_extent(renderer) 

1733 l, b, w, h = bbox.bounds 

1734 xf, yf = self._ref_coord 

1735 x, y = l + w * xf, b + h * yf 

1736 elif isinstance(self._artist, BboxBase): 

1737 l, b, w, h = self._artist.bounds 

1738 xf, yf = self._ref_coord 

1739 x, y = l + w * xf, b + h * yf 

1740 elif isinstance(self._artist, Transform): 

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

1742 else: 

1743 raise RuntimeError("unknown type") 

1744 

1745 sc = self._get_scale(renderer) 

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

1747 

1748 return tr 

1749 

1750 

1751class _AnnotationBase: 

1752 def __init__(self, 

1753 xy, 

1754 xycoords='data', 

1755 annotation_clip=None): 

1756 

1757 self.xy = xy 

1758 self.xycoords = xycoords 

1759 self.set_annotation_clip(annotation_clip) 

1760 

1761 self._draggable = None 

1762 

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

1764 if isinstance(s, tuple): 

1765 s1, s2 = s 

1766 else: 

1767 s1, s2 = s, s 

1768 if s1 == 'data': 

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

1770 if s2 == 'data': 

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

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

1773 

1774 def _get_xy_transform(self, renderer, s): 

1775 

1776 if isinstance(s, tuple): 

1777 s1, s2 = s 

1778 from matplotlib.transforms import blended_transform_factory 

1779 tr1 = self._get_xy_transform(renderer, s1) 

1780 tr2 = self._get_xy_transform(renderer, s2) 

1781 tr = blended_transform_factory(tr1, tr2) 

1782 return tr 

1783 elif callable(s): 

1784 tr = s(renderer) 

1785 if isinstance(tr, BboxBase): 

1786 return BboxTransformTo(tr) 

1787 elif isinstance(tr, Transform): 

1788 return tr 

1789 else: 

1790 raise RuntimeError("unknown return type ...") 

1791 elif isinstance(s, Artist): 

1792 bbox = s.get_window_extent(renderer) 

1793 return BboxTransformTo(bbox) 

1794 elif isinstance(s, BboxBase): 

1795 return BboxTransformTo(s) 

1796 elif isinstance(s, Transform): 

1797 return s 

1798 elif not isinstance(s, str): 

1799 raise RuntimeError("unknown coordinate type : %s" % s) 

1800 

1801 if s == 'data': 

1802 return self.axes.transData 

1803 elif s == 'polar': 

1804 from matplotlib.projections import PolarAxes 

1805 tr = PolarAxes.PolarTransform() 

1806 trans = tr + self.axes.transData 

1807 return trans 

1808 

1809 s_ = s.split() 

1810 if len(s_) != 2: 

1811 raise ValueError("%s is not a recognized coordinate" % s) 

1812 

1813 bbox0, xy0 = None, None 

1814 

1815 bbox_name, unit = s_ 

1816 # if unit is offset-like 

1817 if bbox_name == "figure": 

1818 bbox0 = self.figure.bbox 

1819 elif bbox_name == "axes": 

1820 bbox0 = self.axes.bbox 

1821 # elif bbox_name == "bbox": 

1822 # if bbox is None: 

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

1824 # "never set") 

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

1826 

1827 if bbox0 is not None: 

1828 xy0 = bbox0.bounds[:2] 

1829 elif bbox_name == "offset": 

1830 xy0 = self._get_ref_xy(renderer) 

1831 

1832 if xy0 is not None: 

1833 # reference x, y in display coordinate 

1834 ref_x, ref_y = xy0 

1835 from matplotlib.transforms import Affine2D 

1836 if unit == "points": 

1837 # dots per points 

1838 dpp = self.figure.get_dpi() / 72. 

1839 tr = Affine2D().scale(dpp) 

1840 elif unit == "pixels": 

1841 tr = Affine2D() 

1842 elif unit == "fontsize": 

1843 fontsize = self.get_size() 

1844 dpp = fontsize * self.figure.get_dpi() / 72. 

1845 tr = Affine2D().scale(dpp) 

1846 elif unit == "fraction": 

1847 w, h = bbox0.bounds[2:] 

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

1849 else: 

1850 raise ValueError("%s is not a recognized coordinate" % s) 

1851 

1852 return tr.translate(ref_x, ref_y) 

1853 

1854 else: 

1855 raise ValueError("%s is not a recognized coordinate" % s) 

1856 

1857 def _get_ref_xy(self, renderer): 

1858 """ 

1859 return x, y (in display coordinate) that is to be used for a reference 

1860 of any offset coordinate 

1861 """ 

1862 def is_offset(s): 

1863 return isinstance(s, str) and s.split()[0] == "offset" 

1864 

1865 if isinstance(self.xycoords, tuple): 

1866 if any(map(is_offset, self.xycoords)): 

1867 raise ValueError("xycoords should not be an offset coordinate") 

1868 elif is_offset(self.xycoords): 

1869 raise ValueError("xycoords should not be an offset coordinate") 

1870 x, y = self.xy 

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

1872 

1873 # def _get_bbox(self, renderer): 

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

1875 # return bbox 

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

1877 # bbox = bbox.get_window_extent() 

1878 # return bbox 

1879 # else: 

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

1881 # str(bbox)) 

1882 

1883 def set_annotation_clip(self, b): 

1884 """ 

1885 set *annotation_clip* attribute. 

1886 

1887 * True: the annotation will only be drawn when self.xy is inside 

1888 the axes. 

1889 * False: the annotation will always be drawn regardless of its 

1890 position. 

1891 * None: the self.xy will be checked only if *xycoords* is "data" 

1892 """ 

1893 self._annotation_clip = b 

1894 

1895 def get_annotation_clip(self): 

1896 """ 

1897 Return *annotation_clip* attribute. 

1898 See :meth:`set_annotation_clip` for the meaning of return values. 

1899 """ 

1900 return self._annotation_clip 

1901 

1902 def _get_position_xy(self, renderer): 

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

1904 x, y = self.xy 

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

1906 

1907 def _check_xy(self, renderer, xy_pixel): 

1908 """ 

1909 given the xy pixel coordinate, check if the annotation need to 

1910 be drawn. 

1911 """ 

1912 

1913 b = self.get_annotation_clip() 

1914 

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

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

1917 if not self.axes.contains_point(xy_pixel): 

1918 return False 

1919 

1920 return True 

1921 

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

1923 """ 

1924 Set the draggable state -- if state is 

1925 

1926 * None : toggle the current state 

1927 

1928 * True : turn draggable on 

1929 

1930 * False : turn draggable off 

1931 

1932 If draggable is on, you can drag the annotation on the canvas with 

1933 the mouse. The DraggableAnnotation helper instance is returned if 

1934 draggable is on. 

1935 """ 

1936 from matplotlib.offsetbox import DraggableAnnotation 

1937 is_draggable = self._draggable is not None 

1938 

1939 # if state is None we'll toggle 

1940 if state is None: 

1941 state = not is_draggable 

1942 

1943 if state: 

1944 if self._draggable is None: 

1945 self._draggable = DraggableAnnotation(self, use_blit) 

1946 else: 

1947 if self._draggable is not None: 

1948 self._draggable.disconnect() 

1949 self._draggable = None 

1950 

1951 return self._draggable 

1952 

1953 

1954class Annotation(Text, _AnnotationBase): 

1955 """ 

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

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

1958 

1959 Attributes 

1960 ---------- 

1961 xy 

1962 The annotated position. 

1963 xycoords 

1964 The coordinate system for *xy*. 

1965 arrow_patch 

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

1967 """ 

1968 

1969 def __str__(self): 

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

1971 

1972 @cbook._rename_parameter("3.1", "s", "text") 

1973 def __init__(self, text, xy, 

1974 xytext=None, 

1975 xycoords='data', 

1976 textcoords=None, 

1977 arrowprops=None, 

1978 annotation_clip=None, 

1979 **kwargs): 

1980 """ 

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

1982 

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

1984 

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

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

1987 be added by defining *arrowprops*. 

1988 

1989 Parameters 

1990 ---------- 

1991 text : str 

1992 The text of the annotation. *s* is a deprecated synonym for this 

1993 parameter. 

1994 

1995 xy : (float, float) 

1996 The point *(x, y)* to annotate. 

1997 

1998 xytext : (float, float), optional 

1999 The position *(x, y)* to place the text at. 

2000 If *None*, defaults to *xy*. 

2001 

2002 xycoords : str, `.Artist`, `.Transform`, callable or tuple, optional 

2003 

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

2005 of values are supported: 

2006 

2007 - One of the following strings: 

2008 

2009 ================= ============================================= 

2010 Value Description 

2011 ================= ============================================= 

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

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

2014 'figure fraction' Fraction of figure from lower left 

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

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

2017 'axes fraction' Fraction of axes from lower left 

2018 'data' Use the coordinate system of the object being 

2019 annotated (default) 

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

2021 ================= ============================================= 

2022 

2023 - An `.Artist`: *xy* is interpreted as a fraction of the artists 

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

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

2026 center top of the bounding box. 

2027 

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

2029 

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

2031 

2032 def transform(renderer) -> Bbox 

2033 def transform(renderer) -> Transform 

2034 

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

2036 

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

2038 `.Transform` cases above. 

2039 

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

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

2042 of one of the above described types. 

2043 

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

2045 

2046 Defaults to 'data'. 

2047 

2048 textcoords : str, `.Artist`, `.Transform`, callable or tuple, optional 

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

2050 

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

2052 strings: 

2053 

2054 ================= ========================================= 

2055 Value Description 

2056 ================= ========================================= 

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

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

2059 ================= ========================================= 

2060 

2061 Defaults to the value of *xycoords*, i.e. use the same coordinate 

2062 system for annotation point and text position. 

2063 

2064 arrowprops : dict, optional 

2065 The properties used to draw a 

2066 `~matplotlib.patches.FancyArrowPatch` arrow between the 

2067 positions *xy* and *xytext*. 

2068 

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

2070 allowed keys are: 

2071 

2072 ========== ====================================================== 

2073 Key Description 

2074 ========== ====================================================== 

2075 width The width of the arrow in points 

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

2077 headlength The length of the arrow head in points 

2078 shrink Fraction of total length to shrink from both ends 

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

2080 ========== ====================================================== 

2081 

2082 If *arrowprops* contains the key 'arrowstyle' the 

2083 above keys are forbidden. The allowed values of 

2084 ``'arrowstyle'`` are: 

2085 

2086 ============ ============================================= 

2087 Name Attrs 

2088 ============ ============================================= 

2089 ``'-'`` None 

2090 ``'->'`` head_length=0.4,head_width=0.2 

2091 ``'-['`` widthB=1.0,lengthB=0.2,angleB=None 

2092 ``'|-|'`` widthA=1.0,widthB=1.0 

2093 ``'-|>'`` head_length=0.4,head_width=0.2 

2094 ``'<-'`` head_length=0.4,head_width=0.2 

2095 ``'<->'`` head_length=0.4,head_width=0.2 

2096 ``'<|-'`` head_length=0.4,head_width=0.2 

2097 ``'<|-|>'`` head_length=0.4,head_width=0.2 

2098 ``'fancy'`` head_length=0.4,head_width=0.4,tail_width=0.4 

2099 ``'simple'`` head_length=0.5,head_width=0.5,tail_width=0.2 

2100 ``'wedge'`` tail_width=0.3,shrink_factor=0.5 

2101 ============ ============================================= 

2102 

2103 Valid keys for `~matplotlib.patches.FancyArrowPatch` are: 

2104 

2105 =============== ================================================== 

2106 Key Description 

2107 =============== ================================================== 

2108 arrowstyle the arrow style 

2109 connectionstyle the connection style 

2110 relpos default is (0.5, 0.5) 

2111 patchA default is bounding box of the text 

2112 patchB default is None 

2113 shrinkA default is 2 points 

2114 shrinkB default is 2 points 

2115 mutation_scale default is text size (in points) 

2116 mutation_aspect default is 1. 

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

2118 =============== ================================================== 

2119 

2120 Defaults to None, i.e. no arrow is drawn. 

2121 

2122 annotation_clip : bool or None, optional 

2123 Whether to draw the annotation when the annotation point *xy* is 

2124 outside the axes area. 

2125 

2126 - If *True*, the annotation will only be drawn when *xy* is 

2127 within the axes. 

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

2129 - If *None*, the annotation will only be drawn when *xy* is 

2130 within the axes and *xycoords* is 'data'. 

2131 

2132 Defaults to *None*. 

2133 

2134 **kwargs 

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

2136 

2137 Returns 

2138 ------- 

2139 annotation : `.Annotation` 

2140 

2141 See Also 

2142 -------- 

2143 :ref:`plotting-guide-annotation`. 

2144 

2145 """ 

2146 _AnnotationBase.__init__(self, 

2147 xy, 

2148 xycoords=xycoords, 

2149 annotation_clip=annotation_clip) 

2150 # warn about wonky input data 

2151 if (xytext is None and 

2152 textcoords is not None and 

2153 textcoords != xycoords): 

2154 cbook._warn_external("You have used the `textcoords` kwarg, but " 

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

2156 "surprising results.") 

2157 

2158 # clean up textcoords and assign default 

2159 if textcoords is None: 

2160 textcoords = self.xycoords 

2161 self._textcoords = textcoords 

2162 

2163 # cleanup xytext defaults 

2164 if xytext is None: 

2165 xytext = self.xy 

2166 x, y = xytext 

2167 

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

2169 

2170 self.arrowprops = arrowprops 

2171 

2172 if arrowprops is not None: 

2173 if "arrowstyle" in arrowprops: 

2174 arrowprops = self.arrowprops.copy() 

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

2176 else: 

2177 # modified YAArrow API to be used with FancyArrowPatch 

2178 shapekeys = ('width', 'headwidth', 'headlength', 

2179 'shrink', 'frac') 

2180 arrowprops = dict() 

2181 for key, val in self.arrowprops.items(): 

2182 if key not in shapekeys: 

2183 arrowprops[key] = val # basic Patch properties 

2184 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), 

2185 **arrowprops) 

2186 else: 

2187 self.arrow_patch = None 

2188 

2189 def contains(self, event): 

2190 inside, info = self._default_contains(event) 

2191 if inside is not None: 

2192 return inside, info 

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

2194 if self.arrow_patch is not None: 

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

2196 contains = contains or in_patch 

2197 return contains, tinfo 

2198 

2199 @property 

2200 def xyann(self): 

2201 """ 

2202 The the text position. 

2203 

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

2205 """ 

2206 return self.get_position() 

2207 

2208 @xyann.setter 

2209 def xyann(self, xytext): 

2210 self.set_position(xytext) 

2211 

2212 @property 

2213 def anncoords(self): 

2214 """The coordinate system to use for `.Annotation.xyann`.""" 

2215 return self._textcoords 

2216 

2217 @anncoords.setter 

2218 def anncoords(self, coords): 

2219 self._textcoords = coords 

2220 

2221 get_anncoords = anncoords.fget 

2222 get_anncoords.__doc__ = """ 

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

2224 

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

2226 """ 

2227 

2228 set_anncoords = anncoords.fset 

2229 set_anncoords.__doc__ = """ 

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

2231 

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

2233 """ 

2234 

2235 def set_figure(self, fig): 

2236 if self.arrow_patch is not None: 

2237 self.arrow_patch.set_figure(fig) 

2238 Artist.set_figure(self, fig) 

2239 

2240 def update_positions(self, renderer): 

2241 """Update the pixel positions of the annotated point and the text.""" 

2242 xy_pixel = self._get_position_xy(renderer) 

2243 self._update_position_xytext(renderer, xy_pixel) 

2244 

2245 def _update_position_xytext(self, renderer, xy_pixel): 

2246 """ 

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

2248 """ 

2249 # generate transformation, 

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

2251 

2252 ox0, oy0 = self._get_xy_display() 

2253 ox1, oy1 = xy_pixel 

2254 

2255 if self.arrowprops is not None: 

2256 x0, y0 = xy_pixel 

2257 l, b, w, h = Text.get_window_extent(self, renderer).bounds 

2258 r = l + w 

2259 t = b + h 

2260 xc = 0.5 * (l + r) 

2261 yc = 0.5 * (b + t) 

2262 

2263 d = self.arrowprops.copy() 

2264 ms = d.pop("mutation_scale", self.get_size()) 

2265 self.arrow_patch.set_mutation_scale(ms) 

2266 

2267 if "arrowstyle" not in d: 

2268 # Approximately simulate the YAArrow. 

2269 # Pop its kwargs: 

2270 shrink = d.pop('shrink', 0.0) 

2271 width = d.pop('width', 4) 

2272 headwidth = d.pop('headwidth', 12) 

2273 # Ignore frac--it is useless. 

2274 frac = d.pop('frac', None) 

2275 if frac is not None: 

2276 cbook._warn_external( 

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

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

2279 headlength = d.pop('headlength', 12) 

2280 

2281 # NB: ms is in pts 

2282 stylekw = dict(head_length=headlength / ms, 

2283 head_width=headwidth / ms, 

2284 tail_width=width / ms) 

2285 

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

2287 

2288 # using YAArrow style: 

2289 # pick the (x, y) corner of the text bbox closest to point 

2290 # annotated 

2291 xpos = ((l, 0), (xc, 0.5), (r, 1)) 

2292 ypos = ((b, 0), (yc, 0.5), (t, 1)) 

2293 

2294 _, (x, relposx) = min((abs(val[0] - x0), val) for val in xpos) 

2295 _, (y, relposy) = min((abs(val[0] - y0), val) for val in ypos) 

2296 

2297 self._arrow_relpos = (relposx, relposy) 

2298 

2299 r = np.hypot((y - y0), (x - x0)) 

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

2301 self.arrow_patch.shrinkA = shrink_pts 

2302 self.arrow_patch.shrinkB = shrink_pts 

2303 

2304 # adjust the starting point of the arrow relative to 

2305 # the textbox. 

2306 # TODO : Rotation needs to be accounted. 

2307 relpos = self._arrow_relpos 

2308 bbox = Text.get_window_extent(self, renderer) 

2309 ox0 = bbox.x0 + bbox.width * relpos[0] 

2310 oy0 = bbox.y0 + bbox.height * relpos[1] 

2311 

2312 # The arrow will be drawn from (ox0, oy0) to (ox1, 

2313 # oy1). It will be first clipped by patchA and patchB. 

2314 # Then it will be shrunk by shrinkA and shrinkB 

2315 # (in points). If patch A is not set, self.bbox_patch 

2316 # is used. 

2317 

2318 self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1)) 

2319 

2320 if "patchA" in d: 

2321 self.arrow_patch.set_patchA(d.pop("patchA")) 

2322 else: 

2323 if self._bbox_patch: 

2324 self.arrow_patch.set_patchA(self._bbox_patch) 

2325 else: 

2326 pad = renderer.points_to_pixels(4) 

2327 if self.get_text() == "": 

2328 self.arrow_patch.set_patchA(None) 

2329 return 

2330 

2331 bbox = Text.get_window_extent(self, renderer) 

2332 l, b, w, h = bbox.bounds 

2333 l -= pad / 2. 

2334 b -= pad / 2. 

2335 w += pad 

2336 h += pad 

2337 r = Rectangle(xy=(l, b), 

2338 width=w, 

2339 height=h, 

2340 ) 

2341 r.set_transform(IdentityTransform()) 

2342 r.set_clip_on(False) 

2343 

2344 self.arrow_patch.set_patchA(r) 

2345 

2346 @artist.allow_rasterization 

2347 def draw(self, renderer): 

2348 """ 

2349 Draw the :class:`Annotation` object to the given *renderer*. 

2350 """ 

2351 

2352 if renderer is not None: 

2353 self._renderer = renderer 

2354 if not self.get_visible(): 

2355 return 

2356 

2357 xy_pixel = self._get_position_xy(renderer) 

2358 if not self._check_xy(renderer, xy_pixel): 

2359 return 

2360 

2361 self._update_position_xytext(renderer, xy_pixel) 

2362 self.update_bbox_position_size(renderer) 

2363 

2364 if self.arrow_patch is not None: # FancyArrowPatch 

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

2366 self.arrow_patch.figure = self.figure 

2367 self.arrow_patch.draw(renderer) 

2368 

2369 # Draw text, including FancyBboxPatch, after FancyArrowPatch. 

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

2371 Text.draw(self, renderer) 

2372 

2373 def get_window_extent(self, renderer=None): 

2374 """ 

2375 Return the `.Bbox` bounding the text and arrow, in display units. 

2376 

2377 Parameters 

2378 ---------- 

2379 renderer : Renderer, optional 

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

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

2382 necessary to pass this argument when calling `get_window_extent` 

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

2384 trigger a draw first (e.g. by saving the figure). 

2385 """ 

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

2387 # set the renderer before calling update_positions(). 

2388 if not self.get_visible(): 

2389 return Bbox.unit() 

2390 if renderer is not None: 

2391 self._renderer = renderer 

2392 if self._renderer is None: 

2393 self._renderer = self.figure._cachedRenderer 

2394 if self._renderer is None: 

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

2396 

2397 self.update_positions(self._renderer) 

2398 

2399 text_bbox = Text.get_window_extent(self) 

2400 bboxes = [text_bbox] 

2401 

2402 if self.arrow_patch is not None: 

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

2404 

2405 return Bbox.union(bboxes) 

2406 

2407 

2408docstring.interpd.update(Annotation=Annotation.__init__.__doc__)