Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2The `.OffsetBox` is a simple container artist. Its child artists are 

3meant to be drawn at a relative position to OffsetBox. The [VH]Packer, 

4DrawingArea and TextArea are derived from the OffsetBox. 

5 

6The [VH]Packer automatically adjust the relative positions of their 

7children, which should be instances of the OffsetBox. This is used to 

8align similar artists together, e.g., in legend. 

9 

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

11DrawingArea has a fixed width and height. The position of children 

12relative to the parent is fixed. The TextArea is contains a single 

13Text instance. The width and height of the TextArea instance is the 

14width and height of the its child text. 

15""" 

16 

17import numpy as np 

18 

19from matplotlib import cbook, docstring, rcParams 

20import matplotlib.artist as martist 

21import matplotlib.path as mpath 

22import matplotlib.text as mtext 

23import matplotlib.transforms as mtransforms 

24from matplotlib.font_manager import FontProperties 

25from matplotlib.image import BboxImage 

26from matplotlib.patches import ( 

27 FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist) 

28from matplotlib.text import _AnnotationBase 

29from matplotlib.transforms import Bbox, BboxBase, TransformedBbox 

30 

31 

32DEBUG = False 

33 

34 

35# for debugging use 

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

37 if DEBUG: 

38 mbbox_artist(*args, **kwargs) 

39 

40# _get_packed_offsets() and _get_aligned_offsets() are coded assuming 

41# that we are packing boxes horizontally. But same function will be 

42# used with vertical packing. 

43 

44 

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

46 """ 

47 Given a list of (width, xdescent) of each boxes, calculate the 

48 total width and the x-offset positions of each items according to 

49 *mode*. xdescent is analogous to the usual descent, but along the 

50 x-direction. xdescent values are currently ignored. 

51 

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

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

54 

55 There are three packing modes: 

56 

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

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

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

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

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

62 

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

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

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

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

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

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

69 

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

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

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

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

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

75 

76 Parameters 

77 ---------- 

78 wd_list : list of (float, float) 

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

80 total : float or None 

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

82 sep : float 

83 Spacing between boxes. 

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

85 The packing mode. 

86 

87 Returns 

88 ------- 

89 total : float 

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

91 offsets : array of float 

92 The left offsets of the boxes. 

93 """ 

94 w_list, d_list = zip(*wd_list) 

95 # d_list is currently not used. 

96 

97 if mode == "fixed": 

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

99 offsets = offsets_[:-1] 

100 if total is None: 

101 total = offsets_[-1] - sep 

102 return total, offsets 

103 

104 elif mode == "expand": 

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

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

107 if total is None: 

108 total = 1 

109 if len(w_list) > 1: 

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

111 else: 

112 sep = 0 

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

114 offsets = offsets_[:-1] 

115 return total, offsets 

116 

117 elif mode == "equal": 

118 maxh = max(w_list) 

119 if total is None: 

120 if sep is None: 

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

122 "using layout mode 'equal'.") 

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

124 else: 

125 sep = total / len(w_list) - maxh 

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

127 return total, offsets 

128 

129 else: 

130 raise ValueError("Unknown mode : %s" % (mode,)) 

131 

132 

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

134 """ 

135 Given a list of (height, descent) of each boxes, align the boxes 

136 with *align* and calculate the y-offsets of each boxes. 

137 total width and the offset positions of each items according to 

138 *mode*. xdescent is analogous to the usual descent, but along the 

139 x-direction. xdescent values are currently ignored. 

140 

141 *hd_list* : list of (width, xdescent) of boxes to be aligned. 

142 *sep* : spacing between boxes 

143 *height* : Intended total length. None if not used. 

144 *align* : align mode. 'baseline', 'top', 'bottom', or 'center'. 

145 """ 

146 

147 if height is None: 

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

149 

150 if align == "baseline": 

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

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

153 height = height_descent + descent 

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

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

156 descent = 0. 

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

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

159 descent = 0. 

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

161 elif align == "center": 

162 descent = 0. 

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

164 else: 

165 raise ValueError("Unknown Align mode : %s" % (align,)) 

166 

167 return height, descent, offsets 

168 

169 

170class OffsetBox(martist.Artist): 

171 """ 

172 The OffsetBox is a simple container artist. The child artist are meant 

173 to be drawn at a relative position to its parent. 

174 """ 

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

176 

177 super().__init__(*args, **kwargs) 

178 

179 # Clipping has not been implemented in the OffesetBox family, so 

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

181 # on to zero effect. 

182 self.set_clip_on(False) 

183 

184 self._children = [] 

185 self._offset = (0, 0) 

186 

187 def set_figure(self, fig): 

188 """ 

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

190 

191 Parameters 

192 ---------- 

193 fig : `~matplotlib.figure.Figure` 

194 """ 

195 martist.Artist.set_figure(self, fig) 

196 for c in self.get_children(): 

197 c.set_figure(fig) 

198 

199 @martist.Artist.axes.setter 

200 def axes(self, ax): 

201 # TODO deal with this better 

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

203 for c in self.get_children(): 

204 if c is not None: 

205 c.axes = ax 

206 

207 def contains(self, mouseevent): 

208 """ 

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

210 

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

212 mouseevents. 

213 

214 Parameters 

215 ---------- 

216 mouseevent : `matplotlib.backend_bases.MouseEvent` 

217 

218 Returns 

219 ------- 

220 contains : bool 

221 Whether any values are within the radius. 

222 details : dict 

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

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

225 individual Artist subclasses for details. 

226 

227 See Also 

228 -------- 

229 .Artist.contains 

230 """ 

231 inside, info = self._default_contains(mouseevent) 

232 if inside is not None: 

233 return inside, info 

234 for c in self.get_children(): 

235 a, b = c.contains(mouseevent) 

236 if a: 

237 return a, b 

238 return False, {} 

239 

240 def set_offset(self, xy): 

241 """ 

242 Set the offset. 

243 

244 Parameters 

245 ---------- 

246 xy : (float, float) or callable 

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

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

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

250 must have the signature:: 

251 

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

253-> (float, float) 

254 """ 

255 self._offset = xy 

256 self.stale = True 

257 

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

259 """ 

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

261 

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

263 offset is dynamically determined by a callable (see 

264 `~.OffsetBox.set_offset`). 

265 

266 Parameters 

267 ---------- 

268 width, height, xdescent, ydescent 

269 Extent parameters. 

270 renderer : `.RendererBase` subclass 

271 

272 """ 

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

274 if callable(self._offset) 

275 else self._offset) 

276 

277 def set_width(self, width): 

278 """ 

279 Set the width of the box. 

280 

281 Parameters 

282 ---------- 

283 width : float 

284 """ 

285 self.width = width 

286 self.stale = True 

287 

288 def set_height(self, height): 

289 """ 

290 Set the height of the box. 

291 

292 Parameters 

293 ---------- 

294 height : float 

295 """ 

296 self.height = height 

297 self.stale = True 

298 

299 def get_visible_children(self): 

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

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

302 

303 def get_children(self): 

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

305 return self._children 

306 

307 def get_extent_offsets(self, renderer): 

308 """ 

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

310 

311 Parameters 

312 ---------- 

313 renderer : `.RendererBase` subclass 

314 

315 Returns 

316 ------- 

317 width 

318 height 

319 xdescent 

320 ydescent 

321 list of (xoffset, yoffset) pairs 

322 """ 

323 raise NotImplementedError( 

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

325 

326 def get_extent(self, renderer): 

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

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

329 return w, h, xd, yd 

330 

331 def get_window_extent(self, renderer): 

332 """Return the bounding box (`.Bbox`) in display space.""" 

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

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

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

336 

337 def draw(self, renderer): 

338 """ 

339 Update the location of children if necessary and draw them 

340 to the given *renderer*. 

341 """ 

342 width, height, xdescent, ydescent, offsets = self.get_extent_offsets( 

343 renderer) 

344 

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

346 

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

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

349 c.draw(renderer) 

350 

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

352 self.stale = False 

353 

354 

355class PackerBase(OffsetBox): 

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

357 align=None, mode=None, 

358 children=None): 

359 """ 

360 Parameters 

361 ---------- 

362 pad : float, optional 

363 The boundary padding in points. 

364 

365 sep : float, optional 

366 The spacing between items in points. 

367 

368 width, height : float, optional 

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

370 *None*. 

371 

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

373 Alignment of boxes. 

374 

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

376 The packing mode. 

377 

378 - 'fixed' packs the given `.Artists` tight with *sep* spacing. 

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

380 artists with equal spacing in between. 

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

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

383 

384 children : list of `.Artist` 

385 The artists to pack. 

386 

387 Notes 

388 ----- 

389 *pad* and *sep* need to given in points and will be scale with 

390 the renderer dpi, while *width* and *height* need to be in 

391 pixels. 

392 """ 

393 super().__init__() 

394 

395 self.height = height 

396 self.width = width 

397 self.sep = sep 

398 self.pad = pad 

399 self.mode = mode 

400 self.align = align 

401 

402 self._children = children 

403 

404 

405class VPacker(PackerBase): 

406 """ 

407 The VPacker has its children packed vertically. It automatically 

408 adjust the relative positions of children in the drawing time. 

409 """ 

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

411 align="baseline", mode="fixed", 

412 children=None): 

413 """ 

414 Parameters 

415 ---------- 

416 pad : float, optional 

417 The boundary padding in points. 

418 

419 sep : float, optional 

420 The spacing between items in points. 

421 

422 width, height : float, optional 

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

424 *None*. 

425 

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

427 Alignment of boxes. 

428 

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

430 The packing mode. 

431 

432 - 'fixed' packs the given `.Artists` tight with *sep* spacing. 

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

434 artists with equal spacing in between. 

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

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

437 

438 children : list of `.Artist` 

439 The artists to pack. 

440 

441 Notes 

442 ----- 

443 *pad* and *sep* need to given in points and will be scale with 

444 the renderer dpi, while *width* and *height* need to be in 

445 pixels. 

446 """ 

447 super().__init__(pad, sep, width, height, align, mode, children) 

448 

449 def get_extent_offsets(self, renderer): 

450 # docstring inherited 

451 dpicor = renderer.points_to_pixels(1.) 

452 pad = self.pad * dpicor 

453 sep = self.sep * dpicor 

454 

455 if self.width is not None: 

456 for c in self.get_visible_children(): 

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

458 c.set_width(self.width) 

459 

460 whd_list = [c.get_extent(renderer) 

461 for c in self.get_visible_children()] 

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

463 

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

465 width, xdescent, xoffsets = _get_aligned_offsets(wd_list, 

466 self.width, 

467 self.align) 

468 

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

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

471 sep, self.mode) 

472 

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

474 ydescent = height - yoffsets[0] 

475 yoffsets = height - yoffsets 

476 

477 yoffsets = yoffsets - ydescent 

478 

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

480 xdescent + pad, ydescent + pad, 

481 list(zip(xoffsets, yoffsets))) 

482 

483 

484class HPacker(PackerBase): 

485 """ 

486 The HPacker has its children packed horizontally. It automatically 

487 adjusts the relative positions of children at draw time. 

488 """ 

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

490 align="baseline", mode="fixed", 

491 children=None): 

492 """ 

493 Parameters 

494 ---------- 

495 pad : float, optional 

496 The boundary padding in points. 

497 

498 sep : float, optional 

499 The spacing between items in points. 

500 

501 width, height : float, optional 

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

503 *None*. 

504 

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

506 Alignment of boxes. 

507 

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

509 The packing mode. 

510 

511 - 'fixed' packs the given `.Artists` tight with *sep* spacing. 

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

513 artists with equal spacing in between. 

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

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

516 

517 children : list of `.Artist` 

518 The artists to pack. 

519 

520 Notes 

521 ----- 

522 *pad* and *sep* need to given in points and will be scale with 

523 the renderer dpi, while *width* and *height* need to be in 

524 pixels. 

525 """ 

526 super().__init__(pad, sep, width, height, align, mode, children) 

527 

528 def get_extent_offsets(self, renderer): 

529 # docstring inherited 

530 dpicor = renderer.points_to_pixels(1.) 

531 pad = self.pad * dpicor 

532 sep = self.sep * dpicor 

533 

534 whd_list = [c.get_extent(renderer) 

535 for c in self.get_visible_children()] 

536 

537 if not whd_list: 

538 return 2 * pad, 2 * pad, pad, pad, [] 

539 

540 if self.height is None: 

541 height_descent = max(h - yd for w, h, xd, yd in whd_list) 

542 ydescent = max(yd for w, h, xd, yd in whd_list) 

543 height = height_descent + ydescent 

544 else: 

545 height = self.height - 2 * pad # width w/o pad 

546 

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

548 height, ydescent, yoffsets = _get_aligned_offsets(hd_list, 

549 self.height, 

550 self.align) 

551 

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

553 

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

555 sep, self.mode) 

556 

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

558 

559 xdescent = whd_list[0][2] 

560 xoffsets = xoffsets - xdescent 

561 

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

563 xdescent + pad, ydescent + pad, 

564 list(zip(xoffsets, yoffsets))) 

565 

566 

567class PaddedBox(OffsetBox): 

568 """ 

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

570 

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

572 it when rendering. 

573 """ 

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

575 """ 

576 Parameters 

577 ---------- 

578 child : `~matplotlib.artist.Artist` 

579 The contained `.Artist`. 

580 pad : float 

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

582 In contrast *width* and *hight* are in *pixel* and thus not scaled. 

583 draw_frame : bool 

584 Whether to draw the contained `.FancyBboxPatch`. 

585 patch_attrs : dict or None 

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

587 """ 

588 super().__init__() 

589 

590 self.pad = pad 

591 self._children = [child] 

592 

593 self.patch = FancyBboxPatch( 

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

595 facecolor='w', edgecolor='k', 

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

597 snap=True 

598 ) 

599 

600 self.patch.set_boxstyle("square", pad=0) 

601 

602 if patch_attrs is not None: 

603 self.patch.update(patch_attrs) 

604 

605 self._drawFrame = draw_frame 

606 

607 def get_extent_offsets(self, renderer): 

608 # docstring inherited. 

609 dpicor = renderer.points_to_pixels(1.) 

610 pad = self.pad * dpicor 

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

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

613 [(0, 0)]) 

614 

615 def draw(self, renderer): 

616 """ 

617 Update the location of children if necessary and draw them 

618 to the given *renderer*. 

619 """ 

620 width, height, xdescent, ydescent, offsets = self.get_extent_offsets( 

621 renderer) 

622 

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

624 

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

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

627 

628 self.draw_frame(renderer) 

629 

630 for c in self.get_visible_children(): 

631 c.draw(renderer) 

632 

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

634 self.stale = False 

635 

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

637 self.patch.set_bounds(bbox.x0, bbox.y0, 

638 bbox.width, bbox.height) 

639 

640 if fontsize: 

641 self.patch.set_mutation_scale(fontsize) 

642 self.stale = True 

643 

644 def draw_frame(self, renderer): 

645 # update the location and size of the legend 

646 bbox = self.get_window_extent(renderer) 

647 self.update_frame(bbox) 

648 

649 if self._drawFrame: 

650 self.patch.draw(renderer) 

651 

652 

653class DrawingArea(OffsetBox): 

654 """ 

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

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

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

658 boundaries of the parent. 

659 """ 

660 

661 def __init__(self, width, height, xdescent=0., 

662 ydescent=0., clip=False): 

663 """ 

664 *width*, *height* : width and height of the container box. 

665 *xdescent*, *ydescent* : descent of the box in x- and y-direction. 

666 *clip* : Whether to clip the children 

667 """ 

668 super().__init__() 

669 self.width = width 

670 self.height = height 

671 self.xdescent = xdescent 

672 self.ydescent = ydescent 

673 self._clip_children = clip 

674 self.offset_transform = mtransforms.Affine2D() 

675 self.dpi_transform = mtransforms.Affine2D() 

676 

677 @property 

678 def clip_children(self): 

679 """ 

680 If the children of this DrawingArea should be clipped 

681 by DrawingArea bounding box. 

682 """ 

683 return self._clip_children 

684 

685 @clip_children.setter 

686 def clip_children(self, val): 

687 self._clip_children = bool(val) 

688 self.stale = True 

689 

690 def get_transform(self): 

691 """ 

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

693 """ 

694 return self.dpi_transform + self.offset_transform 

695 

696 def set_transform(self, t): 

697 """ 

698 set_transform is ignored. 

699 """ 

700 

701 def set_offset(self, xy): 

702 """ 

703 Set the offset of the container. 

704 

705 Parameters 

706 ---------- 

707 xy : (float, float) 

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

709 """ 

710 self._offset = xy 

711 self.offset_transform.clear() 

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

713 self.stale = True 

714 

715 def get_offset(self): 

716 """ 

717 return offset of the container. 

718 """ 

719 return self._offset 

720 

721 def get_window_extent(self, renderer): 

722 ''' 

723 get the bounding box in display space. 

724 ''' 

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

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

727 

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

729 

730 def get_extent(self, renderer): 

731 """ 

732 Return with, height, xdescent, ydescent of box 

733 """ 

734 

735 dpi_cor = renderer.points_to_pixels(1.) 

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

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

738 

739 def add_artist(self, a): 

740 'Add any :class:`~matplotlib.artist.Artist` to the container box' 

741 self._children.append(a) 

742 if not a.is_transform_set(): 

743 a.set_transform(self.get_transform()) 

744 if self.axes is not None: 

745 a.axes = self.axes 

746 fig = self.figure 

747 if fig is not None: 

748 a.set_figure(fig) 

749 

750 def draw(self, renderer): 

751 """ 

752 Draw the children 

753 """ 

754 

755 dpi_cor = renderer.points_to_pixels(1.) 

756 self.dpi_transform.clear() 

757 self.dpi_transform.scale(dpi_cor) 

758 

759 # At this point the DrawingArea has a transform 

760 # to the display space so the path created is 

761 # good for clipping children 

762 tpath = mtransforms.TransformedPath( 

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

764 [self.width, self.height], 

765 [self.width, 0]]), 

766 self.get_transform()) 

767 for c in self._children: 

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

769 c.set_clip_path(tpath) 

770 c.draw(renderer) 

771 

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

773 self.stale = False 

774 

775 

776class TextArea(OffsetBox): 

777 """ 

778 The TextArea is contains a single Text instance. The text is 

779 placed at (0, 0) with baseline+left alignment. The width and height 

780 of the TextArea instance is the width and height of the its child 

781 text. 

782 """ 

783 def __init__(self, s, 

784 textprops=None, 

785 multilinebaseline=None, 

786 minimumdescent=True, 

787 ): 

788 """ 

789 Parameters 

790 ---------- 

791 s : str 

792 a string to be displayed. 

793 

794 textprops : dictionary, optional, default: None 

795 Dictionary of keyword parameters to be passed to the 

796 `~matplotlib.text.Text` instance contained inside TextArea. 

797 

798 multilinebaseline : bool, optional 

799 If `True`, baseline for multiline text is adjusted so that it is 

800 (approximately) center-aligned with singleline text. 

801 

802 minimumdescent : bool, optional 

803 If `True`, the box has a minimum descent of "p". 

804 """ 

805 if textprops is None: 

806 textprops = {} 

807 textprops.setdefault("va", "baseline") 

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

809 OffsetBox.__init__(self) 

810 self._children = [self._text] 

811 self.offset_transform = mtransforms.Affine2D() 

812 self._baseline_transform = mtransforms.Affine2D() 

813 self._text.set_transform(self.offset_transform + 

814 self._baseline_transform) 

815 self._multilinebaseline = multilinebaseline 

816 self._minimumdescent = minimumdescent 

817 

818 def set_text(self, s): 

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

820 self._text.set_text(s) 

821 self.stale = True 

822 

823 def get_text(self): 

824 "Returns the string representation of this area's text" 

825 return self._text.get_text() 

826 

827 def set_multilinebaseline(self, t): 

828 """ 

829 Set multilinebaseline . 

830 

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

832 (approximately) center-aligned with single-line text. 

833 """ 

834 self._multilinebaseline = t 

835 self.stale = True 

836 

837 def get_multilinebaseline(self): 

838 """ 

839 get multilinebaseline . 

840 """ 

841 return self._multilinebaseline 

842 

843 def set_minimumdescent(self, t): 

844 """ 

845 Set minimumdescent . 

846 

847 If True, extent of the single line text is adjusted so that 

848 it has minimum descent of "p" 

849 """ 

850 self._minimumdescent = t 

851 self.stale = True 

852 

853 def get_minimumdescent(self): 

854 """ 

855 get minimumdescent. 

856 """ 

857 return self._minimumdescent 

858 

859 def set_transform(self, t): 

860 """ 

861 set_transform is ignored. 

862 """ 

863 

864 def set_offset(self, xy): 

865 """ 

866 Set the offset of the container. 

867 

868 Parameters 

869 ---------- 

870 xy : (float, float) 

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

872 """ 

873 self._offset = xy 

874 self.offset_transform.clear() 

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

876 self.stale = True 

877 

878 def get_offset(self): 

879 """ 

880 return offset of the container. 

881 """ 

882 return self._offset 

883 

884 def get_window_extent(self, renderer): 

885 ''' 

886 get the bounding box in display space. 

887 ''' 

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

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

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

891 

892 def get_extent(self, renderer): 

893 _, h_, d_ = renderer.get_text_width_height_descent( 

894 "lp", self._text._fontproperties, ismath=False) 

895 

896 bbox, info, d = self._text._get_layout(renderer) 

897 w, h = bbox.width, bbox.height 

898 

899 self._baseline_transform.clear() 

900 

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

902 d_new = 0.5 * h - 0.5 * (h_ - d_) 

903 self._baseline_transform.translate(0, d - d_new) 

904 d = d_new 

905 

906 else: # single line 

907 

908 h_d = max(h_ - d_, h - d) 

909 

910 if self.get_minimumdescent(): 

911 ## to have a minimum descent, #i.e., "l" and "p" have same 

912 ## descents. 

913 d = max(d, d_) 

914 #else: 

915 # d = d 

916 

917 h = h_d + d 

918 

919 return w, h, 0., d 

920 

921 def draw(self, renderer): 

922 """ 

923 Draw the children 

924 """ 

925 

926 self._text.draw(renderer) 

927 

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

929 self.stale = False 

930 

931 

932class AuxTransformBox(OffsetBox): 

933 """ 

934 Offset Box with the aux_transform. Its children will be 

935 transformed with the aux_transform first then will be 

936 offseted. The absolute coordinate of the aux_transform is meaning 

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

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

939 offset transform. 

940 

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

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

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

944 calculated in the transformed coordinate. 

945 """ 

946 def __init__(self, aux_transform): 

947 self.aux_transform = aux_transform 

948 OffsetBox.__init__(self) 

949 self.offset_transform = mtransforms.Affine2D() 

950 # ref_offset_transform makes offset_transform always relative to the 

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

952 self.ref_offset_transform = mtransforms.Affine2D() 

953 

954 def add_artist(self, a): 

955 'Add any :class:`~matplotlib.artist.Artist` to the container box' 

956 self._children.append(a) 

957 a.set_transform(self.get_transform()) 

958 self.stale = True 

959 

960 def get_transform(self): 

961 """ 

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

963 to the children 

964 """ 

965 return (self.aux_transform 

966 + self.ref_offset_transform 

967 + self.offset_transform) 

968 

969 def set_transform(self, t): 

970 """ 

971 set_transform is ignored. 

972 """ 

973 

974 def set_offset(self, xy): 

975 """ 

976 Set the offset of the container. 

977 

978 Parameters 

979 ---------- 

980 xy : (float, float) 

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

982 """ 

983 self._offset = xy 

984 self.offset_transform.clear() 

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

986 self.stale = True 

987 

988 def get_offset(self): 

989 """ 

990 return offset of the container. 

991 """ 

992 return self._offset 

993 

994 def get_window_extent(self, renderer): 

995 ''' 

996 get the bounding box in display space. 

997 ''' 

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

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

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

1001 

1002 def get_extent(self, renderer): 

1003 # clear the offset transforms 

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

1005 self.ref_offset_transform.clear() 

1006 self.offset_transform.clear() 

1007 # calculate the extent 

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

1009 ub = mtransforms.Bbox.union(bboxes) 

1010 # adjust ref_offset_transform 

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

1012 # restor offset transform 

1013 self.offset_transform.set_matrix(_off) 

1014 

1015 return ub.width, ub.height, 0., 0. 

1016 

1017 def draw(self, renderer): 

1018 """ 

1019 Draw the children 

1020 """ 

1021 

1022 for c in self._children: 

1023 c.draw(renderer) 

1024 

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

1026 self.stale = False 

1027 

1028 

1029class AnchoredOffsetbox(OffsetBox): 

1030 """ 

1031 An offset box placed according to the legend location 

1032 loc. AnchoredOffsetbox has a single child. When multiple children 

1033 is needed, use other OffsetBox class to enclose them. By default, 

1034 the offset box is anchored against its parent axes. You may 

1035 explicitly specify the bbox_to_anchor. 

1036 """ 

1037 zorder = 5 # zorder of the legend 

1038 

1039 # Location codes 

1040 codes = {'upper right': 1, 

1041 'upper left': 2, 

1042 'lower left': 3, 

1043 'lower right': 4, 

1044 'right': 5, 

1045 'center left': 6, 

1046 'center right': 7, 

1047 'lower center': 8, 

1048 'upper center': 9, 

1049 'center': 10, 

1050 } 

1051 

1052 def __init__(self, loc, 

1053 pad=0.4, borderpad=0.5, 

1054 child=None, prop=None, frameon=True, 

1055 bbox_to_anchor=None, 

1056 bbox_transform=None, 

1057 **kwargs): 

1058 """ 

1059 loc is a string or an integer specifying the legend location. 

1060 The valid location codes are:: 

1061 

1062 'upper right' : 1, 

1063 'upper left' : 2, 

1064 'lower left' : 3, 

1065 'lower right' : 4, 

1066 'right' : 5, (same as 'center right', for back-compatibility) 

1067 'center left' : 6, 

1068 'center right' : 7, 

1069 'lower center' : 8, 

1070 'upper center' : 9, 

1071 'center' : 10, 

1072 

1073 pad : pad around the child for drawing a frame. given in 

1074 fraction of fontsize. 

1075 

1076 borderpad : pad between offsetbox frame and the bbox_to_anchor, 

1077 

1078 child : OffsetBox instance that will be anchored. 

1079 

1080 prop : font property. This is only used as a reference for paddings. 

1081 

1082 frameon : draw a frame box if True. 

1083 

1084 bbox_to_anchor : bbox to anchor. Use self.axes.bbox if None. 

1085 

1086 bbox_transform : with which the bbox_to_anchor will be transformed. 

1087 

1088 """ 

1089 super().__init__(**kwargs) 

1090 

1091 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) 

1092 self.set_child(child) 

1093 

1094 if isinstance(loc, str): 

1095 loc = cbook._check_getitem(self.codes, loc=loc) 

1096 

1097 self.loc = loc 

1098 self.borderpad = borderpad 

1099 self.pad = pad 

1100 

1101 if prop is None: 

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

1103 elif isinstance(prop, dict): 

1104 self.prop = FontProperties(**prop) 

1105 if "size" not in prop: 

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

1107 else: 

1108 self.prop = prop 

1109 

1110 self.patch = FancyBboxPatch( 

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

1112 facecolor='w', edgecolor='k', 

1113 mutation_scale=self.prop.get_size_in_points(), 

1114 snap=True 

1115 ) 

1116 self.patch.set_boxstyle("square", pad=0) 

1117 self._drawFrame = frameon 

1118 

1119 def set_child(self, child): 

1120 "set the child to be anchored" 

1121 self._child = child 

1122 if child is not None: 

1123 child.axes = self.axes 

1124 self.stale = True 

1125 

1126 def get_child(self): 

1127 "return the child" 

1128 return self._child 

1129 

1130 def get_children(self): 

1131 "return the list of children" 

1132 return [self._child] 

1133 

1134 def get_extent(self, renderer): 

1135 """ 

1136 return the extent of the artist. The extent of the child 

1137 added with the pad is returned 

1138 """ 

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

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

1141 pad = self.pad * fontsize 

1142 

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

1144 

1145 def get_bbox_to_anchor(self): 

1146 """ 

1147 return the bbox that the legend will be anchored 

1148 """ 

1149 if self._bbox_to_anchor is None: 

1150 return self.axes.bbox 

1151 else: 

1152 transform = self._bbox_to_anchor_transform 

1153 if transform is None: 

1154 return self._bbox_to_anchor 

1155 else: 

1156 return TransformedBbox(self._bbox_to_anchor, 

1157 transform) 

1158 

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

1160 """ 

1161 set the bbox that the child will be anchored. 

1162 

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

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

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

1166 transformed to display coordinate by the given transform. 

1167 """ 

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

1169 self._bbox_to_anchor = bbox 

1170 else: 

1171 try: 

1172 l = len(bbox) 

1173 except TypeError: 

1174 raise ValueError("Invalid argument for bbox : %s" % str(bbox)) 

1175 

1176 if l == 2: 

1177 bbox = [bbox[0], bbox[1], 0, 0] 

1178 

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

1180 

1181 self._bbox_to_anchor_transform = transform 

1182 self.stale = True 

1183 

1184 def get_window_extent(self, renderer): 

1185 ''' 

1186 get the bounding box in display space. 

1187 ''' 

1188 self._update_offset_func(renderer) 

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

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

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

1192 

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

1194 """ 

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

1196 renderer (because of the padding). 

1197 """ 

1198 if fontsize is None: 

1199 fontsize = renderer.points_to_pixels( 

1200 self.prop.get_size_in_points()) 

1201 

1202 def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self): 

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

1204 borderpad = self.borderpad * fontsize 

1205 bbox_to_anchor = self.get_bbox_to_anchor() 

1206 

1207 x0, y0 = self._get_anchored_bbox(self.loc, 

1208 bbox, 

1209 bbox_to_anchor, 

1210 borderpad) 

1211 return x0 + xd, y0 + yd 

1212 

1213 self.set_offset(_offset) 

1214 

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

1216 self.patch.set_bounds(bbox.x0, bbox.y0, 

1217 bbox.width, bbox.height) 

1218 

1219 if fontsize: 

1220 self.patch.set_mutation_scale(fontsize) 

1221 

1222 def draw(self, renderer): 

1223 "draw the artist" 

1224 

1225 if not self.get_visible(): 

1226 return 

1227 

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

1229 self._update_offset_func(renderer, fontsize) 

1230 

1231 if self._drawFrame: 

1232 # update the location and size of the legend 

1233 bbox = self.get_window_extent(renderer) 

1234 self.update_frame(bbox, fontsize) 

1235 self.patch.draw(renderer) 

1236 

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

1238 

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

1240 

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

1242 self.get_child().draw(renderer) 

1243 self.stale = False 

1244 

1245 def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad): 

1246 """ 

1247 return the position of the bbox anchored at the parentbbox 

1248 with the loc code, with the borderpad. 

1249 """ 

1250 assert loc in range(1, 11) # called only internally 

1251 

1252 BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) 

1253 

1254 anchor_coefs = {UR: "NE", 

1255 UL: "NW", 

1256 LL: "SW", 

1257 LR: "SE", 

1258 R: "E", 

1259 CL: "W", 

1260 CR: "E", 

1261 LC: "S", 

1262 UC: "N", 

1263 C: "C"} 

1264 

1265 c = anchor_coefs[loc] 

1266 

1267 container = parentbbox.padded(-borderpad) 

1268 anchored_box = bbox.anchored(c, container=container) 

1269 return anchored_box.x0, anchored_box.y0 

1270 

1271 

1272class AnchoredText(AnchoredOffsetbox): 

1273 """ 

1274 AnchoredOffsetbox with Text. 

1275 """ 

1276 

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

1278 """ 

1279 Parameters 

1280 ---------- 

1281 s : str 

1282 Text. 

1283 

1284 loc : str 

1285 Location code. 

1286 

1287 pad : float, optional 

1288 Pad between the text and the frame as fraction of the font 

1289 size. 

1290 

1291 borderpad : float, optional 

1292 Pad between the frame and the axes (or *bbox_to_anchor*). 

1293 

1294 prop : dictionary, optional, default: None 

1295 Dictionary of keyword parameters to be passed to the 

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

1297 

1298 Notes 

1299 ----- 

1300 Other keyword parameters of `AnchoredOffsetbox` are also 

1301 allowed. 

1302 """ 

1303 

1304 if prop is None: 

1305 prop = {} 

1306 badkwargs = {'ha', 'horizontalalignment', 'va', 'verticalalignment'} 

1307 if badkwargs & set(prop): 

1308 cbook.warn_deprecated( 

1309 "3.1", message="Mixing horizontalalignment or " 

1310 "verticalalignment with AnchoredText is not supported, " 

1311 "deprecated since %(since)s, and will raise an exception " 

1312 "%(removal)s.") 

1313 

1314 self.txt = TextArea(s, textprops=prop, minimumdescent=False) 

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

1316 super().__init__( 

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

1318 **kwargs) 

1319 

1320 

1321class OffsetImage(OffsetBox): 

1322 def __init__(self, arr, 

1323 zoom=1, 

1324 cmap=None, 

1325 norm=None, 

1326 interpolation=None, 

1327 origin=None, 

1328 filternorm=1, 

1329 filterrad=4.0, 

1330 resample=False, 

1331 dpi_cor=True, 

1332 **kwargs 

1333 ): 

1334 

1335 OffsetBox.__init__(self) 

1336 self._dpi_cor = dpi_cor 

1337 

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

1339 cmap=cmap, 

1340 norm=norm, 

1341 interpolation=interpolation, 

1342 origin=origin, 

1343 filternorm=filternorm, 

1344 filterrad=filterrad, 

1345 resample=resample, 

1346 **kwargs 

1347 ) 

1348 

1349 self._children = [self.image] 

1350 

1351 self.set_zoom(zoom) 

1352 self.set_data(arr) 

1353 

1354 def set_data(self, arr): 

1355 self._data = np.asarray(arr) 

1356 self.image.set_data(self._data) 

1357 self.stale = True 

1358 

1359 def get_data(self): 

1360 return self._data 

1361 

1362 def set_zoom(self, zoom): 

1363 self._zoom = zoom 

1364 self.stale = True 

1365 

1366 def get_zoom(self): 

1367 return self._zoom 

1368 

1369# def set_axes(self, axes): 

1370# self.image.set_axes(axes) 

1371# martist.Artist.set_axes(self, axes) 

1372 

1373# def set_offset(self, xy): 

1374# """ 

1375# Set the offset of the container. 

1376# 

1377# Parameters 

1378# ---------- 

1379# xy : (float, float) 

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

1381# """ 

1382# self._offset = xy 

1383 

1384# self.offset_transform.clear() 

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

1386 

1387 def get_offset(self): 

1388 """ 

1389 return offset of the container. 

1390 """ 

1391 return self._offset 

1392 

1393 def get_children(self): 

1394 return [self.image] 

1395 

1396 def get_window_extent(self, renderer): 

1397 ''' 

1398 get the bounding box in display space. 

1399 ''' 

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

1401 ox, oy = self.get_offset() 

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

1403 

1404 def get_extent(self, renderer): 

1405 if self._dpi_cor: # True, do correction 

1406 dpi_cor = renderer.points_to_pixels(1.) 

1407 else: 

1408 dpi_cor = 1. 

1409 

1410 zoom = self.get_zoom() 

1411 data = self.get_data() 

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

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

1414 

1415 return w, h, 0, 0 

1416 

1417 def draw(self, renderer): 

1418 """ 

1419 Draw the children 

1420 """ 

1421 self.image.draw(renderer) 

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

1423 self.stale = False 

1424 

1425 

1426class AnnotationBbox(martist.Artist, _AnnotationBase): 

1427 """ 

1428 Annotation-like class, but with offsetbox instead of Text. 

1429 """ 

1430 zorder = 3 

1431 

1432 def __str__(self): 

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

1434 

1435 @docstring.dedent_interpd 

1436 def __init__(self, offsetbox, xy, 

1437 xybox=None, 

1438 xycoords='data', 

1439 boxcoords=None, 

1440 frameon=True, pad=0.4, # BboxPatch 

1441 annotation_clip=None, 

1442 box_alignment=(0.5, 0.5), 

1443 bboxprops=None, 

1444 arrowprops=None, 

1445 fontsize=None, 

1446 **kwargs): 

1447 """ 

1448 *offsetbox* : OffsetBox instance 

1449 

1450 *xycoords* : same as Annotation but can be a tuple of two 

1451 strings which are interpreted as x and y coordinates. 

1452 

1453 *boxcoords* : similar to textcoords as Annotation but can be a 

1454 tuple of two strings which are interpreted as x and y 

1455 coordinates. 

1456 

1457 *box_alignment* : a tuple of two floats for a vertical and 

1458 horizontal alignment of the offset box w.r.t. the *boxcoords*. 

1459 The lower-left corner is (0.0) and upper-right corner is (1.1). 

1460 

1461 other parameters are identical to that of Annotation. 

1462 """ 

1463 

1464 martist.Artist.__init__(self, **kwargs) 

1465 _AnnotationBase.__init__(self, 

1466 xy, 

1467 xycoords=xycoords, 

1468 annotation_clip=annotation_clip) 

1469 

1470 self.offsetbox = offsetbox 

1471 

1472 self.arrowprops = arrowprops 

1473 

1474 self.set_fontsize(fontsize) 

1475 

1476 if xybox is None: 

1477 self.xybox = xy 

1478 else: 

1479 self.xybox = xybox 

1480 

1481 if boxcoords is None: 

1482 self.boxcoords = xycoords 

1483 else: 

1484 self.boxcoords = boxcoords 

1485 

1486 if arrowprops is not None: 

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

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

1489 **self.arrowprops) 

1490 else: 

1491 self._arrow_relpos = None 

1492 self.arrow_patch = None 

1493 

1494 self._box_alignment = box_alignment 

1495 

1496 # frame 

1497 self.patch = FancyBboxPatch( 

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

1499 facecolor='w', edgecolor='k', 

1500 mutation_scale=self.prop.get_size_in_points(), 

1501 snap=True 

1502 ) 

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

1504 if bboxprops: 

1505 self.patch.set(**bboxprops) 

1506 self._drawFrame = frameon 

1507 

1508 @property 

1509 def xyann(self): 

1510 return self.xybox 

1511 

1512 @xyann.setter 

1513 def xyann(self, xyann): 

1514 self.xybox = xyann 

1515 self.stale = True 

1516 

1517 @property 

1518 def anncoords(self): 

1519 return self.boxcoords 

1520 

1521 @anncoords.setter 

1522 def anncoords(self, coords): 

1523 self.boxcoords = coords 

1524 self.stale = True 

1525 

1526 def contains(self, mouseevent): 

1527 inside, info = self._default_contains(mouseevent) 

1528 if inside is not None: 

1529 return inside, info 

1530 t, tinfo = self.offsetbox.contains(mouseevent) 

1531 #if self.arrow_patch is not None: 

1532 # a, ainfo=self.arrow_patch.contains(event) 

1533 # t = t or a 

1534 

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

1536 

1537 return t, tinfo 

1538 

1539 def get_children(self): 

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

1541 if self.arrow_patch: 

1542 children.append(self.arrow_patch) 

1543 return children 

1544 

1545 def set_figure(self, fig): 

1546 

1547 if self.arrow_patch is not None: 

1548 self.arrow_patch.set_figure(fig) 

1549 self.offsetbox.set_figure(fig) 

1550 martist.Artist.set_figure(self, fig) 

1551 

1552 def set_fontsize(self, s=None): 

1553 """ 

1554 set fontsize in points 

1555 """ 

1556 if s is None: 

1557 s = rcParams["legend.fontsize"] 

1558 

1559 self.prop = FontProperties(size=s) 

1560 self.stale = True 

1561 

1562 def get_fontsize(self, s=None): 

1563 """ 

1564 return fontsize in points 

1565 """ 

1566 return self.prop.get_size_in_points() 

1567 

1568 def update_positions(self, renderer): 

1569 """ 

1570 Update the pixel positions of the annotated point and the text. 

1571 """ 

1572 xy_pixel = self._get_position_xy(renderer) 

1573 self._update_position_xybox(renderer, xy_pixel) 

1574 

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

1576 self.patch.set_mutation_scale(mutation_scale) 

1577 

1578 if self.arrow_patch: 

1579 self.arrow_patch.set_mutation_scale(mutation_scale) 

1580 

1581 def _update_position_xybox(self, renderer, xy_pixel): 

1582 """ 

1583 Update the pixel positions of the annotation text and the arrow 

1584 patch. 

1585 """ 

1586 

1587 x, y = self.xybox 

1588 if isinstance(self.boxcoords, tuple): 

1589 xcoord, ycoord = self.boxcoords 

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

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

1592 ox0, oy0 = x1, y2 

1593 else: 

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

1595 

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

1597 

1598 _fw, _fh = self._box_alignment 

1599 self.offsetbox.set_offset((ox0 - _fw * w + xd, oy0 - _fh * h + yd)) 

1600 

1601 # update patch position 

1602 bbox = self.offsetbox.get_window_extent(renderer) 

1603 #self.offsetbox.set_offset((ox0-_fw*w, oy0-_fh*h)) 

1604 self.patch.set_bounds(bbox.x0, bbox.y0, 

1605 bbox.width, bbox.height) 

1606 

1607 x, y = xy_pixel 

1608 

1609 ox1, oy1 = x, y 

1610 

1611 if self.arrowprops: 

1612 d = self.arrowprops.copy() 

1613 

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

1615 

1616 # adjust the starting point of the arrow relative to 

1617 # the textbox. 

1618 # TODO : Rotation needs to be accounted. 

1619 relpos = self._arrow_relpos 

1620 

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

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

1623 

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

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

1626 # Then it will be shrunk by shrinkA and shrinkB 

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

1628 # is used. 

1629 

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

1631 fs = self.prop.get_size_in_points() 

1632 mutation_scale = d.pop("mutation_scale", fs) 

1633 mutation_scale = renderer.points_to_pixels(mutation_scale) 

1634 self.arrow_patch.set_mutation_scale(mutation_scale) 

1635 

1636 patchA = d.pop("patchA", self.patch) 

1637 self.arrow_patch.set_patchA(patchA) 

1638 

1639 def draw(self, renderer): 

1640 """ 

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

1642 """ 

1643 

1644 if renderer is not None: 

1645 self._renderer = renderer 

1646 if not self.get_visible(): 

1647 return 

1648 

1649 xy_pixel = self._get_position_xy(renderer) 

1650 

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

1652 return 

1653 

1654 self.update_positions(renderer) 

1655 

1656 if self.arrow_patch is not None: 

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

1658 self.arrow_patch.figure = self.figure 

1659 self.arrow_patch.draw(renderer) 

1660 

1661 if self._drawFrame: 

1662 self.patch.draw(renderer) 

1663 

1664 self.offsetbox.draw(renderer) 

1665 self.stale = False 

1666 

1667 

1668class DraggableBase: 

1669 """ 

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

1671 

1672 Derived classes must override the following methods:: 

1673 

1674 def save_offset(self): 

1675 ''' 

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

1677 reference position of the artist. 

1678 ''' 

1679 

1680 def update_offset(self, dx, dy): 

1681 ''' 

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

1683 the point where the mouse drag started. 

1684 ''' 

1685 

1686 Optionally, you may override the following methods:: 

1687 

1688 def artist_picker(self, artist, evt): 

1689 '''The picker method that will be used.''' 

1690 return self.ref_artist.contains(evt) 

1691 

1692 def finalize_offset(self): 

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

1694 

1695 In the current implementation of `DraggableLegend` and 

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

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

1698 coordinate and set a relevant attribute. 

1699 """ 

1700 

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

1702 self.ref_artist = ref_artist 

1703 self.got_artist = False 

1704 

1705 self.canvas = self.ref_artist.figure.canvas 

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

1707 

1708 c2 = self.canvas.mpl_connect('pick_event', self.on_pick) 

1709 c3 = self.canvas.mpl_connect('button_release_event', self.on_release) 

1710 

1711 ref_artist.set_picker(self.artist_picker) 

1712 self.cids = [c2, c3] 

1713 

1714 def on_motion(self, evt): 

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

1716 dx = evt.x - self.mouse_x 

1717 dy = evt.y - self.mouse_y 

1718 self.update_offset(dx, dy) 

1719 self.canvas.draw() 

1720 

1721 def on_motion_blit(self, evt): 

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

1723 dx = evt.x - self.mouse_x 

1724 dy = evt.y - self.mouse_y 

1725 self.update_offset(dx, dy) 

1726 self.canvas.restore_region(self.background) 

1727 self.ref_artist.draw(self.ref_artist.figure._cachedRenderer) 

1728 self.canvas.blit() 

1729 

1730 def on_pick(self, evt): 

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

1732 

1733 self.mouse_x = evt.mouseevent.x 

1734 self.mouse_y = evt.mouseevent.y 

1735 self.got_artist = True 

1736 

1737 if self._use_blit: 

1738 self.ref_artist.set_animated(True) 

1739 self.canvas.draw() 

1740 self.background = self.canvas.copy_from_bbox( 

1741 self.ref_artist.figure.bbox) 

1742 self.ref_artist.draw(self.ref_artist.figure._cachedRenderer) 

1743 self.canvas.blit() 

1744 self._c1 = self.canvas.mpl_connect('motion_notify_event', 

1745 self.on_motion_blit) 

1746 else: 

1747 self._c1 = self.canvas.mpl_connect('motion_notify_event', 

1748 self.on_motion) 

1749 self.save_offset() 

1750 

1751 def on_release(self, event): 

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

1753 self.finalize_offset() 

1754 self.got_artist = False 

1755 self.canvas.mpl_disconnect(self._c1) 

1756 

1757 if self._use_blit: 

1758 self.ref_artist.set_animated(False) 

1759 

1760 def _check_still_parented(self): 

1761 if self.ref_artist.figure is None: 

1762 self.disconnect() 

1763 return False 

1764 else: 

1765 return True 

1766 

1767 def disconnect(self): 

1768 """Disconnect the callbacks.""" 

1769 for cid in self.cids: 

1770 self.canvas.mpl_disconnect(cid) 

1771 try: 

1772 c1 = self._c1 

1773 except AttributeError: 

1774 pass 

1775 else: 

1776 self.canvas.mpl_disconnect(c1) 

1777 

1778 def artist_picker(self, artist, evt): 

1779 return self.ref_artist.contains(evt) 

1780 

1781 def save_offset(self): 

1782 pass 

1783 

1784 def update_offset(self, dx, dy): 

1785 pass 

1786 

1787 def finalize_offset(self): 

1788 pass 

1789 

1790 

1791class DraggableOffsetBox(DraggableBase): 

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

1793 DraggableBase.__init__(self, ref_artist, use_blit=use_blit) 

1794 self.offsetbox = offsetbox 

1795 

1796 def save_offset(self): 

1797 offsetbox = self.offsetbox 

1798 renderer = offsetbox.figure._cachedRenderer 

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

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

1801 self.offsetbox_x, self.offsetbox_y = offset 

1802 self.offsetbox.set_offset(offset) 

1803 

1804 def update_offset(self, dx, dy): 

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

1806 self.offsetbox.set_offset(loc_in_canvas) 

1807 

1808 def get_loc_in_canvas(self): 

1809 offsetbox = self.offsetbox 

1810 renderer = offsetbox.figure._cachedRenderer 

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

1812 ox, oy = offsetbox._offset 

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

1814 return loc_in_canvas 

1815 

1816 

1817class DraggableAnnotation(DraggableBase): 

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

1819 DraggableBase.__init__(self, annotation, use_blit=use_blit) 

1820 self.annotation = annotation 

1821 

1822 def save_offset(self): 

1823 ann = self.annotation 

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

1825 

1826 def update_offset(self, dx, dy): 

1827 ann = self.annotation 

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

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