Coverage for /usr/lib/python3/dist-packages/matplotlib/offsetbox.py: 44%

691 statements  

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

1r""" 

2Container classes for `.Artist`\s. 

3 

4`OffsetBox` 

5 The base of all container artists defined in this module. 

6 

7`AnchoredOffsetbox`, `AnchoredText` 

8 Anchor and align an arbitrary `.Artist` or a text relative to the parent 

9 axes or a specific anchor point. 

10 

11`DrawingArea` 

12 A container with fixed width and height. Children have a fixed position 

13 inside the container and may be clipped. 

14 

15`HPacker`, `VPacker` 

16 Containers for layouting their children vertically or horizontally. 

17 

18`PaddedBox` 

19 A container to add a padding around an `.Artist`. 

20 

21`TextArea` 

22 Contains a single `.Text` instance. 

23""" 

24 

25import numpy as np 

26 

27import matplotlib as mpl 

28from matplotlib import _api, _docstring 

29import matplotlib.artist as martist 

30import matplotlib.path as mpath 

31import matplotlib.text as mtext 

32import matplotlib.transforms as mtransforms 

33from matplotlib.font_manager import FontProperties 

34from matplotlib.image import BboxImage 

35from matplotlib.patches import ( 

36 FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist) 

37from matplotlib.transforms import Bbox, BboxBase, TransformedBbox 

38 

39 

40DEBUG = False 

41 

42 

43# for debugging use 

44def bbox_artist(*args, **kwargs): 

45 if DEBUG: 

46 mbbox_artist(*args, **kwargs) 

47 

48 

49def _get_packed_offsets(wd_list, total, sep, mode="fixed"): 

50 r""" 

51 Pack boxes specified by their ``(width, xdescent)`` pair. 

52 

53 For simplicity of the description, the terminology used here assumes a 

54 horizontal layout, but the function works equally for a vertical layout. 

55 

56 *xdescent* is analogous to the usual descent, but along the x-direction; it 

57 is currently ignored. 

58 

59 There are three packing *mode*\s: 

60 

61 - 'fixed': The elements are packed tight to the left with a spacing of 

62 *sep* in between. If *total* is *None* the returned total will be the 

63 right edge of the last box. A non-*None* total will be passed unchecked 

64 to the output. In particular this means that right edge of the last 

65 box may be further to the right than the returned total. 

66 

67 - 'expand': Distribute the boxes with equal spacing so that the left edge 

68 of the first box is at 0, and the right edge of the last box is at 

69 *total*. The parameter *sep* is ignored in this mode. A total of *None* 

70 is accepted and considered equal to 1. The total is returned unchanged 

71 (except for the conversion *None* to 1). If the total is smaller than 

72 the sum of the widths, the laid out boxes will overlap. 

73 

74 - 'equal': If *total* is given, the total space is divided in N equal 

75 ranges and each box is left-aligned within its subspace. 

76 Otherwise (*total* is *None*), *sep* must be provided and each box is 

77 left-aligned in its subspace of width ``(max(widths) + sep)``. The 

78 total width is then calculated to be ``N * (max(widths) + sep)``. 

79 

80 Parameters 

81 ---------- 

82 wd_list : list of (float, float) 

83 (width, xdescent) of boxes to be packed. 

84 total : float or None 

85 Intended total length. *None* if not used. 

86 sep : float 

87 Spacing between boxes. 

88 mode : {'fixed', 'expand', 'equal'} 

89 The packing mode. 

90 

91 Returns 

92 ------- 

93 total : float 

94 The total width needed to accommodate the laid out boxes. 

95 offsets : array of float 

96 The left offsets of the boxes. 

97 """ 

98 w_list, d_list = zip(*wd_list) # d_list is currently not used. 

99 _api.check_in_list(["fixed", "expand", "equal"], mode=mode) 

100 

101 if mode == "fixed": 

102 offsets_ = np.cumsum([0] + [w + sep for w in w_list]) 

103 offsets = offsets_[:-1] 

104 if total is None: 

105 total = offsets_[-1] - sep 

106 return total, offsets 

107 

108 elif mode == "expand": 

109 # This is a bit of a hack to avoid a TypeError when *total* 

110 # is None and used in conjugation with tight layout. 

111 if total is None: 

112 total = 1 

113 if len(w_list) > 1: 

114 sep = (total - sum(w_list)) / (len(w_list) - 1) 

115 else: 

116 sep = 0 

117 offsets_ = np.cumsum([0] + [w + sep for w in w_list]) 

118 offsets = offsets_[:-1] 

119 return total, offsets 

120 

121 elif mode == "equal": 

122 maxh = max(w_list) 

123 if total is None: 

124 if sep is None: 

125 raise ValueError("total and sep cannot both be None when " 

126 "using layout mode 'equal'") 

127 total = (maxh + sep) * len(w_list) 

128 else: 

129 sep = total / len(w_list) - maxh 

130 offsets = (maxh + sep) * np.arange(len(w_list)) 

131 return total, offsets 

132 

133 

134def _get_aligned_offsets(hd_list, height, align="baseline"): 

135 """ 

136 Align boxes each specified by their ``(height, descent)`` pair. 

137 

138 For simplicity of the description, the terminology used here assumes a 

139 horizontal layout (i.e., vertical alignment), but the function works 

140 equally for a vertical layout. 

141 

142 Parameters 

143 ---------- 

144 hd_list 

145 List of (height, xdescent) of boxes to be aligned. 

146 height : float or None 

147 Intended total height. If None, the maximum of the heights in *hd_list* 

148 is used. 

149 align : {'baseline', 'left', 'top', 'right', 'bottom', 'center'} 

150 The alignment anchor of the boxes. 

151 

152 Returns 

153 ------- 

154 height 

155 The total height of the packing (if a value was originally passed in, 

156 it is returned without checking that it is actually large enough). 

157 descent 

158 The descent of the packing. 

159 offsets 

160 The bottom offsets of the boxes. 

161 """ 

162 

163 if height is None: 

164 height = max(h for h, d in hd_list) 

165 _api.check_in_list( 

166 ["baseline", "left", "top", "right", "bottom", "center"], align=align) 

167 

168 if align == "baseline": 

169 height_descent = max(h - d for h, d in hd_list) 

170 descent = max(d for h, d in hd_list) 

171 height = height_descent + descent 

172 offsets = [0. for h, d in hd_list] 

173 elif align in ["left", "top"]: 

174 descent = 0. 

175 offsets = [d for h, d in hd_list] 

176 elif align in ["right", "bottom"]: 

177 descent = 0. 

178 offsets = [height - h + d for h, d in hd_list] 

179 elif align == "center": 

180 descent = 0. 

181 offsets = [(height - h) * .5 + d for h, d in hd_list] 

182 

183 return height, descent, offsets 

184 

185 

186class OffsetBox(martist.Artist): 

187 """ 

188 The OffsetBox is a simple container artist. 

189 

190 The child artists are meant to be drawn at a relative position to its 

191 parent. 

192 

193 Being an artist itself, all parameters are passed on to `.Artist`. 

194 """ 

195 def __init__(self, *args, **kwargs): 

196 super().__init__(*args) 

197 self._internal_update(kwargs) 

198 # Clipping has not been implemented in the OffsetBox family, so 

199 # disable the clip flag for consistency. It can always be turned back 

200 # on to zero effect. 

201 self.set_clip_on(False) 

202 self._children = [] 

203 self._offset = (0, 0) 

204 

205 def set_figure(self, fig): 

206 """ 

207 Set the `.Figure` for the `.OffsetBox` and all its children. 

208 

209 Parameters 

210 ---------- 

211 fig : `~matplotlib.figure.Figure` 

212 """ 

213 super().set_figure(fig) 

214 for c in self.get_children(): 

215 c.set_figure(fig) 

216 

217 @martist.Artist.axes.setter 

218 def axes(self, ax): 

219 # TODO deal with this better 

220 martist.Artist.axes.fset(self, ax) 

221 for c in self.get_children(): 

222 if c is not None: 

223 c.axes = ax 

224 

225 def contains(self, mouseevent): 

226 """ 

227 Delegate the mouse event contains-check to the children. 

228 

229 As a container, the `.OffsetBox` does not respond itself to 

230 mouseevents. 

231 

232 Parameters 

233 ---------- 

234 mouseevent : `matplotlib.backend_bases.MouseEvent` 

235 

236 Returns 

237 ------- 

238 contains : bool 

239 Whether any values are within the radius. 

240 details : dict 

241 An artist-specific dictionary of details of the event context, 

242 such as which points are contained in the pick radius. See the 

243 individual Artist subclasses for details. 

244 

245 See Also 

246 -------- 

247 .Artist.contains 

248 """ 

249 inside, info = self._default_contains(mouseevent) 

250 if inside is not None: 

251 return inside, info 

252 for c in self.get_children(): 

253 a, b = c.contains(mouseevent) 

254 if a: 

255 return a, b 

256 return False, {} 

257 

258 def set_offset(self, xy): 

259 """ 

260 Set the offset. 

261 

262 Parameters 

263 ---------- 

264 xy : (float, float) or callable 

265 The (x, y) coordinates of the offset in display units. These can 

266 either be given explicitly as a tuple (x, y), or by providing a 

267 function that converts the extent into the offset. This function 

268 must have the signature:: 

269 

270 def offset(width, height, xdescent, ydescent, renderer) \ 

271-> (float, float) 

272 """ 

273 self._offset = xy 

274 self.stale = True 

275 

276 def get_offset(self, width, height, xdescent, ydescent, renderer): 

277 """ 

278 Return the offset as a tuple (x, y). 

279 

280 The extent parameters have to be provided to handle the case where the 

281 offset is dynamically determined by a callable (see 

282 `~.OffsetBox.set_offset`). 

283 

284 Parameters 

285 ---------- 

286 width, height, xdescent, ydescent 

287 Extent parameters. 

288 renderer : `.RendererBase` subclass 

289 

290 """ 

291 return (self._offset(width, height, xdescent, ydescent, renderer) 

292 if callable(self._offset) 

293 else self._offset) 

294 

295 def set_width(self, width): 

296 """ 

297 Set the width of the box. 

298 

299 Parameters 

300 ---------- 

301 width : float 

302 """ 

303 self.width = width 

304 self.stale = True 

305 

306 def set_height(self, height): 

307 """ 

308 Set the height of the box. 

309 

310 Parameters 

311 ---------- 

312 height : float 

313 """ 

314 self.height = height 

315 self.stale = True 

316 

317 def get_visible_children(self): 

318 r"""Return a list of the visible child `.Artist`\s.""" 

319 return [c for c in self._children if c.get_visible()] 

320 

321 def get_children(self): 

322 r"""Return a list of the child `.Artist`\s.""" 

323 return self._children 

324 

325 def get_extent_offsets(self, renderer): 

326 """ 

327 Update offset of the children and return the extent of the box. 

328 

329 Parameters 

330 ---------- 

331 renderer : `.RendererBase` subclass 

332 

333 Returns 

334 ------- 

335 width 

336 height 

337 xdescent 

338 ydescent 

339 list of (xoffset, yoffset) pairs 

340 """ 

341 raise NotImplementedError( 

342 "get_extent_offsets must be overridden in derived classes.") 

343 

344 def get_extent(self, renderer): 

345 """Return a tuple ``width, height, xdescent, ydescent`` of the box.""" 

346 w, h, xd, yd, offsets = self.get_extent_offsets(renderer) 

347 return w, h, xd, yd 

348 

349 def get_window_extent(self, renderer=None): 

350 # docstring inherited 

351 if renderer is None: 

352 renderer = self.figure._get_renderer() 

353 w, h, xd, yd, offsets = self.get_extent_offsets(renderer) 

354 px, py = self.get_offset(w, h, xd, yd, renderer) 

355 return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h) 

356 

357 def draw(self, renderer): 

358 """ 

359 Update the location of children if necessary and draw them 

360 to the given *renderer*. 

361 """ 

362 w, h, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) 

363 px, py = self.get_offset(w, h, xdescent, ydescent, renderer) 

364 for c, (ox, oy) in zip(self.get_visible_children(), offsets): 

365 c.set_offset((px + ox, py + oy)) 

366 c.draw(renderer) 

367 bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

368 self.stale = False 

369 

370 

371class PackerBase(OffsetBox): 

372 def __init__(self, pad=None, sep=None, width=None, height=None, 

373 align="baseline", mode="fixed", children=None): 

374 """ 

375 Parameters 

376 ---------- 

377 pad : float, optional 

378 The boundary padding in points. 

379 

380 sep : float, optional 

381 The spacing between items in points. 

382 

383 width, height : float, optional 

384 Width and height of the container box in pixels, calculated if 

385 *None*. 

386 

387 align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}, \ 

388default: 'baseline' 

389 Alignment of boxes. 

390 

391 mode : {'fixed', 'expand', 'equal'}, default: 'fixed' 

392 The packing mode. 

393 

394 - 'fixed' packs the given `.Artist`\\s tight with *sep* spacing. 

395 - 'expand' uses the maximal available space to distribute the 

396 artists with equal spacing in between. 

397 - 'equal': Each artist an equal fraction of the available space 

398 and is left-aligned (or top-aligned) therein. 

399 

400 children : list of `.Artist` 

401 The artists to pack. 

402 

403 Notes 

404 ----- 

405 *pad* and *sep* are in points and will be scaled with the renderer 

406 dpi, while *width* and *height* are in pixels. 

407 """ 

408 super().__init__() 

409 self.height = height 

410 self.width = width 

411 self.sep = sep 

412 self.pad = pad 

413 self.mode = mode 

414 self.align = align 

415 self._children = children 

416 

417 

418class VPacker(PackerBase): 

419 """ 

420 VPacker packs its children vertically, automatically adjusting their 

421 relative positions at draw time. 

422 """ 

423 

424 def get_extent_offsets(self, renderer): 

425 # docstring inherited 

426 dpicor = renderer.points_to_pixels(1.) 

427 pad = self.pad * dpicor 

428 sep = self.sep * dpicor 

429 

430 if self.width is not None: 

431 for c in self.get_visible_children(): 

432 if isinstance(c, PackerBase) and c.mode == "expand": 

433 c.set_width(self.width) 

434 

435 whd_list = [c.get_extent(renderer) 

436 for c in self.get_visible_children()] 

437 whd_list = [(w, h, xd, (h - yd)) for w, h, xd, yd in whd_list] 

438 

439 wd_list = [(w, xd) for w, h, xd, yd in whd_list] 

440 width, xdescent, xoffsets = _get_aligned_offsets(wd_list, 

441 self.width, 

442 self.align) 

443 

444 pack_list = [(h, yd) for w, h, xd, yd in whd_list] 

445 height, yoffsets_ = _get_packed_offsets(pack_list, self.height, 

446 sep, self.mode) 

447 

448 yoffsets = yoffsets_ + [yd for w, h, xd, yd in whd_list] 

449 ydescent = height - yoffsets[0] 

450 yoffsets = height - yoffsets 

451 

452 yoffsets = yoffsets - ydescent 

453 

454 return (width + 2 * pad, height + 2 * pad, 

455 xdescent + pad, ydescent + pad, 

456 list(zip(xoffsets, yoffsets))) 

457 

458 

459class HPacker(PackerBase): 

460 """ 

461 HPacker packs its children horizontally, automatically adjusting their 

462 relative positions at draw time. 

463 """ 

464 

465 def get_extent_offsets(self, renderer): 

466 # docstring inherited 

467 dpicor = renderer.points_to_pixels(1.) 

468 pad = self.pad * dpicor 

469 sep = self.sep * dpicor 

470 

471 whd_list = [c.get_extent(renderer) 

472 for c in self.get_visible_children()] 

473 

474 if not whd_list: 

475 return 2 * pad, 2 * pad, pad, pad, [] 

476 

477 hd_list = [(h, yd) for w, h, xd, yd in whd_list] 

478 height, ydescent, yoffsets = _get_aligned_offsets(hd_list, 

479 self.height, 

480 self.align) 

481 

482 pack_list = [(w, xd) for w, h, xd, yd in whd_list] 

483 

484 width, xoffsets_ = _get_packed_offsets(pack_list, self.width, 

485 sep, self.mode) 

486 

487 xoffsets = xoffsets_ + [xd for w, h, xd, yd in whd_list] 

488 

489 xdescent = whd_list[0][2] 

490 xoffsets = xoffsets - xdescent 

491 

492 return (width + 2 * pad, height + 2 * pad, 

493 xdescent + pad, ydescent + pad, 

494 list(zip(xoffsets, yoffsets))) 

495 

496 

497class PaddedBox(OffsetBox): 

498 """ 

499 A container to add a padding around an `.Artist`. 

500 

501 The `.PaddedBox` contains a `.FancyBboxPatch` that is used to visualize 

502 it when rendering. 

503 """ 

504 

505 @_api.make_keyword_only("3.6", name="draw_frame") 

506 def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None): 

507 """ 

508 Parameters 

509 ---------- 

510 child : `~matplotlib.artist.Artist` 

511 The contained `.Artist`. 

512 pad : float 

513 The padding in points. This will be scaled with the renderer dpi. 

514 In contrast, *width* and *height* are in *pixels* and thus not 

515 scaled. 

516 draw_frame : bool 

517 Whether to draw the contained `.FancyBboxPatch`. 

518 patch_attrs : dict or None 

519 Additional parameters passed to the contained `.FancyBboxPatch`. 

520 """ 

521 super().__init__() 

522 self.pad = pad 

523 self._children = [child] 

524 self.patch = FancyBboxPatch( 

525 xy=(0.0, 0.0), width=1., height=1., 

526 facecolor='w', edgecolor='k', 

527 mutation_scale=1, # self.prop.get_size_in_points(), 

528 snap=True, 

529 visible=draw_frame, 

530 boxstyle="square,pad=0", 

531 ) 

532 if patch_attrs is not None: 

533 self.patch.update(patch_attrs) 

534 

535 def get_extent_offsets(self, renderer): 

536 # docstring inherited. 

537 dpicor = renderer.points_to_pixels(1.) 

538 pad = self.pad * dpicor 

539 w, h, xd, yd = self._children[0].get_extent(renderer) 

540 return (w + 2 * pad, h + 2 * pad, xd + pad, yd + pad, 

541 [(0, 0)]) 

542 

543 def draw(self, renderer): 

544 # docstring inherited 

545 w, h, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) 

546 px, py = self.get_offset(w, h, xdescent, ydescent, renderer) 

547 for c, (ox, oy) in zip(self.get_visible_children(), offsets): 

548 c.set_offset((px + ox, py + oy)) 

549 

550 self.draw_frame(renderer) 

551 

552 for c in self.get_visible_children(): 

553 c.draw(renderer) 

554 

555 self.stale = False 

556 

557 def update_frame(self, bbox, fontsize=None): 

558 self.patch.set_bounds(bbox.bounds) 

559 if fontsize: 

560 self.patch.set_mutation_scale(fontsize) 

561 self.stale = True 

562 

563 def draw_frame(self, renderer): 

564 # update the location and size of the legend 

565 self.update_frame(self.get_window_extent(renderer)) 

566 self.patch.draw(renderer) 

567 

568 

569class DrawingArea(OffsetBox): 

570 """ 

571 The DrawingArea can contain any Artist as a child. The DrawingArea 

572 has a fixed width and height. The position of children relative to 

573 the parent is fixed. The children can be clipped at the 

574 boundaries of the parent. 

575 """ 

576 

577 def __init__(self, width, height, xdescent=0., ydescent=0., clip=False): 

578 """ 

579 Parameters 

580 ---------- 

581 width, height : float 

582 Width and height of the container box. 

583 xdescent, ydescent : float 

584 Descent of the box in x- and y-direction. 

585 clip : bool 

586 Whether to clip the children to the box. 

587 """ 

588 super().__init__() 

589 self.width = width 

590 self.height = height 

591 self.xdescent = xdescent 

592 self.ydescent = ydescent 

593 self._clip_children = clip 

594 self.offset_transform = mtransforms.Affine2D() 

595 self.dpi_transform = mtransforms.Affine2D() 

596 

597 @property 

598 def clip_children(self): 

599 """ 

600 If the children of this DrawingArea should be clipped 

601 by DrawingArea bounding box. 

602 """ 

603 return self._clip_children 

604 

605 @clip_children.setter 

606 def clip_children(self, val): 

607 self._clip_children = bool(val) 

608 self.stale = True 

609 

610 def get_transform(self): 

611 """ 

612 Return the `~matplotlib.transforms.Transform` applied to the children. 

613 """ 

614 return self.dpi_transform + self.offset_transform 

615 

616 def set_transform(self, t): 

617 """ 

618 set_transform is ignored. 

619 """ 

620 

621 def set_offset(self, xy): 

622 """ 

623 Set the offset of the container. 

624 

625 Parameters 

626 ---------- 

627 xy : (float, float) 

628 The (x, y) coordinates of the offset in display units. 

629 """ 

630 self._offset = xy 

631 self.offset_transform.clear() 

632 self.offset_transform.translate(xy[0], xy[1]) 

633 self.stale = True 

634 

635 def get_offset(self): 

636 """Return offset of the container.""" 

637 return self._offset 

638 

639 def get_window_extent(self, renderer=None): 

640 # docstring inherited 

641 if renderer is None: 

642 renderer = self.figure._get_renderer() 

643 w, h, xd, yd = self.get_extent(renderer) 

644 ox, oy = self.get_offset() # w, h, xd, yd) 

645 

646 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) 

647 

648 def get_extent(self, renderer): 

649 """Return width, height, xdescent, ydescent of box.""" 

650 dpi_cor = renderer.points_to_pixels(1.) 

651 return (self.width * dpi_cor, self.height * dpi_cor, 

652 self.xdescent * dpi_cor, self.ydescent * dpi_cor) 

653 

654 def add_artist(self, a): 

655 """Add an `.Artist` to the container box.""" 

656 self._children.append(a) 

657 if not a.is_transform_set(): 

658 a.set_transform(self.get_transform()) 

659 if self.axes is not None: 

660 a.axes = self.axes 

661 fig = self.figure 

662 if fig is not None: 

663 a.set_figure(fig) 

664 

665 def draw(self, renderer): 

666 # docstring inherited 

667 

668 dpi_cor = renderer.points_to_pixels(1.) 

669 self.dpi_transform.clear() 

670 self.dpi_transform.scale(dpi_cor) 

671 

672 # At this point the DrawingArea has a transform 

673 # to the display space so the path created is 

674 # good for clipping children 

675 tpath = mtransforms.TransformedPath( 

676 mpath.Path([[0, 0], [0, self.height], 

677 [self.width, self.height], 

678 [self.width, 0]]), 

679 self.get_transform()) 

680 for c in self._children: 

681 if self._clip_children and not (c.clipbox or c._clippath): 

682 c.set_clip_path(tpath) 

683 c.draw(renderer) 

684 

685 bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

686 self.stale = False 

687 

688 

689class TextArea(OffsetBox): 

690 """ 

691 The TextArea is a container artist for a single Text instance. 

692 

693 The text is placed at (0, 0) with baseline+left alignment, by default. The 

694 width and height of the TextArea instance is the width and height of its 

695 child text. 

696 """ 

697 

698 @_api.make_keyword_only("3.6", name="textprops") 

699 def __init__(self, s, 

700 textprops=None, 

701 multilinebaseline=False, 

702 ): 

703 """ 

704 Parameters 

705 ---------- 

706 s : str 

707 The text to be displayed. 

708 textprops : dict, default: {} 

709 Dictionary of keyword parameters to be passed to the `.Text` 

710 instance in the TextArea. 

711 multilinebaseline : bool, default: False 

712 Whether the baseline for multiline text is adjusted so that it 

713 is (approximately) center-aligned with single-line text. 

714 """ 

715 if textprops is None: 

716 textprops = {} 

717 self._text = mtext.Text(0, 0, s, **textprops) 

718 super().__init__() 

719 self._children = [self._text] 

720 self.offset_transform = mtransforms.Affine2D() 

721 self._baseline_transform = mtransforms.Affine2D() 

722 self._text.set_transform(self.offset_transform + 

723 self._baseline_transform) 

724 self._multilinebaseline = multilinebaseline 

725 

726 def set_text(self, s): 

727 """Set the text of this area as a string.""" 

728 self._text.set_text(s) 

729 self.stale = True 

730 

731 def get_text(self): 

732 """Return the string representation of this area's text.""" 

733 return self._text.get_text() 

734 

735 def set_multilinebaseline(self, t): 

736 """ 

737 Set multilinebaseline. 

738 

739 If True, the baseline for multiline text is adjusted so that it is 

740 (approximately) center-aligned with single-line text. This is used 

741 e.g. by the legend implementation so that single-line labels are 

742 baseline-aligned, but multiline labels are "center"-aligned with them. 

743 """ 

744 self._multilinebaseline = t 

745 self.stale = True 

746 

747 def get_multilinebaseline(self): 

748 """ 

749 Get multilinebaseline. 

750 """ 

751 return self._multilinebaseline 

752 

753 def set_transform(self, t): 

754 """ 

755 set_transform is ignored. 

756 """ 

757 

758 def set_offset(self, xy): 

759 """ 

760 Set the offset of the container. 

761 

762 Parameters 

763 ---------- 

764 xy : (float, float) 

765 The (x, y) coordinates of the offset in display units. 

766 """ 

767 self._offset = xy 

768 self.offset_transform.clear() 

769 self.offset_transform.translate(xy[0], xy[1]) 

770 self.stale = True 

771 

772 def get_offset(self): 

773 """Return offset of the container.""" 

774 return self._offset 

775 

776 def get_window_extent(self, renderer=None): 

777 # docstring inherited 

778 if renderer is None: 

779 renderer = self.figure._get_renderer() 

780 w, h, xd, yd = self.get_extent(renderer) 

781 ox, oy = self.get_offset() 

782 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) 

783 

784 def get_extent(self, renderer): 

785 _, h_, d_ = renderer.get_text_width_height_descent( 

786 "lp", self._text._fontproperties, 

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

788 

789 bbox, info, yd = self._text._get_layout(renderer) 

790 w, h = bbox.size 

791 

792 self._baseline_transform.clear() 

793 

794 if len(info) > 1 and self._multilinebaseline: 

795 yd_new = 0.5 * h - 0.5 * (h_ - d_) 

796 self._baseline_transform.translate(0, yd - yd_new) 

797 yd = yd_new 

798 else: # single line 

799 h_d = max(h_ - d_, h - yd) 

800 h = h_d + yd 

801 

802 ha = self._text.get_horizontalalignment() 

803 if ha == 'left': 

804 xd = 0 

805 elif ha == 'center': 

806 xd = w / 2 

807 elif ha == 'right': 

808 xd = w 

809 

810 return w, h, xd, yd 

811 

812 def draw(self, renderer): 

813 # docstring inherited 

814 self._text.draw(renderer) 

815 bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

816 self.stale = False 

817 

818 

819class AuxTransformBox(OffsetBox): 

820 """ 

821 Offset Box with the aux_transform. Its children will be 

822 transformed with the aux_transform first then will be 

823 offsetted. The absolute coordinate of the aux_transform is meaning 

824 as it will be automatically adjust so that the left-lower corner 

825 of the bounding box of children will be set to (0, 0) before the 

826 offset transform. 

827 

828 It is similar to drawing area, except that the extent of the box 

829 is not predetermined but calculated from the window extent of its 

830 children. Furthermore, the extent of the children will be 

831 calculated in the transformed coordinate. 

832 """ 

833 def __init__(self, aux_transform): 

834 self.aux_transform = aux_transform 

835 super().__init__() 

836 self.offset_transform = mtransforms.Affine2D() 

837 # ref_offset_transform makes offset_transform always relative to the 

838 # lower-left corner of the bbox of its children. 

839 self.ref_offset_transform = mtransforms.Affine2D() 

840 

841 def add_artist(self, a): 

842 """Add an `.Artist` to the container box.""" 

843 self._children.append(a) 

844 a.set_transform(self.get_transform()) 

845 self.stale = True 

846 

847 def get_transform(self): 

848 """ 

849 Return the :class:`~matplotlib.transforms.Transform` applied 

850 to the children 

851 """ 

852 return (self.aux_transform 

853 + self.ref_offset_transform 

854 + self.offset_transform) 

855 

856 def set_transform(self, t): 

857 """ 

858 set_transform is ignored. 

859 """ 

860 

861 def set_offset(self, xy): 

862 """ 

863 Set the offset of the container. 

864 

865 Parameters 

866 ---------- 

867 xy : (float, float) 

868 The (x, y) coordinates of the offset in display units. 

869 """ 

870 self._offset = xy 

871 self.offset_transform.clear() 

872 self.offset_transform.translate(xy[0], xy[1]) 

873 self.stale = True 

874 

875 def get_offset(self): 

876 """Return offset of the container.""" 

877 return self._offset 

878 

879 def get_window_extent(self, renderer=None): 

880 # docstring inherited 

881 if renderer is None: 

882 renderer = self.figure._get_renderer() 

883 w, h, xd, yd = self.get_extent(renderer) 

884 ox, oy = self.get_offset() # w, h, xd, yd) 

885 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) 

886 

887 def get_extent(self, renderer): 

888 # clear the offset transforms 

889 _off = self.offset_transform.get_matrix() # to be restored later 

890 self.ref_offset_transform.clear() 

891 self.offset_transform.clear() 

892 # calculate the extent 

893 bboxes = [c.get_window_extent(renderer) for c in self._children] 

894 ub = mtransforms.Bbox.union(bboxes) 

895 # adjust ref_offset_transform 

896 self.ref_offset_transform.translate(-ub.x0, -ub.y0) 

897 # restore offset transform 

898 self.offset_transform.set_matrix(_off) 

899 

900 return ub.width, ub.height, 0., 0. 

901 

902 def draw(self, renderer): 

903 # docstring inherited 

904 for c in self._children: 

905 c.draw(renderer) 

906 bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

907 self.stale = False 

908 

909 

910class AnchoredOffsetbox(OffsetBox): 

911 """ 

912 An offset box placed according to location *loc*. 

913 

914 AnchoredOffsetbox has a single child. When multiple children are needed, 

915 use an extra OffsetBox to enclose them. By default, the offset box is 

916 anchored against its parent axes. You may explicitly specify the 

917 *bbox_to_anchor*. 

918 """ 

919 zorder = 5 # zorder of the legend 

920 

921 # Location codes 

922 codes = {'upper right': 1, 

923 'upper left': 2, 

924 'lower left': 3, 

925 'lower right': 4, 

926 'right': 5, 

927 'center left': 6, 

928 'center right': 7, 

929 'lower center': 8, 

930 'upper center': 9, 

931 'center': 10, 

932 } 

933 

934 @_api.make_keyword_only("3.6", name="pad") 

935 def __init__(self, loc, 

936 pad=0.4, borderpad=0.5, 

937 child=None, prop=None, frameon=True, 

938 bbox_to_anchor=None, 

939 bbox_transform=None, 

940 **kwargs): 

941 """ 

942 Parameters 

943 ---------- 

944 loc : str 

945 The box location. Valid locations are 

946 'upper left', 'upper center', 'upper right', 

947 'center left', 'center', 'center right', 

948 'lower left', 'lower center', 'lower right'. 

949 For backward compatibility, numeric values are accepted as well. 

950 See the parameter *loc* of `.Legend` for details. 

951 pad : float, default: 0.4 

952 Padding around the child as fraction of the fontsize. 

953 borderpad : float, default: 0.5 

954 Padding between the offsetbox frame and the *bbox_to_anchor*. 

955 child : `.OffsetBox` 

956 The box that will be anchored. 

957 prop : `.FontProperties` 

958 This is only used as a reference for paddings. If not given, 

959 :rc:`legend.fontsize` is used. 

960 frameon : bool 

961 Whether to draw a frame around the box. 

962 bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats 

963 Box that is used to position the legend in conjunction with *loc*. 

964 bbox_transform : None or :class:`matplotlib.transforms.Transform` 

965 The transform for the bounding box (*bbox_to_anchor*). 

966 **kwargs 

967 All other parameters are passed on to `.OffsetBox`. 

968 

969 Notes 

970 ----- 

971 See `.Legend` for a detailed description of the anchoring mechanism. 

972 """ 

973 super().__init__(**kwargs) 

974 

975 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) 

976 self.set_child(child) 

977 

978 if isinstance(loc, str): 

979 loc = _api.check_getitem(self.codes, loc=loc) 

980 

981 self.loc = loc 

982 self.borderpad = borderpad 

983 self.pad = pad 

984 

985 if prop is None: 

986 self.prop = FontProperties(size=mpl.rcParams["legend.fontsize"]) 

987 else: 

988 self.prop = FontProperties._from_any(prop) 

989 if isinstance(prop, dict) and "size" not in prop: 

990 self.prop.set_size(mpl.rcParams["legend.fontsize"]) 

991 

992 self.patch = FancyBboxPatch( 

993 xy=(0.0, 0.0), width=1., height=1., 

994 facecolor='w', edgecolor='k', 

995 mutation_scale=self.prop.get_size_in_points(), 

996 snap=True, 

997 visible=frameon, 

998 boxstyle="square,pad=0", 

999 ) 

1000 

1001 def set_child(self, child): 

1002 """Set the child to be anchored.""" 

1003 self._child = child 

1004 if child is not None: 

1005 child.axes = self.axes 

1006 self.stale = True 

1007 

1008 def get_child(self): 

1009 """Return the child.""" 

1010 return self._child 

1011 

1012 def get_children(self): 

1013 """Return the list of children.""" 

1014 return [self._child] 

1015 

1016 def get_extent(self, renderer): 

1017 """ 

1018 Return the extent of the box as (width, height, x, y). 

1019 

1020 This is the extent of the child plus the padding. 

1021 """ 

1022 w, h, xd, yd = self.get_child().get_extent(renderer) 

1023 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) 

1024 pad = self.pad * fontsize 

1025 

1026 return w + 2 * pad, h + 2 * pad, xd + pad, yd + pad 

1027 

1028 def get_bbox_to_anchor(self): 

1029 """Return the bbox that the box is anchored to.""" 

1030 if self._bbox_to_anchor is None: 

1031 return self.axes.bbox 

1032 else: 

1033 transform = self._bbox_to_anchor_transform 

1034 if transform is None: 

1035 return self._bbox_to_anchor 

1036 else: 

1037 return TransformedBbox(self._bbox_to_anchor, transform) 

1038 

1039 def set_bbox_to_anchor(self, bbox, transform=None): 

1040 """ 

1041 Set the bbox that the box is anchored to. 

1042 

1043 *bbox* can be a Bbox instance, a list of [left, bottom, width, 

1044 height], or a list of [left, bottom] where the width and 

1045 height will be assumed to be zero. The bbox will be 

1046 transformed to display coordinate by the given transform. 

1047 """ 

1048 if bbox is None or isinstance(bbox, BboxBase): 

1049 self._bbox_to_anchor = bbox 

1050 else: 

1051 try: 

1052 l = len(bbox) 

1053 except TypeError as err: 

1054 raise ValueError(f"Invalid bbox: {bbox}") from err 

1055 

1056 if l == 2: 

1057 bbox = [bbox[0], bbox[1], 0, 0] 

1058 

1059 self._bbox_to_anchor = Bbox.from_bounds(*bbox) 

1060 

1061 self._bbox_to_anchor_transform = transform 

1062 self.stale = True 

1063 

1064 def get_window_extent(self, renderer=None): 

1065 # docstring inherited 

1066 if renderer is None: 

1067 renderer = self.figure._get_renderer() 

1068 

1069 self._update_offset_func(renderer) 

1070 w, h, xd, yd = self.get_extent(renderer) 

1071 ox, oy = self.get_offset(w, h, xd, yd, renderer) 

1072 return Bbox.from_bounds(ox - xd, oy - yd, w, h) 

1073 

1074 def _update_offset_func(self, renderer, fontsize=None): 

1075 """ 

1076 Update the offset func which depends on the dpi of the 

1077 renderer (because of the padding). 

1078 """ 

1079 if fontsize is None: 

1080 fontsize = renderer.points_to_pixels( 

1081 self.prop.get_size_in_points()) 

1082 

1083 def _offset(w, h, xd, yd, renderer): 

1084 bbox = Bbox.from_bounds(0, 0, w, h) 

1085 borderpad = self.borderpad * fontsize 

1086 bbox_to_anchor = self.get_bbox_to_anchor() 

1087 x0, y0 = _get_anchored_bbox( 

1088 self.loc, bbox, bbox_to_anchor, borderpad) 

1089 return x0 + xd, y0 + yd 

1090 

1091 self.set_offset(_offset) 

1092 

1093 def update_frame(self, bbox, fontsize=None): 

1094 self.patch.set_bounds(bbox.bounds) 

1095 if fontsize: 

1096 self.patch.set_mutation_scale(fontsize) 

1097 

1098 def draw(self, renderer): 

1099 # docstring inherited 

1100 if not self.get_visible(): 

1101 return 

1102 

1103 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) 

1104 self._update_offset_func(renderer, fontsize) 

1105 

1106 # update the location and size of the legend 

1107 bbox = self.get_window_extent(renderer) 

1108 self.update_frame(bbox, fontsize) 

1109 self.patch.draw(renderer) 

1110 

1111 width, height, xdescent, ydescent = self.get_extent(renderer) 

1112 

1113 px, py = self.get_offset(width, height, xdescent, ydescent, renderer) 

1114 

1115 self.get_child().set_offset((px, py)) 

1116 self.get_child().draw(renderer) 

1117 self.stale = False 

1118 

1119 

1120def _get_anchored_bbox(loc, bbox, parentbbox, borderpad): 

1121 """ 

1122 Return the (x, y) position of the *bbox* anchored at the *parentbbox* with 

1123 the *loc* code with the *borderpad*. 

1124 """ 

1125 # This is only called internally and *loc* should already have been 

1126 # validated. If 0 (None), we just let ``bbox.anchored`` raise. 

1127 c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc] 

1128 container = parentbbox.padded(-borderpad) 

1129 return bbox.anchored(c, container=container).p0 

1130 

1131 

1132class AnchoredText(AnchoredOffsetbox): 

1133 """ 

1134 AnchoredOffsetbox with Text. 

1135 """ 

1136 

1137 @_api.make_keyword_only("3.6", name="pad") 

1138 def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs): 

1139 """ 

1140 Parameters 

1141 ---------- 

1142 s : str 

1143 Text. 

1144 

1145 loc : str 

1146 Location code. See `AnchoredOffsetbox`. 

1147 

1148 pad : float, default: 0.4 

1149 Padding around the text as fraction of the fontsize. 

1150 

1151 borderpad : float, default: 0.5 

1152 Spacing between the offsetbox frame and the *bbox_to_anchor*. 

1153 

1154 prop : dict, optional 

1155 Dictionary of keyword parameters to be passed to the 

1156 `~matplotlib.text.Text` instance contained inside AnchoredText. 

1157 

1158 **kwargs 

1159 All other parameters are passed to `AnchoredOffsetbox`. 

1160 """ 

1161 

1162 if prop is None: 

1163 prop = {} 

1164 badkwargs = {'va', 'verticalalignment'} 

1165 if badkwargs & set(prop): 

1166 raise ValueError( 

1167 'Mixing verticalalignment with AnchoredText is not supported.') 

1168 

1169 self.txt = TextArea(s, textprops=prop) 

1170 fp = self.txt._text.get_fontproperties() 

1171 super().__init__( 

1172 loc, pad=pad, borderpad=borderpad, child=self.txt, prop=fp, 

1173 **kwargs) 

1174 

1175 

1176class OffsetImage(OffsetBox): 

1177 

1178 @_api.make_keyword_only("3.6", name="zoom") 

1179 def __init__(self, arr, 

1180 zoom=1, 

1181 cmap=None, 

1182 norm=None, 

1183 interpolation=None, 

1184 origin=None, 

1185 filternorm=True, 

1186 filterrad=4.0, 

1187 resample=False, 

1188 dpi_cor=True, 

1189 **kwargs 

1190 ): 

1191 

1192 super().__init__() 

1193 self._dpi_cor = dpi_cor 

1194 

1195 self.image = BboxImage(bbox=self.get_window_extent, 

1196 cmap=cmap, 

1197 norm=norm, 

1198 interpolation=interpolation, 

1199 origin=origin, 

1200 filternorm=filternorm, 

1201 filterrad=filterrad, 

1202 resample=resample, 

1203 **kwargs 

1204 ) 

1205 

1206 self._children = [self.image] 

1207 

1208 self.set_zoom(zoom) 

1209 self.set_data(arr) 

1210 

1211 def set_data(self, arr): 

1212 self._data = np.asarray(arr) 

1213 self.image.set_data(self._data) 

1214 self.stale = True 

1215 

1216 def get_data(self): 

1217 return self._data 

1218 

1219 def set_zoom(self, zoom): 

1220 self._zoom = zoom 

1221 self.stale = True 

1222 

1223 def get_zoom(self): 

1224 return self._zoom 

1225 

1226 def get_offset(self): 

1227 """Return offset of the container.""" 

1228 return self._offset 

1229 

1230 def get_children(self): 

1231 return [self.image] 

1232 

1233 def get_window_extent(self, renderer=None): 

1234 # docstring inherited 

1235 if renderer is None: 

1236 renderer = self.figure._get_renderer() 

1237 w, h, xd, yd = self.get_extent(renderer) 

1238 ox, oy = self.get_offset() 

1239 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) 

1240 

1241 def get_extent(self, renderer): 

1242 if self._dpi_cor: # True, do correction 

1243 dpi_cor = renderer.points_to_pixels(1.) 

1244 else: 

1245 dpi_cor = 1. 

1246 

1247 zoom = self.get_zoom() 

1248 data = self.get_data() 

1249 ny, nx = data.shape[:2] 

1250 w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom 

1251 

1252 return w, h, 0, 0 

1253 

1254 def draw(self, renderer): 

1255 # docstring inherited 

1256 self.image.draw(renderer) 

1257 # bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

1258 self.stale = False 

1259 

1260 

1261class AnnotationBbox(martist.Artist, mtext._AnnotationBase): 

1262 """ 

1263 Container for an `OffsetBox` referring to a specific position *xy*. 

1264 

1265 Optionally an arrow pointing from the offsetbox to *xy* can be drawn. 

1266 

1267 This is like `.Annotation`, but with `OffsetBox` instead of `.Text`. 

1268 """ 

1269 

1270 zorder = 3 

1271 

1272 def __str__(self): 

1273 return "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1]) 

1274 

1275 @_docstring.dedent_interpd 

1276 @_api.make_keyword_only("3.6", name="xycoords") 

1277 def __init__(self, offsetbox, xy, 

1278 xybox=None, 

1279 xycoords='data', 

1280 boxcoords=None, 

1281 frameon=True, pad=0.4, # FancyBboxPatch boxstyle. 

1282 annotation_clip=None, 

1283 box_alignment=(0.5, 0.5), 

1284 bboxprops=None, 

1285 arrowprops=None, 

1286 fontsize=None, 

1287 **kwargs): 

1288 """ 

1289 Parameters 

1290 ---------- 

1291 offsetbox : `OffsetBox` 

1292 

1293 xy : (float, float) 

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

1295 by *xycoords*. 

1296 

1297 xybox : (float, float), default: *xy* 

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

1299 is determined by *boxcoords*. 

1300 

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

1302callable, default: 'data' 

1303 The coordinate system that *xy* is given in. See the parameter 

1304 *xycoords* in `.Annotation` for a detailed description. 

1305 

1306 boxcoords : single or two-tuple of str or `.Artist` or `.Transform` \ 

1307or callable, default: value of *xycoords* 

1308 The coordinate system that *xybox* is given in. See the parameter 

1309 *textcoords* in `.Annotation` for a detailed description. 

1310 

1311 frameon : bool, default: True 

1312 By default, the text is surrounded by a white `.FancyBboxPatch` 

1313 (accessible as the ``patch`` attribute of the `.AnnotationBbox`). 

1314 If *frameon* is set to False, this patch is made invisible. 

1315 

1316 annotation_clip: bool or None, default: None 

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

1318 point *xy* is outside the axes area. 

1319 

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

1321 the axes. 

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

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

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

1325 

1326 pad : float, default: 0.4 

1327 Padding around the offsetbox. 

1328 

1329 box_alignment : (float, float) 

1330 A tuple of two floats for a vertical and horizontal alignment of 

1331 the offset box w.r.t. the *boxcoords*. 

1332 The lower-left corner is (0, 0) and upper-right corner is (1, 1). 

1333 

1334 bboxprops : dict, optional 

1335 A dictionary of properties to set for the annotation bounding box, 

1336 for example *boxstyle* and *alpha*. See `.FancyBboxPatch` for 

1337 details. 

1338 

1339 arrowprops: dict, optional 

1340 Arrow properties, see `.Annotation` for description. 

1341 

1342 fontsize: float or str, optional 

1343 Translated to points and passed as *mutation_scale* into 

1344 `.FancyBboxPatch` to scale attributes of the box style (e.g. pad 

1345 or rounding_size). The name is chosen in analogy to `.Text` where 

1346 *fontsize* defines the mutation scale as well. If not given, 

1347 :rc:`legend.fontsize` is used. See `.Text.set_fontsize` for valid 

1348 values. 

1349 

1350 **kwargs 

1351 Other `AnnotationBbox` properties. See `.AnnotationBbox.set` for 

1352 a list. 

1353 """ 

1354 

1355 martist.Artist.__init__(self) 

1356 mtext._AnnotationBase.__init__( 

1357 self, xy, xycoords=xycoords, annotation_clip=annotation_clip) 

1358 

1359 self.offsetbox = offsetbox 

1360 self.arrowprops = arrowprops.copy() if arrowprops is not None else None 

1361 self.set_fontsize(fontsize) 

1362 self.xybox = xybox if xybox is not None else xy 

1363 self.boxcoords = boxcoords if boxcoords is not None else xycoords 

1364 self._box_alignment = box_alignment 

1365 

1366 if arrowprops is not None: 

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

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

1369 **self.arrowprops) 

1370 else: 

1371 self._arrow_relpos = None 

1372 self.arrow_patch = None 

1373 

1374 self.patch = FancyBboxPatch( # frame 

1375 xy=(0.0, 0.0), width=1., height=1., 

1376 facecolor='w', edgecolor='k', 

1377 mutation_scale=self.prop.get_size_in_points(), 

1378 snap=True, 

1379 visible=frameon, 

1380 ) 

1381 self.patch.set_boxstyle("square", pad=pad) 

1382 if bboxprops: 

1383 self.patch.set(**bboxprops) 

1384 

1385 self._internal_update(kwargs) 

1386 

1387 @property 

1388 def xyann(self): 

1389 return self.xybox 

1390 

1391 @xyann.setter 

1392 def xyann(self, xyann): 

1393 self.xybox = xyann 

1394 self.stale = True 

1395 

1396 @property 

1397 def anncoords(self): 

1398 return self.boxcoords 

1399 

1400 @anncoords.setter 

1401 def anncoords(self, coords): 

1402 self.boxcoords = coords 

1403 self.stale = True 

1404 

1405 def contains(self, mouseevent): 

1406 inside, info = self._default_contains(mouseevent) 

1407 if inside is not None: 

1408 return inside, info 

1409 if not self._check_xy(None): 

1410 return False, {} 

1411 return self.offsetbox.contains(mouseevent) 

1412 # self.arrow_patch is currently not checked as this can be a line - JJ 

1413 

1414 def get_children(self): 

1415 children = [self.offsetbox, self.patch] 

1416 if self.arrow_patch: 

1417 children.append(self.arrow_patch) 

1418 return children 

1419 

1420 def set_figure(self, fig): 

1421 if self.arrow_patch is not None: 

1422 self.arrow_patch.set_figure(fig) 

1423 self.offsetbox.set_figure(fig) 

1424 martist.Artist.set_figure(self, fig) 

1425 

1426 def set_fontsize(self, s=None): 

1427 """ 

1428 Set the fontsize in points. 

1429 

1430 If *s* is not given, reset to :rc:`legend.fontsize`. 

1431 """ 

1432 if s is None: 

1433 s = mpl.rcParams["legend.fontsize"] 

1434 

1435 self.prop = FontProperties(size=s) 

1436 self.stale = True 

1437 

1438 def get_fontsize(self): 

1439 """Return the fontsize in points.""" 

1440 return self.prop.get_size_in_points() 

1441 

1442 def get_window_extent(self, renderer=None): 

1443 # docstring inherited 

1444 if renderer is None: 

1445 renderer = self.figure._get_renderer() 

1446 return Bbox.union([child.get_window_extent(renderer) 

1447 for child in self.get_children()]) 

1448 

1449 def get_tightbbox(self, renderer=None): 

1450 # docstring inherited 

1451 return Bbox.union([child.get_tightbbox(renderer) 

1452 for child in self.get_children()]) 

1453 

1454 def update_positions(self, renderer): 

1455 """ 

1456 Update pixel positions for the annotated point, the text and the arrow. 

1457 """ 

1458 

1459 x, y = self.xybox 

1460 if isinstance(self.boxcoords, tuple): 

1461 xcoord, ycoord = self.boxcoords 

1462 x1, y1 = self._get_xy(renderer, x, y, xcoord) 

1463 x2, y2 = self._get_xy(renderer, x, y, ycoord) 

1464 ox0, oy0 = x1, y2 

1465 else: 

1466 ox0, oy0 = self._get_xy(renderer, x, y, self.boxcoords) 

1467 

1468 w, h, xd, yd = self.offsetbox.get_extent(renderer) 

1469 fw, fh = self._box_alignment 

1470 self.offsetbox.set_offset((ox0 - fw * w + xd, oy0 - fh * h + yd)) 

1471 

1472 bbox = self.offsetbox.get_window_extent(renderer) 

1473 self.patch.set_bounds(bbox.bounds) 

1474 

1475 mutation_scale = renderer.points_to_pixels(self.get_fontsize()) 

1476 self.patch.set_mutation_scale(mutation_scale) 

1477 

1478 if self.arrowprops: 

1479 # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key. 

1480 

1481 # Adjust the starting point of the arrow relative to the textbox. 

1482 # TODO: Rotation needs to be accounted. 

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

1484 arrow_end = self._get_position_xy(renderer) 

1485 # The arrow (from arrow_begin to arrow_end) will be first clipped 

1486 # by patchA and patchB, then shrunk by shrinkA and shrinkB (in 

1487 # points). If patch A is not set, self.bbox_patch is used. 

1488 self.arrow_patch.set_positions(arrow_begin, arrow_end) 

1489 

1490 if "mutation_scale" in self.arrowprops: 

1491 mutation_scale = renderer.points_to_pixels( 

1492 self.arrowprops["mutation_scale"]) 

1493 # Else, use fontsize-based mutation_scale defined above. 

1494 self.arrow_patch.set_mutation_scale(mutation_scale) 

1495 

1496 patchA = self.arrowprops.get("patchA", self.patch) 

1497 self.arrow_patch.set_patchA(patchA) 

1498 

1499 def draw(self, renderer): 

1500 # docstring inherited 

1501 if renderer is not None: 

1502 self._renderer = renderer 

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

1504 return 

1505 self.update_positions(renderer) 

1506 if self.arrow_patch is not None: 

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

1508 self.arrow_patch.figure = self.figure 

1509 self.arrow_patch.draw(renderer) 

1510 self.patch.draw(renderer) 

1511 self.offsetbox.draw(renderer) 

1512 self.stale = False 

1513 

1514 

1515class DraggableBase: 

1516 """ 

1517 Helper base class for a draggable artist (legend, offsetbox). 

1518 

1519 Derived classes must override the following methods:: 

1520 

1521 def save_offset(self): 

1522 ''' 

1523 Called when the object is picked for dragging; should save the 

1524 reference position of the artist. 

1525 ''' 

1526 

1527 def update_offset(self, dx, dy): 

1528 ''' 

1529 Called during the dragging; (*dx*, *dy*) is the pixel offset from 

1530 the point where the mouse drag started. 

1531 ''' 

1532 

1533 Optionally, you may override the following method:: 

1534 

1535 def finalize_offset(self): 

1536 '''Called when the mouse is released.''' 

1537 

1538 In the current implementation of `.DraggableLegend` and 

1539 `DraggableAnnotation`, `update_offset` places the artists in display 

1540 coordinates, and `finalize_offset` recalculates their position in axes 

1541 coordinate and set a relevant attribute. 

1542 """ 

1543 

1544 def __init__(self, ref_artist, use_blit=False): 

1545 self.ref_artist = ref_artist 

1546 if not ref_artist.pickable(): 

1547 ref_artist.set_picker(True) 

1548 self.got_artist = False 

1549 self.canvas = self.ref_artist.figure.canvas 

1550 self._use_blit = use_blit and self.canvas.supports_blit 

1551 self.cids = [ 

1552 self.canvas.callbacks._connect_picklable( 

1553 'pick_event', self.on_pick), 

1554 self.canvas.callbacks._connect_picklable( 

1555 'button_release_event', self.on_release), 

1556 ] 

1557 

1558 def on_motion(self, evt): 

1559 if self._check_still_parented() and self.got_artist: 

1560 dx = evt.x - self.mouse_x 

1561 dy = evt.y - self.mouse_y 

1562 self.update_offset(dx, dy) 

1563 if self._use_blit: 

1564 self.canvas.restore_region(self.background) 

1565 self.ref_artist.draw( 

1566 self.ref_artist.figure._get_renderer()) 

1567 self.canvas.blit() 

1568 else: 

1569 self.canvas.draw() 

1570 

1571 def on_pick(self, evt): 

1572 if self._check_still_parented() and evt.artist == self.ref_artist: 

1573 self.mouse_x = evt.mouseevent.x 

1574 self.mouse_y = evt.mouseevent.y 

1575 self.got_artist = True 

1576 if self._use_blit: 

1577 self.ref_artist.set_animated(True) 

1578 self.canvas.draw() 

1579 self.background = \ 

1580 self.canvas.copy_from_bbox(self.ref_artist.figure.bbox) 

1581 self.ref_artist.draw( 

1582 self.ref_artist.figure._get_renderer()) 

1583 self.canvas.blit() 

1584 self._c1 = self.canvas.callbacks._connect_picklable( 

1585 "motion_notify_event", self.on_motion) 

1586 self.save_offset() 

1587 

1588 def on_release(self, event): 

1589 if self._check_still_parented() and self.got_artist: 

1590 self.finalize_offset() 

1591 self.got_artist = False 

1592 self.canvas.mpl_disconnect(self._c1) 

1593 

1594 if self._use_blit: 

1595 self.ref_artist.set_animated(False) 

1596 

1597 def _check_still_parented(self): 

1598 if self.ref_artist.figure is None: 

1599 self.disconnect() 

1600 return False 

1601 else: 

1602 return True 

1603 

1604 def disconnect(self): 

1605 """Disconnect the callbacks.""" 

1606 for cid in self.cids: 

1607 self.canvas.mpl_disconnect(cid) 

1608 try: 

1609 c1 = self._c1 

1610 except AttributeError: 

1611 pass 

1612 else: 

1613 self.canvas.mpl_disconnect(c1) 

1614 

1615 def save_offset(self): 

1616 pass 

1617 

1618 def update_offset(self, dx, dy): 

1619 pass 

1620 

1621 def finalize_offset(self): 

1622 pass 

1623 

1624 

1625class DraggableOffsetBox(DraggableBase): 

1626 def __init__(self, ref_artist, offsetbox, use_blit=False): 

1627 super().__init__(ref_artist, use_blit=use_blit) 

1628 self.offsetbox = offsetbox 

1629 

1630 def save_offset(self): 

1631 offsetbox = self.offsetbox 

1632 renderer = offsetbox.figure._get_renderer() 

1633 w, h, xd, yd = offsetbox.get_extent(renderer) 

1634 offset = offsetbox.get_offset(w, h, xd, yd, renderer) 

1635 self.offsetbox_x, self.offsetbox_y = offset 

1636 self.offsetbox.set_offset(offset) 

1637 

1638 def update_offset(self, dx, dy): 

1639 loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy 

1640 self.offsetbox.set_offset(loc_in_canvas) 

1641 

1642 def get_loc_in_canvas(self): 

1643 offsetbox = self.offsetbox 

1644 renderer = offsetbox.figure._get_renderer() 

1645 w, h, xd, yd = offsetbox.get_extent(renderer) 

1646 ox, oy = offsetbox._offset 

1647 loc_in_canvas = (ox - xd, oy - yd) 

1648 return loc_in_canvas 

1649 

1650 

1651class DraggableAnnotation(DraggableBase): 

1652 def __init__(self, annotation, use_blit=False): 

1653 super().__init__(annotation, use_blit=use_blit) 

1654 self.annotation = annotation 

1655 

1656 def save_offset(self): 

1657 ann = self.annotation 

1658 self.ox, self.oy = ann.get_transform().transform(ann.xyann) 

1659 

1660 def update_offset(self, dx, dy): 

1661 ann = self.annotation 

1662 ann.xyann = ann.get_transform().inverted().transform( 

1663 (self.ox + dx, self.oy + dy))