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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

635 statements  

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 functools 

26 

27import numpy as np 

28 

29import matplotlib as mpl 

30from matplotlib import _api, _docstring 

31import matplotlib.artist as martist 

32import matplotlib.path as mpath 

33import matplotlib.text as mtext 

34import matplotlib.transforms as mtransforms 

35from matplotlib.font_manager import FontProperties 

36from matplotlib.image import BboxImage 

37from matplotlib.patches import ( 

38 FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist) 

39from matplotlib.transforms import Bbox, BboxBase, TransformedBbox 

40 

41 

42DEBUG = False 

43 

44 

45def _compat_get_offset(meth): 

46 """ 

47 Decorator for the get_offset method of OffsetBox and subclasses, that 

48 allows supporting both the new signature (self, bbox, renderer) and the old 

49 signature (self, width, height, xdescent, ydescent, renderer). 

50 """ 

51 sigs = [lambda self, width, height, xdescent, ydescent, renderer: locals(), 

52 lambda self, bbox, renderer: locals()] 

53 

54 @functools.wraps(meth) 

55 def get_offset(self, *args, **kwargs): 

56 params = _api.select_matching_signature(sigs, self, *args, **kwargs) 

57 bbox = (params["bbox"] if "bbox" in params else 

58 Bbox.from_bounds(-params["xdescent"], -params["ydescent"], 

59 params["width"], params["height"])) 

60 return meth(params["self"], bbox, params["renderer"]) 

61 return get_offset 

62 

63 

64# for debugging use 

65def _bbox_artist(*args, **kwargs): 

66 if DEBUG: 

67 mbbox_artist(*args, **kwargs) 

68 

69 

70def _get_packed_offsets(widths, total, sep, mode="fixed"): 

71 r""" 

72 Pack boxes specified by their *widths*. 

73 

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

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

76 

77 There are three packing *mode*\s: 

78 

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

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

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

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

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

84 

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

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

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

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

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

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

91 

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

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

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

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

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

97 

98 Parameters 

99 ---------- 

100 widths : list of float 

101 Widths of boxes to be packed. 

102 total : float or None 

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

104 sep : float or None 

105 Spacing between boxes. 

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

107 The packing mode. 

108 

109 Returns 

110 ------- 

111 total : float 

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

113 offsets : array of float 

114 The left offsets of the boxes. 

115 """ 

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

117 

118 if mode == "fixed": 

119 offsets_ = np.cumsum([0] + [w + sep for w in widths]) 

120 offsets = offsets_[:-1] 

121 if total is None: 

122 total = offsets_[-1] - sep 

123 return total, offsets 

124 

125 elif mode == "expand": 

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

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

128 if total is None: 

129 total = 1 

130 if len(widths) > 1: 

131 sep = (total - sum(widths)) / (len(widths) - 1) 

132 else: 

133 sep = 0 

134 offsets_ = np.cumsum([0] + [w + sep for w in widths]) 

135 offsets = offsets_[:-1] 

136 return total, offsets 

137 

138 elif mode == "equal": 

139 maxh = max(widths) 

140 if total is None: 

141 if sep is None: 

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

143 "using layout mode 'equal'") 

144 total = (maxh + sep) * len(widths) 

145 else: 

146 sep = total / len(widths) - maxh 

147 offsets = (maxh + sep) * np.arange(len(widths)) 

148 return total, offsets 

149 

150 

151def _get_aligned_offsets(yspans, height, align="baseline"): 

152 """ 

153 Align boxes each specified by their ``(y0, y1)`` spans. 

154 

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

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

157 equally for a vertical layout. 

158 

159 Parameters 

160 ---------- 

161 yspans 

162 List of (y0, y1) spans of boxes to be aligned. 

163 height : float or None 

164 Intended total height. If None, the maximum of the heights 

165 (``y1 - y0``) in *yspans* is used. 

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

167 The alignment anchor of the boxes. 

168 

169 Returns 

170 ------- 

171 (y0, y1) 

172 y range spanned by the packing. If a *height* was originally passed 

173 in, then for all alignments other than "baseline", a span of ``(0, 

174 height)`` is used without checking that it is actually large enough). 

175 descent 

176 The descent of the packing. 

177 offsets 

178 The bottom offsets of the boxes. 

179 """ 

180 

181 _api.check_in_list( 

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

183 if height is None: 

184 height = max(y1 - y0 for y0, y1 in yspans) 

185 

186 if align == "baseline": 

187 yspan = (min(y0 for y0, y1 in yspans), max(y1 for y0, y1 in yspans)) 

188 offsets = [0] * len(yspans) 

189 elif align in ["left", "bottom"]: 

190 yspan = (0, height) 

191 offsets = [-y0 for y0, y1 in yspans] 

192 elif align in ["right", "top"]: 

193 yspan = (0, height) 

194 offsets = [height - y1 for y0, y1 in yspans] 

195 elif align == "center": 

196 yspan = (0, height) 

197 offsets = [(height - (y1 - y0)) * .5 - y0 for y0, y1 in yspans] 

198 

199 return yspan, offsets 

200 

201 

202class OffsetBox(martist.Artist): 

203 """ 

204 The OffsetBox is a simple container artist. 

205 

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

207 parent. 

208 

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

210 """ 

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

212 super().__init__(*args) 

213 self._internal_update(kwargs) 

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

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

216 # on to zero effect. 

217 self.set_clip_on(False) 

218 self._children = [] 

219 self._offset = (0, 0) 

220 

221 def set_figure(self, fig): 

222 """ 

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

224 

225 Parameters 

226 ---------- 

227 fig : `~matplotlib.figure.Figure` 

228 """ 

229 super().set_figure(fig) 

230 for c in self.get_children(): 

231 c.set_figure(fig) 

232 

233 @martist.Artist.axes.setter 

234 def axes(self, ax): 

235 # TODO deal with this better 

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

237 for c in self.get_children(): 

238 if c is not None: 

239 c.axes = ax 

240 

241 def contains(self, mouseevent): 

242 """ 

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

244 

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

246 mouseevents. 

247 

248 Parameters 

249 ---------- 

250 mouseevent : `~matplotlib.backend_bases.MouseEvent` 

251 

252 Returns 

253 ------- 

254 contains : bool 

255 Whether any values are within the radius. 

256 details : dict 

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

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

259 individual Artist subclasses for details. 

260 

261 See Also 

262 -------- 

263 .Artist.contains 

264 """ 

265 if self._different_canvas(mouseevent): 

266 return False, {} 

267 for c in self.get_children(): 

268 a, b = c.contains(mouseevent) 

269 if a: 

270 return a, b 

271 return False, {} 

272 

273 def set_offset(self, xy): 

274 """ 

275 Set the offset. 

276 

277 Parameters 

278 ---------- 

279 xy : (float, float) or callable 

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

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

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

283 must have the signature:: 

284 

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

286-> (float, float) 

287 """ 

288 self._offset = xy 

289 self.stale = True 

290 

291 @_compat_get_offset 

292 def get_offset(self, bbox, renderer): 

293 """ 

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

295 

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

297 offset is dynamically determined by a callable (see 

298 `~.OffsetBox.set_offset`). 

299 

300 Parameters 

301 ---------- 

302 bbox : `.Bbox` 

303 renderer : `.RendererBase` subclass 

304 """ 

305 return ( 

306 self._offset(bbox.width, bbox.height, -bbox.x0, -bbox.y0, renderer) 

307 if callable(self._offset) 

308 else self._offset) 

309 

310 def set_width(self, width): 

311 """ 

312 Set the width of the box. 

313 

314 Parameters 

315 ---------- 

316 width : float 

317 """ 

318 self.width = width 

319 self.stale = True 

320 

321 def set_height(self, height): 

322 """ 

323 Set the height of the box. 

324 

325 Parameters 

326 ---------- 

327 height : float 

328 """ 

329 self.height = height 

330 self.stale = True 

331 

332 def get_visible_children(self): 

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

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

335 

336 def get_children(self): 

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

338 return self._children 

339 

340 def _get_bbox_and_child_offsets(self, renderer): 

341 """ 

342 Return the bbox of the offsetbox and the child offsets. 

343 

344 The bbox should satisfy ``x0 <= x1 and y0 <= y1``. 

345 

346 Parameters 

347 ---------- 

348 renderer : `.RendererBase` subclass 

349 

350 Returns 

351 ------- 

352 bbox 

353 list of (xoffset, yoffset) pairs 

354 """ 

355 raise NotImplementedError( 

356 "get_bbox_and_offsets must be overridden in derived classes") 

357 

358 def get_bbox(self, renderer): 

359 """Return the bbox of the offsetbox, ignoring parent offsets.""" 

360 bbox, offsets = self._get_bbox_and_child_offsets(renderer) 

361 return bbox 

362 

363 def get_window_extent(self, renderer=None): 

364 # docstring inherited 

365 if renderer is None: 

366 renderer = self.figure._get_renderer() 

367 bbox = self.get_bbox(renderer) 

368 try: # Some subclasses redefine get_offset to take no args. 

369 px, py = self.get_offset(bbox, renderer) 

370 except TypeError: 

371 px, py = self.get_offset() 

372 return bbox.translated(px, py) 

373 

374 def draw(self, renderer): 

375 """ 

376 Update the location of children if necessary and draw them 

377 to the given *renderer*. 

378 """ 

379 bbox, offsets = self._get_bbox_and_child_offsets(renderer) 

380 px, py = self.get_offset(bbox, renderer) 

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

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

383 c.draw(renderer) 

384 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

385 self.stale = False 

386 

387 

388class PackerBase(OffsetBox): 

389 def __init__(self, pad=0., sep=0., width=None, height=None, 

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

391 """ 

392 Parameters 

393 ---------- 

394 pad : float, default: 0.0 

395 The boundary padding in points. 

396 

397 sep : float, default: 0.0 

398 The spacing between items in points. 

399 

400 width, height : float, optional 

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

402 *None*. 

403 

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

405default: 'baseline' 

406 Alignment of boxes. 

407 

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

409 The packing mode. 

410 

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

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

413 artists with equal spacing in between. 

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

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

416 

417 children : list of `.Artist` 

418 The artists to pack. 

419 

420 Notes 

421 ----- 

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

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

424 """ 

425 super().__init__() 

426 self.height = height 

427 self.width = width 

428 self.sep = sep 

429 self.pad = pad 

430 self.mode = mode 

431 self.align = align 

432 self._children = children 

433 

434 

435class VPacker(PackerBase): 

436 """ 

437 VPacker packs its children vertically, automatically adjusting their 

438 relative positions at draw time. 

439 """ 

440 

441 def _get_bbox_and_child_offsets(self, renderer): 

442 # docstring inherited 

443 dpicor = renderer.points_to_pixels(1.) 

444 pad = self.pad * dpicor 

445 sep = self.sep * dpicor 

446 

447 if self.width is not None: 

448 for c in self.get_visible_children(): 

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

450 c.set_width(self.width) 

451 

452 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()] 

453 (x0, x1), xoffsets = _get_aligned_offsets( 

454 [bbox.intervalx for bbox in bboxes], self.width, self.align) 

455 height, yoffsets = _get_packed_offsets( 

456 [bbox.height for bbox in bboxes], self.height, sep, self.mode) 

457 

458 yoffsets = height - (yoffsets + [bbox.y1 for bbox in bboxes]) 

459 ydescent = yoffsets[0] 

460 yoffsets = yoffsets - ydescent 

461 

462 return ( 

463 Bbox.from_bounds(x0, -ydescent, x1 - x0, height).padded(pad), 

464 [*zip(xoffsets, yoffsets)]) 

465 

466 

467class HPacker(PackerBase): 

468 """ 

469 HPacker packs its children horizontally, automatically adjusting their 

470 relative positions at draw time. 

471 """ 

472 

473 def _get_bbox_and_child_offsets(self, renderer): 

474 # docstring inherited 

475 dpicor = renderer.points_to_pixels(1.) 

476 pad = self.pad * dpicor 

477 sep = self.sep * dpicor 

478 

479 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()] 

480 if not bboxes: 

481 return Bbox.from_bounds(0, 0, 0, 0).padded(pad), [] 

482 

483 (y0, y1), yoffsets = _get_aligned_offsets( 

484 [bbox.intervaly for bbox in bboxes], self.height, self.align) 

485 width, xoffsets = _get_packed_offsets( 

486 [bbox.width for bbox in bboxes], self.width, sep, self.mode) 

487 

488 x0 = bboxes[0].x0 

489 xoffsets -= ([bbox.x0 for bbox in bboxes] - x0) 

490 

491 return (Bbox.from_bounds(x0, y0, width, y1 - y0).padded(pad), 

492 [*zip(xoffsets, yoffsets)]) 

493 

494 

495class PaddedBox(OffsetBox): 

496 """ 

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

498 

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

500 it when rendering. 

501 """ 

502 

503 def __init__(self, child, pad=0., *, draw_frame=False, patch_attrs=None): 

504 """ 

505 Parameters 

506 ---------- 

507 child : `~matplotlib.artist.Artist` 

508 The contained `.Artist`. 

509 pad : float, default: 0.0 

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

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

512 scaled. 

513 draw_frame : bool 

514 Whether to draw the contained `.FancyBboxPatch`. 

515 patch_attrs : dict or None 

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

517 """ 

518 super().__init__() 

519 self.pad = pad 

520 self._children = [child] 

521 self.patch = FancyBboxPatch( 

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

523 facecolor='w', edgecolor='k', 

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

525 snap=True, 

526 visible=draw_frame, 

527 boxstyle="square,pad=0", 

528 ) 

529 if patch_attrs is not None: 

530 self.patch.update(patch_attrs) 

531 

532 def _get_bbox_and_child_offsets(self, renderer): 

533 # docstring inherited. 

534 pad = self.pad * renderer.points_to_pixels(1.) 

535 return (self._children[0].get_bbox(renderer).padded(pad), [(0, 0)]) 

536 

537 def draw(self, renderer): 

538 # docstring inherited 

539 bbox, offsets = self._get_bbox_and_child_offsets(renderer) 

540 px, py = self.get_offset(bbox, renderer) 

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

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

543 

544 self.draw_frame(renderer) 

545 

546 for c in self.get_visible_children(): 

547 c.draw(renderer) 

548 

549 self.stale = False 

550 

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

552 self.patch.set_bounds(bbox.bounds) 

553 if fontsize: 

554 self.patch.set_mutation_scale(fontsize) 

555 self.stale = True 

556 

557 def draw_frame(self, renderer): 

558 # update the location and size of the legend 

559 self.update_frame(self.get_window_extent(renderer)) 

560 self.patch.draw(renderer) 

561 

562 

563class DrawingArea(OffsetBox): 

564 """ 

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

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

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

568 boundaries of the parent. 

569 """ 

570 

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

572 """ 

573 Parameters 

574 ---------- 

575 width, height : float 

576 Width and height of the container box. 

577 xdescent, ydescent : float 

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

579 clip : bool 

580 Whether to clip the children to the box. 

581 """ 

582 super().__init__() 

583 self.width = width 

584 self.height = height 

585 self.xdescent = xdescent 

586 self.ydescent = ydescent 

587 self._clip_children = clip 

588 self.offset_transform = mtransforms.Affine2D() 

589 self.dpi_transform = mtransforms.Affine2D() 

590 

591 @property 

592 def clip_children(self): 

593 """ 

594 If the children of this DrawingArea should be clipped 

595 by DrawingArea bounding box. 

596 """ 

597 return self._clip_children 

598 

599 @clip_children.setter 

600 def clip_children(self, val): 

601 self._clip_children = bool(val) 

602 self.stale = True 

603 

604 def get_transform(self): 

605 """ 

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

607 """ 

608 return self.dpi_transform + self.offset_transform 

609 

610 def set_transform(self, t): 

611 """ 

612 set_transform is ignored. 

613 """ 

614 

615 def set_offset(self, xy): 

616 """ 

617 Set the offset of the container. 

618 

619 Parameters 

620 ---------- 

621 xy : (float, float) 

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

623 """ 

624 self._offset = xy 

625 self.offset_transform.clear() 

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

627 self.stale = True 

628 

629 def get_offset(self): 

630 """Return offset of the container.""" 

631 return self._offset 

632 

633 def get_bbox(self, renderer): 

634 # docstring inherited 

635 dpi_cor = renderer.points_to_pixels(1.) 

636 return Bbox.from_bounds( 

637 -self.xdescent * dpi_cor, -self.ydescent * dpi_cor, 

638 self.width * dpi_cor, self.height * dpi_cor) 

639 

640 def add_artist(self, a): 

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

642 self._children.append(a) 

643 if not a.is_transform_set(): 

644 a.set_transform(self.get_transform()) 

645 if self.axes is not None: 

646 a.axes = self.axes 

647 fig = self.figure 

648 if fig is not None: 

649 a.set_figure(fig) 

650 

651 def draw(self, renderer): 

652 # docstring inherited 

653 

654 dpi_cor = renderer.points_to_pixels(1.) 

655 self.dpi_transform.clear() 

656 self.dpi_transform.scale(dpi_cor) 

657 

658 # At this point the DrawingArea has a transform 

659 # to the display space so the path created is 

660 # good for clipping children 

661 tpath = mtransforms.TransformedPath( 

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

663 [self.width, self.height], 

664 [self.width, 0]]), 

665 self.get_transform()) 

666 for c in self._children: 

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

668 c.set_clip_path(tpath) 

669 c.draw(renderer) 

670 

671 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

672 self.stale = False 

673 

674 

675class TextArea(OffsetBox): 

676 """ 

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

678 

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

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

681 child text. 

682 """ 

683 

684 def __init__(self, s, 

685 *, 

686 textprops=None, 

687 multilinebaseline=False, 

688 ): 

689 """ 

690 Parameters 

691 ---------- 

692 s : str 

693 The text to be displayed. 

694 textprops : dict, default: {} 

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

696 instance in the TextArea. 

697 multilinebaseline : bool, default: False 

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

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

700 """ 

701 if textprops is None: 

702 textprops = {} 

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

704 super().__init__() 

705 self._children = [self._text] 

706 self.offset_transform = mtransforms.Affine2D() 

707 self._baseline_transform = mtransforms.Affine2D() 

708 self._text.set_transform(self.offset_transform + 

709 self._baseline_transform) 

710 self._multilinebaseline = multilinebaseline 

711 

712 def set_text(self, s): 

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

714 self._text.set_text(s) 

715 self.stale = True 

716 

717 def get_text(self): 

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

719 return self._text.get_text() 

720 

721 def set_multilinebaseline(self, t): 

722 """ 

723 Set multilinebaseline. 

724 

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

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

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

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

729 """ 

730 self._multilinebaseline = t 

731 self.stale = True 

732 

733 def get_multilinebaseline(self): 

734 """ 

735 Get multilinebaseline. 

736 """ 

737 return self._multilinebaseline 

738 

739 def set_transform(self, t): 

740 """ 

741 set_transform is ignored. 

742 """ 

743 

744 def set_offset(self, xy): 

745 """ 

746 Set the offset of the container. 

747 

748 Parameters 

749 ---------- 

750 xy : (float, float) 

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

752 """ 

753 self._offset = xy 

754 self.offset_transform.clear() 

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

756 self.stale = True 

757 

758 def get_offset(self): 

759 """Return offset of the container.""" 

760 return self._offset 

761 

762 def get_bbox(self, renderer): 

763 _, h_, d_ = renderer.get_text_width_height_descent( 

764 "lp", self._text._fontproperties, 

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

766 

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

768 w, h = bbox.size 

769 

770 self._baseline_transform.clear() 

771 

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

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

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

775 yd = yd_new 

776 else: # single line 

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

778 h = h_d + yd 

779 

780 ha = self._text.get_horizontalalignment() 

781 x0 = {"left": 0, "center": -w / 2, "right": -w}[ha] 

782 

783 return Bbox.from_bounds(x0, -yd, w, h) 

784 

785 def draw(self, renderer): 

786 # docstring inherited 

787 self._text.draw(renderer) 

788 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

789 self.stale = False 

790 

791 

792class AuxTransformBox(OffsetBox): 

793 """ 

794 Offset Box with the aux_transform. Its children will be 

795 transformed with the aux_transform first then will be 

796 offsetted. The absolute coordinate of the aux_transform is meaning 

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

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

799 offset transform. 

800 

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

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

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

804 calculated in the transformed coordinate. 

805 """ 

806 def __init__(self, aux_transform): 

807 self.aux_transform = aux_transform 

808 super().__init__() 

809 self.offset_transform = mtransforms.Affine2D() 

810 # ref_offset_transform makes offset_transform always relative to the 

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

812 self.ref_offset_transform = mtransforms.Affine2D() 

813 

814 def add_artist(self, a): 

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

816 self._children.append(a) 

817 a.set_transform(self.get_transform()) 

818 self.stale = True 

819 

820 def get_transform(self): 

821 """ 

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

823 to the children 

824 """ 

825 return (self.aux_transform 

826 + self.ref_offset_transform 

827 + self.offset_transform) 

828 

829 def set_transform(self, t): 

830 """ 

831 set_transform is ignored. 

832 """ 

833 

834 def set_offset(self, xy): 

835 """ 

836 Set the offset of the container. 

837 

838 Parameters 

839 ---------- 

840 xy : (float, float) 

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

842 """ 

843 self._offset = xy 

844 self.offset_transform.clear() 

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

846 self.stale = True 

847 

848 def get_offset(self): 

849 """Return offset of the container.""" 

850 return self._offset 

851 

852 def get_bbox(self, renderer): 

853 # clear the offset transforms 

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

855 self.ref_offset_transform.clear() 

856 self.offset_transform.clear() 

857 # calculate the extent 

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

859 ub = Bbox.union(bboxes) 

860 # adjust ref_offset_transform 

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

862 # restore offset transform 

863 self.offset_transform.set_matrix(_off) 

864 return Bbox.from_bounds(0, 0, ub.width, ub.height) 

865 

866 def draw(self, renderer): 

867 # docstring inherited 

868 for c in self._children: 

869 c.draw(renderer) 

870 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) 

871 self.stale = False 

872 

873 

874class AnchoredOffsetbox(OffsetBox): 

875 """ 

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

877 

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

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

880 anchored against its parent Axes. You may explicitly specify the 

881 *bbox_to_anchor*. 

882 """ 

883 zorder = 5 # zorder of the legend 

884 

885 # Location codes 

886 codes = {'upper right': 1, 

887 'upper left': 2, 

888 'lower left': 3, 

889 'lower right': 4, 

890 'right': 5, 

891 'center left': 6, 

892 'center right': 7, 

893 'lower center': 8, 

894 'upper center': 9, 

895 'center': 10, 

896 } 

897 

898 def __init__(self, loc, *, 

899 pad=0.4, borderpad=0.5, 

900 child=None, prop=None, frameon=True, 

901 bbox_to_anchor=None, 

902 bbox_transform=None, 

903 **kwargs): 

904 """ 

905 Parameters 

906 ---------- 

907 loc : str 

908 The box location. Valid locations are 

909 'upper left', 'upper center', 'upper right', 

910 'center left', 'center', 'center right', 

911 'lower left', 'lower center', 'lower right'. 

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

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

914 pad : float, default: 0.4 

915 Padding around the child as fraction of the fontsize. 

916 borderpad : float, default: 0.5 

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

918 child : `.OffsetBox` 

919 The box that will be anchored. 

920 prop : `.FontProperties` 

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

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

923 frameon : bool 

924 Whether to draw a frame around the box. 

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

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

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

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

929 **kwargs 

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

931 

932 Notes 

933 ----- 

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

935 """ 

936 super().__init__(**kwargs) 

937 

938 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) 

939 self.set_child(child) 

940 

941 if isinstance(loc, str): 

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

943 

944 self.loc = loc 

945 self.borderpad = borderpad 

946 self.pad = pad 

947 

948 if prop is None: 

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

950 else: 

951 self.prop = FontProperties._from_any(prop) 

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

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

954 

955 self.patch = FancyBboxPatch( 

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

957 facecolor='w', edgecolor='k', 

958 mutation_scale=self.prop.get_size_in_points(), 

959 snap=True, 

960 visible=frameon, 

961 boxstyle="square,pad=0", 

962 ) 

963 

964 def set_child(self, child): 

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

966 self._child = child 

967 if child is not None: 

968 child.axes = self.axes 

969 self.stale = True 

970 

971 def get_child(self): 

972 """Return the child.""" 

973 return self._child 

974 

975 def get_children(self): 

976 """Return the list of children.""" 

977 return [self._child] 

978 

979 def get_bbox(self, renderer): 

980 # docstring inherited 

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

982 pad = self.pad * fontsize 

983 return self.get_child().get_bbox(renderer).padded(pad) 

984 

985 def get_bbox_to_anchor(self): 

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

987 if self._bbox_to_anchor is None: 

988 return self.axes.bbox 

989 else: 

990 transform = self._bbox_to_anchor_transform 

991 if transform is None: 

992 return self._bbox_to_anchor 

993 else: 

994 return TransformedBbox(self._bbox_to_anchor, transform) 

995 

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

997 """ 

998 Set the bbox that the box is anchored to. 

999 

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

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

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

1003 transformed to display coordinate by the given transform. 

1004 """ 

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

1006 self._bbox_to_anchor = bbox 

1007 else: 

1008 try: 

1009 l = len(bbox) 

1010 except TypeError as err: 

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

1012 

1013 if l == 2: 

1014 bbox = [bbox[0], bbox[1], 0, 0] 

1015 

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

1017 

1018 self._bbox_to_anchor_transform = transform 

1019 self.stale = True 

1020 

1021 @_compat_get_offset 

1022 def get_offset(self, bbox, renderer): 

1023 # docstring inherited 

1024 pad = (self.borderpad 

1025 * renderer.points_to_pixels(self.prop.get_size_in_points())) 

1026 bbox_to_anchor = self.get_bbox_to_anchor() 

1027 x0, y0 = _get_anchored_bbox( 

1028 self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height), 

1029 bbox_to_anchor, pad) 

1030 return x0 - bbox.x0, y0 - bbox.y0 

1031 

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

1033 self.patch.set_bounds(bbox.bounds) 

1034 if fontsize: 

1035 self.patch.set_mutation_scale(fontsize) 

1036 

1037 def draw(self, renderer): 

1038 # docstring inherited 

1039 if not self.get_visible(): 

1040 return 

1041 

1042 # update the location and size of the legend 

1043 bbox = self.get_window_extent(renderer) 

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

1045 self.update_frame(bbox, fontsize) 

1046 self.patch.draw(renderer) 

1047 

1048 px, py = self.get_offset(self.get_bbox(renderer), renderer) 

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

1050 self.get_child().draw(renderer) 

1051 self.stale = False 

1052 

1053 

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

1055 """ 

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

1057 the *loc* code with the *borderpad*. 

1058 """ 

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

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

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

1062 container = parentbbox.padded(-borderpad) 

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

1064 

1065 

1066class AnchoredText(AnchoredOffsetbox): 

1067 """ 

1068 AnchoredOffsetbox with Text. 

1069 """ 

1070 

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

1072 """ 

1073 Parameters 

1074 ---------- 

1075 s : str 

1076 Text. 

1077 

1078 loc : str 

1079 Location code. See `AnchoredOffsetbox`. 

1080 

1081 pad : float, default: 0.4 

1082 Padding around the text as fraction of the fontsize. 

1083 

1084 borderpad : float, default: 0.5 

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

1086 

1087 prop : dict, optional 

1088 Dictionary of keyword parameters to be passed to the 

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

1090 

1091 **kwargs 

1092 All other parameters are passed to `AnchoredOffsetbox`. 

1093 """ 

1094 

1095 if prop is None: 

1096 prop = {} 

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

1098 if badkwargs & set(prop): 

1099 raise ValueError( 

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

1101 

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

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

1104 super().__init__( 

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

1106 **kwargs) 

1107 

1108 

1109class OffsetImage(OffsetBox): 

1110 

1111 def __init__(self, arr, *, 

1112 zoom=1, 

1113 cmap=None, 

1114 norm=None, 

1115 interpolation=None, 

1116 origin=None, 

1117 filternorm=True, 

1118 filterrad=4.0, 

1119 resample=False, 

1120 dpi_cor=True, 

1121 **kwargs 

1122 ): 

1123 

1124 super().__init__() 

1125 self._dpi_cor = dpi_cor 

1126 

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

1128 cmap=cmap, 

1129 norm=norm, 

1130 interpolation=interpolation, 

1131 origin=origin, 

1132 filternorm=filternorm, 

1133 filterrad=filterrad, 

1134 resample=resample, 

1135 **kwargs 

1136 ) 

1137 

1138 self._children = [self.image] 

1139 

1140 self.set_zoom(zoom) 

1141 self.set_data(arr) 

1142 

1143 def set_data(self, arr): 

1144 self._data = np.asarray(arr) 

1145 self.image.set_data(self._data) 

1146 self.stale = True 

1147 

1148 def get_data(self): 

1149 return self._data 

1150 

1151 def set_zoom(self, zoom): 

1152 self._zoom = zoom 

1153 self.stale = True 

1154 

1155 def get_zoom(self): 

1156 return self._zoom 

1157 

1158 def get_offset(self): 

1159 """Return offset of the container.""" 

1160 return self._offset 

1161 

1162 def get_children(self): 

1163 return [self.image] 

1164 

1165 def get_bbox(self, renderer): 

1166 dpi_cor = renderer.points_to_pixels(1.) if self._dpi_cor else 1. 

1167 zoom = self.get_zoom() 

1168 data = self.get_data() 

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

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

1171 return Bbox.from_bounds(0, 0, w, h) 

1172 

1173 def draw(self, renderer): 

1174 # docstring inherited 

1175 self.image.draw(renderer) 

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

1177 self.stale = False 

1178 

1179 

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

1181 """ 

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

1183 

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

1185 

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

1187 """ 

1188 

1189 zorder = 3 

1190 

1191 def __str__(self): 

1192 return f"AnnotationBbox({self.xy[0]:g},{self.xy[1]:g})" 

1193 

1194 @_docstring.dedent_interpd 

1195 def __init__(self, offsetbox, xy, xybox=None, xycoords='data', boxcoords=None, *, 

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

1197 annotation_clip=None, 

1198 box_alignment=(0.5, 0.5), 

1199 bboxprops=None, 

1200 arrowprops=None, 

1201 fontsize=None, 

1202 **kwargs): 

1203 """ 

1204 Parameters 

1205 ---------- 

1206 offsetbox : `OffsetBox` 

1207 

1208 xy : (float, float) 

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

1210 by *xycoords*. 

1211 

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

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

1214 is determined by *boxcoords*. 

1215 

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

1217callable, default: 'data' 

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

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

1220 

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

1222or callable, default: value of *xycoords* 

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

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

1225 

1226 frameon : bool, default: True 

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

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

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

1230 

1231 annotation_clip: bool or None, default: None 

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

1233 point *xy* is outside the Axes area. 

1234 

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

1236 the Axes. 

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

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

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

1240 

1241 pad : float, default: 0.4 

1242 Padding around the offsetbox. 

1243 

1244 box_alignment : (float, float) 

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

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

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

1248 

1249 bboxprops : dict, optional 

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

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

1252 details. 

1253 

1254 arrowprops: dict, optional 

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

1256 

1257 fontsize: float or str, optional 

1258 Translated to points and passed as *mutation_scale* into 

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

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

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

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

1263 values. 

1264 

1265 **kwargs 

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

1267 a list. 

1268 """ 

1269 

1270 martist.Artist.__init__(self) 

1271 mtext._AnnotationBase.__init__( 

1272 self, xy, xycoords=xycoords, annotation_clip=annotation_clip) 

1273 

1274 self.offsetbox = offsetbox 

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

1276 self.set_fontsize(fontsize) 

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

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

1279 self._box_alignment = box_alignment 

1280 

1281 if arrowprops is not None: 

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

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

1284 **self.arrowprops) 

1285 else: 

1286 self._arrow_relpos = None 

1287 self.arrow_patch = None 

1288 

1289 self.patch = FancyBboxPatch( # frame 

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

1291 facecolor='w', edgecolor='k', 

1292 mutation_scale=self.prop.get_size_in_points(), 

1293 snap=True, 

1294 visible=frameon, 

1295 ) 

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

1297 if bboxprops: 

1298 self.patch.set(**bboxprops) 

1299 

1300 self._internal_update(kwargs) 

1301 

1302 @property 

1303 def xyann(self): 

1304 return self.xybox 

1305 

1306 @xyann.setter 

1307 def xyann(self, xyann): 

1308 self.xybox = xyann 

1309 self.stale = True 

1310 

1311 @property 

1312 def anncoords(self): 

1313 return self.boxcoords 

1314 

1315 @anncoords.setter 

1316 def anncoords(self, coords): 

1317 self.boxcoords = coords 

1318 self.stale = True 

1319 

1320 def contains(self, mouseevent): 

1321 if self._different_canvas(mouseevent): 

1322 return False, {} 

1323 if not self._check_xy(None): 

1324 return False, {} 

1325 return self.offsetbox.contains(mouseevent) 

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

1327 

1328 def get_children(self): 

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

1330 if self.arrow_patch: 

1331 children.append(self.arrow_patch) 

1332 return children 

1333 

1334 def set_figure(self, fig): 

1335 if self.arrow_patch is not None: 

1336 self.arrow_patch.set_figure(fig) 

1337 self.offsetbox.set_figure(fig) 

1338 martist.Artist.set_figure(self, fig) 

1339 

1340 def set_fontsize(self, s=None): 

1341 """ 

1342 Set the fontsize in points. 

1343 

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

1345 """ 

1346 if s is None: 

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

1348 

1349 self.prop = FontProperties(size=s) 

1350 self.stale = True 

1351 

1352 def get_fontsize(self): 

1353 """Return the fontsize in points.""" 

1354 return self.prop.get_size_in_points() 

1355 

1356 def get_window_extent(self, renderer=None): 

1357 # docstring inherited 

1358 if renderer is None: 

1359 renderer = self.figure._get_renderer() 

1360 self.update_positions(renderer) 

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

1362 for child in self.get_children()]) 

1363 

1364 def get_tightbbox(self, renderer=None): 

1365 # docstring inherited 

1366 if renderer is None: 

1367 renderer = self.figure._get_renderer() 

1368 self.update_positions(renderer) 

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

1370 for child in self.get_children()]) 

1371 

1372 def update_positions(self, renderer): 

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

1374 

1375 ox0, oy0 = self._get_xy(renderer, self.xybox, self.boxcoords) 

1376 bbox = self.offsetbox.get_bbox(renderer) 

1377 fw, fh = self._box_alignment 

1378 self.offsetbox.set_offset( 

1379 (ox0 - fw*bbox.width - bbox.x0, oy0 - fh*bbox.height - bbox.y0)) 

1380 

1381 bbox = self.offsetbox.get_window_extent(renderer) 

1382 self.patch.set_bounds(bbox.bounds) 

1383 

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

1385 self.patch.set_mutation_scale(mutation_scale) 

1386 

1387 if self.arrowprops: 

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

1389 

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

1391 # TODO: Rotation needs to be accounted. 

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

1393 arrow_end = self._get_position_xy(renderer) 

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

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

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

1397 self.arrow_patch.set_positions(arrow_begin, arrow_end) 

1398 

1399 if "mutation_scale" in self.arrowprops: 

1400 mutation_scale = renderer.points_to_pixels( 

1401 self.arrowprops["mutation_scale"]) 

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

1403 self.arrow_patch.set_mutation_scale(mutation_scale) 

1404 

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

1406 self.arrow_patch.set_patchA(patchA) 

1407 

1408 def draw(self, renderer): 

1409 # docstring inherited 

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

1411 return 

1412 renderer.open_group(self.__class__.__name__, gid=self.get_gid()) 

1413 self.update_positions(renderer) 

1414 if self.arrow_patch is not None: 

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

1416 self.arrow_patch.figure = self.figure 

1417 self.arrow_patch.draw(renderer) 

1418 self.patch.draw(renderer) 

1419 self.offsetbox.draw(renderer) 

1420 renderer.close_group(self.__class__.__name__) 

1421 self.stale = False 

1422 

1423 

1424class DraggableBase: 

1425 """ 

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

1427 

1428 Derived classes must override the following methods:: 

1429 

1430 def save_offset(self): 

1431 ''' 

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

1433 reference position of the artist. 

1434 ''' 

1435 

1436 def update_offset(self, dx, dy): 

1437 ''' 

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

1439 the point where the mouse drag started. 

1440 ''' 

1441 

1442 Optionally, you may override the following method:: 

1443 

1444 def finalize_offset(self): 

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

1446 

1447 In the current implementation of `.DraggableLegend` and 

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

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

1450 coordinate and set a relevant attribute. 

1451 """ 

1452 

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

1454 self.ref_artist = ref_artist 

1455 if not ref_artist.pickable(): 

1456 ref_artist.set_picker(True) 

1457 self.got_artist = False 

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

1459 callbacks = self.canvas.callbacks 

1460 self._disconnectors = [ 

1461 functools.partial( 

1462 callbacks.disconnect, callbacks._connect_picklable(name, func)) 

1463 for name, func in [ 

1464 ("pick_event", self.on_pick), 

1465 ("button_release_event", self.on_release), 

1466 ("motion_notify_event", self.on_motion), 

1467 ] 

1468 ] 

1469 

1470 # A property, not an attribute, to maintain picklability. 

1471 canvas = property(lambda self: self.ref_artist.figure.canvas) 

1472 cids = property(lambda self: [ 

1473 disconnect.args[0] for disconnect in self._disconnectors[:2]]) 

1474 

1475 def on_motion(self, evt): 

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

1477 dx = evt.x - self.mouse_x 

1478 dy = evt.y - self.mouse_y 

1479 self.update_offset(dx, dy) 

1480 if self._use_blit: 

1481 self.canvas.restore_region(self.background) 

1482 self.ref_artist.draw( 

1483 self.ref_artist.figure._get_renderer()) 

1484 self.canvas.blit() 

1485 else: 

1486 self.canvas.draw() 

1487 

1488 def on_pick(self, evt): 

1489 if self._check_still_parented(): 

1490 if evt.artist == self.ref_artist: 

1491 self.mouse_x = evt.mouseevent.x 

1492 self.mouse_y = evt.mouseevent.y 

1493 self.save_offset() 

1494 self.got_artist = True 

1495 if self.got_artist and self._use_blit: 

1496 self.ref_artist.set_animated(True) 

1497 self.canvas.draw() 

1498 self.background = \ 

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

1500 self.ref_artist.draw( 

1501 self.ref_artist.figure._get_renderer()) 

1502 self.canvas.blit() 

1503 

1504 def on_release(self, event): 

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

1506 self.finalize_offset() 

1507 self.got_artist = False 

1508 if self._use_blit: 

1509 self.canvas.restore_region(self.background) 

1510 self.ref_artist.draw(self.ref_artist.figure._get_renderer()) 

1511 self.canvas.blit() 

1512 self.ref_artist.set_animated(False) 

1513 

1514 def _check_still_parented(self): 

1515 if self.ref_artist.figure is None: 

1516 self.disconnect() 

1517 return False 

1518 else: 

1519 return True 

1520 

1521 def disconnect(self): 

1522 """Disconnect the callbacks.""" 

1523 for disconnector in self._disconnectors: 

1524 disconnector() 

1525 

1526 def save_offset(self): 

1527 pass 

1528 

1529 def update_offset(self, dx, dy): 

1530 pass 

1531 

1532 def finalize_offset(self): 

1533 pass 

1534 

1535 

1536class DraggableOffsetBox(DraggableBase): 

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

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

1539 self.offsetbox = offsetbox 

1540 

1541 def save_offset(self): 

1542 offsetbox = self.offsetbox 

1543 renderer = offsetbox.figure._get_renderer() 

1544 offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer) 

1545 self.offsetbox_x, self.offsetbox_y = offset 

1546 self.offsetbox.set_offset(offset) 

1547 

1548 def update_offset(self, dx, dy): 

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

1550 self.offsetbox.set_offset(loc_in_canvas) 

1551 

1552 def get_loc_in_canvas(self): 

1553 offsetbox = self.offsetbox 

1554 renderer = offsetbox.figure._get_renderer() 

1555 bbox = offsetbox.get_bbox(renderer) 

1556 ox, oy = offsetbox._offset 

1557 loc_in_canvas = (ox + bbox.x0, oy + bbox.y0) 

1558 return loc_in_canvas 

1559 

1560 

1561class DraggableAnnotation(DraggableBase): 

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

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

1564 self.annotation = annotation 

1565 

1566 def save_offset(self): 

1567 ann = self.annotation 

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

1569 

1570 def update_offset(self, dx, dy): 

1571 ann = self.annotation 

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

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