You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1812 lines
54 KiB

4 years ago
  1. """
  2. The OffsetBox is a simple container artist. The child artist are meant
  3. to be drawn at a relative position to its parent. The [VH]Packer,
  4. DrawingArea and TextArea are derived from the OffsetBox.
  5. The [VH]Packer automatically adjust the relative postisions of their
  6. children, which should be instances of the OffsetBox. This is used to
  7. align similar artists together, e.g., in legend.
  8. The DrawingArea can contain any Artist as a child. The
  9. DrawingArea has a fixed width and height. The position of children
  10. relative to the parent is fixed. The TextArea is contains a single
  11. Text instance. The width and height of the TextArea instance is the
  12. width and height of the its child text.
  13. """
  14. import warnings
  15. import numpy as np
  16. import matplotlib.transforms as mtransforms
  17. import matplotlib.artist as martist
  18. import matplotlib.text as mtext
  19. import matplotlib.path as mpath
  20. from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
  21. from matplotlib.font_manager import FontProperties
  22. from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
  23. from matplotlib import rcParams
  24. from matplotlib import docstring
  25. from matplotlib.image import BboxImage
  26. from matplotlib.patches import bbox_artist as mbbox_artist
  27. from matplotlib.text import _AnnotationBase
  28. DEBUG = False
  29. # for debugging use
  30. def bbox_artist(*args, **kwargs):
  31. if DEBUG:
  32. mbbox_artist(*args, **kwargs)
  33. # _get_packed_offsets() and _get_aligned_offsets() are coded assuming
  34. # that we are packing boxes horizontally. But same function will be
  35. # used with vertical packing.
  36. def _get_packed_offsets(wd_list, total, sep, mode="fixed"):
  37. """
  38. Geiven a list of (width, xdescent) of each boxes, calculate the
  39. total width and the x-offset positions of each items according to
  40. *mode*. xdescent is analogous to the usual descent, but along the
  41. x-direction. xdescent values are currently ignored.
  42. *wd_list* : list of (width, xdescent) of boxes to be packed.
  43. *sep* : spacing between boxes
  44. *total* : Intended total length. None if not used.
  45. *mode* : packing mode. 'fixed', 'expand', or 'equal'.
  46. """
  47. w_list, d_list = zip(*wd_list)
  48. # d_list is currently not used.
  49. if mode == "fixed":
  50. offsets_ = np.cumsum([0] + [w + sep for w in w_list])
  51. offsets = offsets_[:-1]
  52. if total is None:
  53. total = offsets_[-1] - sep
  54. return total, offsets
  55. elif mode == "expand":
  56. # This is a bit of a hack to avoid a TypeError when *total*
  57. # is None and used in conjugation with tight layout.
  58. if total is None:
  59. total = 1
  60. if len(w_list) > 1:
  61. sep = (total - sum(w_list)) / (len(w_list) - 1)
  62. else:
  63. sep = 0
  64. offsets_ = np.cumsum([0] + [w + sep for w in w_list])
  65. offsets = offsets_[:-1]
  66. return total, offsets
  67. elif mode == "equal":
  68. maxh = max(w_list)
  69. if total is None:
  70. total = (maxh + sep) * len(w_list)
  71. else:
  72. sep = total / len(w_list) - maxh
  73. offsets = (maxh + sep) * np.arange(len(w_list))
  74. return total, offsets
  75. else:
  76. raise ValueError("Unknown mode : %s" % (mode,))
  77. def _get_aligned_offsets(hd_list, height, align="baseline"):
  78. """
  79. Given a list of (height, descent) of each boxes, align the boxes
  80. with *align* and calculate the y-offsets of each boxes.
  81. total width and the offset positions of each items according to
  82. *mode*. xdescent is analogous to the usual descent, but along the
  83. x-direction. xdescent values are currently ignored.
  84. *hd_list* : list of (width, xdescent) of boxes to be aligned.
  85. *sep* : spacing between boxes
  86. *height* : Intended total length. None if not used.
  87. *align* : align mode. 'baseline', 'top', 'bottom', or 'center'.
  88. """
  89. if height is None:
  90. height = max(h for h, d in hd_list)
  91. if align == "baseline":
  92. height_descent = max(h - d for h, d in hd_list)
  93. descent = max(d for h, d in hd_list)
  94. height = height_descent + descent
  95. offsets = [0. for h, d in hd_list]
  96. elif align in ["left", "top"]:
  97. descent = 0.
  98. offsets = [d for h, d in hd_list]
  99. elif align in ["right", "bottom"]:
  100. descent = 0.
  101. offsets = [height - h + d for h, d in hd_list]
  102. elif align == "center":
  103. descent = 0.
  104. offsets = [(height - h) * .5 + d for h, d in hd_list]
  105. else:
  106. raise ValueError("Unknown Align mode : %s" % (align,))
  107. return height, descent, offsets
  108. class OffsetBox(martist.Artist):
  109. """
  110. The OffsetBox is a simple container artist. The child artist are meant
  111. to be drawn at a relative position to its parent.
  112. """
  113. def __init__(self, *args, **kwargs):
  114. super().__init__(*args, **kwargs)
  115. # Clipping has not been implemented in the OffesetBox family, so
  116. # disable the clip flag for consistency. It can always be turned back
  117. # on to zero effect.
  118. self.set_clip_on(False)
  119. self._children = []
  120. self._offset = (0, 0)
  121. def set_figure(self, fig):
  122. """
  123. Set the figure
  124. accepts a class:`~matplotlib.figure.Figure` instance
  125. """
  126. martist.Artist.set_figure(self, fig)
  127. for c in self.get_children():
  128. c.set_figure(fig)
  129. @martist.Artist.axes.setter
  130. def axes(self, ax):
  131. # TODO deal with this better
  132. martist.Artist.axes.fset(self, ax)
  133. for c in self.get_children():
  134. if c is not None:
  135. c.axes = ax
  136. def contains(self, mouseevent):
  137. for c in self.get_children():
  138. a, b = c.contains(mouseevent)
  139. if a:
  140. return a, b
  141. return False, {}
  142. def set_offset(self, xy):
  143. """
  144. Set the offset.
  145. Parameters
  146. ----------
  147. xy : (float, float) or callable
  148. The (x,y) coordinates of the offset in display units.
  149. A callable must have the signature::
  150. def offset(width, height, xdescent, ydescent, renderer) \
  151. -> (float, float)
  152. """
  153. self._offset = xy
  154. self.stale = True
  155. def get_offset(self, width, height, xdescent, ydescent, renderer):
  156. """
  157. Get the offset
  158. accepts extent of the box
  159. """
  160. return (self._offset(width, height, xdescent, ydescent, renderer)
  161. if callable(self._offset)
  162. else self._offset)
  163. def set_width(self, width):
  164. """
  165. Set the width
  166. accepts float
  167. """
  168. self.width = width
  169. self.stale = True
  170. def set_height(self, height):
  171. """
  172. Set the height
  173. accepts float
  174. """
  175. self.height = height
  176. self.stale = True
  177. def get_visible_children(self):
  178. """
  179. Return a list of visible artists it contains.
  180. """
  181. return [c for c in self._children if c.get_visible()]
  182. def get_children(self):
  183. """
  184. Return a list of artists it contains.
  185. """
  186. return self._children
  187. def get_extent_offsets(self, renderer):
  188. raise Exception("")
  189. def get_extent(self, renderer):
  190. """
  191. Return with, height, xdescent, ydescent of box
  192. """
  193. w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
  194. return w, h, xd, yd
  195. def get_window_extent(self, renderer):
  196. '''
  197. get the bounding box in display space.
  198. '''
  199. w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
  200. px, py = self.get_offset(w, h, xd, yd, renderer)
  201. return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h)
  202. def draw(self, renderer):
  203. """
  204. Update the location of children if necessary and draw them
  205. to the given *renderer*.
  206. """
  207. width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
  208. renderer)
  209. px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
  210. for c, (ox, oy) in zip(self.get_visible_children(), offsets):
  211. c.set_offset((px + ox, py + oy))
  212. c.draw(renderer)
  213. bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  214. self.stale = False
  215. class PackerBase(OffsetBox):
  216. def __init__(self, pad=None, sep=None, width=None, height=None,
  217. align=None, mode=None,
  218. children=None):
  219. """
  220. Parameters
  221. ----------
  222. pad : float, optional
  223. Boundary pad.
  224. sep : float, optional
  225. Spacing between items.
  226. width : float, optional
  227. height : float, optional
  228. Width and height of the container box, calculated if
  229. `None`.
  230. align : str, optional
  231. Alignment of boxes. Can be one of ``top``, ``bottom``,
  232. ``left``, ``right``, ``center`` and ``baseline``
  233. mode : str, optional
  234. Packing mode.
  235. Notes
  236. -----
  237. *pad* and *sep* need to given in points and will be scale with
  238. the renderer dpi, while *width* and *height* need to be in
  239. pixels.
  240. """
  241. super().__init__()
  242. self.height = height
  243. self.width = width
  244. self.sep = sep
  245. self.pad = pad
  246. self.mode = mode
  247. self.align = align
  248. self._children = children
  249. class VPacker(PackerBase):
  250. """
  251. The VPacker has its children packed vertically. It automatically
  252. adjust the relative positions of children in the drawing time.
  253. """
  254. def __init__(self, pad=None, sep=None, width=None, height=None,
  255. align="baseline", mode="fixed",
  256. children=None):
  257. """
  258. Parameters
  259. ----------
  260. pad : float, optional
  261. Boundary pad.
  262. sep : float, optional
  263. Spacing between items.
  264. width : float, optional
  265. height : float, optional
  266. width and height of the container box, calculated if
  267. `None`.
  268. align : str, optional
  269. Alignment of boxes.
  270. mode : str, optional
  271. Packing mode.
  272. Notes
  273. -----
  274. *pad* and *sep* need to given in points and will be scale with
  275. the renderer dpi, while *width* and *height* need to be in
  276. pixels.
  277. """
  278. super().__init__(pad, sep, width, height, align, mode, children)
  279. def get_extent_offsets(self, renderer):
  280. """
  281. update offset of childrens and return the extents of the box
  282. """
  283. dpicor = renderer.points_to_pixels(1.)
  284. pad = self.pad * dpicor
  285. sep = self.sep * dpicor
  286. if self.width is not None:
  287. for c in self.get_visible_children():
  288. if isinstance(c, PackerBase) and c.mode == "expand":
  289. c.set_width(self.width)
  290. whd_list = [c.get_extent(renderer)
  291. for c in self.get_visible_children()]
  292. whd_list = [(w, h, xd, (h - yd)) for w, h, xd, yd in whd_list]
  293. wd_list = [(w, xd) for w, h, xd, yd in whd_list]
  294. width, xdescent, xoffsets = _get_aligned_offsets(wd_list,
  295. self.width,
  296. self.align)
  297. pack_list = [(h, yd) for w, h, xd, yd in whd_list]
  298. height, yoffsets_ = _get_packed_offsets(pack_list, self.height,
  299. sep, self.mode)
  300. yoffsets = yoffsets_ + [yd for w, h, xd, yd in whd_list]
  301. ydescent = height - yoffsets[0]
  302. yoffsets = height - yoffsets
  303. yoffsets = yoffsets - ydescent
  304. return (width + 2 * pad, height + 2 * pad,
  305. xdescent + pad, ydescent + pad,
  306. list(zip(xoffsets, yoffsets)))
  307. class HPacker(PackerBase):
  308. """
  309. The HPacker has its children packed horizontally. It automatically
  310. adjusts the relative positions of children at draw time.
  311. """
  312. def __init__(self, pad=None, sep=None, width=None, height=None,
  313. align="baseline", mode="fixed",
  314. children=None):
  315. """
  316. Parameters
  317. ----------
  318. pad : float, optional
  319. Boundary pad.
  320. sep : float, optional
  321. Spacing between items.
  322. width : float, optional
  323. height : float, optional
  324. Width and height of the container box, calculated if
  325. `None`.
  326. align : str
  327. Alignment of boxes.
  328. mode : str
  329. Packing mode.
  330. Notes
  331. -----
  332. *pad* and *sep* need to given in points and will be scale with
  333. the renderer dpi, while *width* and *height* need to be in
  334. pixels.
  335. """
  336. super().__init__(pad, sep, width, height, align, mode, children)
  337. def get_extent_offsets(self, renderer):
  338. """
  339. update offset of children and return the extents of the box
  340. """
  341. dpicor = renderer.points_to_pixels(1.)
  342. pad = self.pad * dpicor
  343. sep = self.sep * dpicor
  344. whd_list = [c.get_extent(renderer)
  345. for c in self.get_visible_children()]
  346. if not whd_list:
  347. return 2 * pad, 2 * pad, pad, pad, []
  348. if self.height is None:
  349. height_descent = max(h - yd for w, h, xd, yd in whd_list)
  350. ydescent = max(yd for w, h, xd, yd in whd_list)
  351. height = height_descent + ydescent
  352. else:
  353. height = self.height - 2 * pad # width w/o pad
  354. hd_list = [(h, yd) for w, h, xd, yd in whd_list]
  355. height, ydescent, yoffsets = _get_aligned_offsets(hd_list,
  356. self.height,
  357. self.align)
  358. pack_list = [(w, xd) for w, h, xd, yd in whd_list]
  359. width, xoffsets_ = _get_packed_offsets(pack_list, self.width,
  360. sep, self.mode)
  361. xoffsets = xoffsets_ + [xd for w, h, xd, yd in whd_list]
  362. xdescent = whd_list[0][2]
  363. xoffsets = xoffsets - xdescent
  364. return (width + 2 * pad, height + 2 * pad,
  365. xdescent + pad, ydescent + pad,
  366. list(zip(xoffsets, yoffsets)))
  367. class PaddedBox(OffsetBox):
  368. def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None):
  369. """
  370. *pad* : boundary pad
  371. .. note::
  372. *pad* need to given in points and will be
  373. scale with the renderer dpi, while *width* and *height*
  374. need to be in pixels.
  375. """
  376. super().__init__()
  377. self.pad = pad
  378. self._children = [child]
  379. self.patch = FancyBboxPatch(
  380. xy=(0.0, 0.0), width=1., height=1.,
  381. facecolor='w', edgecolor='k',
  382. mutation_scale=1, # self.prop.get_size_in_points(),
  383. snap=True
  384. )
  385. self.patch.set_boxstyle("square", pad=0)
  386. if patch_attrs is not None:
  387. self.patch.update(patch_attrs)
  388. self._drawFrame = draw_frame
  389. def get_extent_offsets(self, renderer):
  390. """
  391. update offset of childrens and return the extents of the box
  392. """
  393. dpicor = renderer.points_to_pixels(1.)
  394. pad = self.pad * dpicor
  395. w, h, xd, yd = self._children[0].get_extent(renderer)
  396. return w + 2 * pad, h + 2 * pad, \
  397. xd + pad, yd + pad, \
  398. [(0, 0)]
  399. def draw(self, renderer):
  400. """
  401. Update the location of children if necessary and draw them
  402. to the given *renderer*.
  403. """
  404. width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
  405. renderer)
  406. px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
  407. for c, (ox, oy) in zip(self.get_visible_children(), offsets):
  408. c.set_offset((px + ox, py + oy))
  409. self.draw_frame(renderer)
  410. for c in self.get_visible_children():
  411. c.draw(renderer)
  412. #bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  413. self.stale = False
  414. def update_frame(self, bbox, fontsize=None):
  415. self.patch.set_bounds(bbox.x0, bbox.y0,
  416. bbox.width, bbox.height)
  417. if fontsize:
  418. self.patch.set_mutation_scale(fontsize)
  419. self.stale = True
  420. def draw_frame(self, renderer):
  421. # update the location and size of the legend
  422. bbox = self.get_window_extent(renderer)
  423. self.update_frame(bbox)
  424. if self._drawFrame:
  425. self.patch.draw(renderer)
  426. class DrawingArea(OffsetBox):
  427. """
  428. The DrawingArea can contain any Artist as a child. The DrawingArea
  429. has a fixed width and height. The position of children relative to
  430. the parent is fixed. The children can be clipped at the
  431. boundaries of the parent.
  432. """
  433. def __init__(self, width, height, xdescent=0.,
  434. ydescent=0., clip=False):
  435. """
  436. *width*, *height* : width and height of the container box.
  437. *xdescent*, *ydescent* : descent of the box in x- and y-direction.
  438. *clip* : Whether to clip the children
  439. """
  440. super().__init__()
  441. self.width = width
  442. self.height = height
  443. self.xdescent = xdescent
  444. self.ydescent = ydescent
  445. self._clip_children = clip
  446. self.offset_transform = mtransforms.Affine2D()
  447. self.offset_transform.clear()
  448. self.offset_transform.translate(0, 0)
  449. self.dpi_transform = mtransforms.Affine2D()
  450. @property
  451. def clip_children(self):
  452. """
  453. If the children of this DrawingArea should be clipped
  454. by DrawingArea bounding box.
  455. """
  456. return self._clip_children
  457. @clip_children.setter
  458. def clip_children(self, val):
  459. self._clip_children = bool(val)
  460. self.stale = True
  461. def get_transform(self):
  462. """
  463. Return the :class:`~matplotlib.transforms.Transform` applied
  464. to the children
  465. """
  466. return self.dpi_transform + self.offset_transform
  467. def set_transform(self, t):
  468. """
  469. set_transform is ignored.
  470. """
  471. pass
  472. def set_offset(self, xy):
  473. """
  474. Set the offset of the container.
  475. Parameters
  476. ----------
  477. xy : (float, float)
  478. The (x,y) coordinates of the offset in display units.
  479. """
  480. self._offset = xy
  481. self.offset_transform.clear()
  482. self.offset_transform.translate(xy[0], xy[1])
  483. self.stale = True
  484. def get_offset(self):
  485. """
  486. return offset of the container.
  487. """
  488. return self._offset
  489. def get_window_extent(self, renderer):
  490. '''
  491. get the bounding box in display space.
  492. '''
  493. w, h, xd, yd = self.get_extent(renderer)
  494. ox, oy = self.get_offset() # w, h, xd, yd)
  495. return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
  496. def get_extent(self, renderer):
  497. """
  498. Return with, height, xdescent, ydescent of box
  499. """
  500. dpi_cor = renderer.points_to_pixels(1.)
  501. return self.width * dpi_cor, self.height * dpi_cor, \
  502. self.xdescent * dpi_cor, self.ydescent * dpi_cor
  503. def add_artist(self, a):
  504. 'Add any :class:`~matplotlib.artist.Artist` to the container box'
  505. self._children.append(a)
  506. if not a.is_transform_set():
  507. a.set_transform(self.get_transform())
  508. if self.axes is not None:
  509. a.axes = self.axes
  510. fig = self.figure
  511. if fig is not None:
  512. a.set_figure(fig)
  513. def draw(self, renderer):
  514. """
  515. Draw the children
  516. """
  517. dpi_cor = renderer.points_to_pixels(1.)
  518. self.dpi_transform.clear()
  519. self.dpi_transform.scale(dpi_cor, dpi_cor)
  520. # At this point the DrawingArea has a transform
  521. # to the display space so the path created is
  522. # good for clipping children
  523. tpath = mtransforms.TransformedPath(
  524. mpath.Path([[0, 0], [0, self.height],
  525. [self.width, self.height],
  526. [self.width, 0]]),
  527. self.get_transform())
  528. for c in self._children:
  529. if self._clip_children and not (c.clipbox or c._clippath):
  530. c.set_clip_path(tpath)
  531. c.draw(renderer)
  532. bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  533. self.stale = False
  534. class TextArea(OffsetBox):
  535. """
  536. The TextArea is contains a single Text instance. The text is
  537. placed at (0,0) with baseline+left alignment. The width and height
  538. of the TextArea instance is the width and height of the its child
  539. text.
  540. """
  541. def __init__(self, s,
  542. textprops=None,
  543. multilinebaseline=None,
  544. minimumdescent=True,
  545. ):
  546. """
  547. Parameters
  548. ----------
  549. s : str
  550. a string to be displayed.
  551. textprops : `~matplotlib.font_manager.FontProperties`, optional
  552. multilinebaseline : bool, optional
  553. If `True`, baseline for multiline text is adjusted so that
  554. it is (approximatedly) center-aligned with singleline
  555. text.
  556. minimumdescent : bool, optional
  557. If `True`, the box has a minimum descent of "p".
  558. """
  559. if textprops is None:
  560. textprops = {}
  561. if "va" not in textprops:
  562. textprops["va"] = "baseline"
  563. self._text = mtext.Text(0, 0, s, **textprops)
  564. OffsetBox.__init__(self)
  565. self._children = [self._text]
  566. self.offset_transform = mtransforms.Affine2D()
  567. self.offset_transform.clear()
  568. self.offset_transform.translate(0, 0)
  569. self._baseline_transform = mtransforms.Affine2D()
  570. self._text.set_transform(self.offset_transform +
  571. self._baseline_transform)
  572. self._multilinebaseline = multilinebaseline
  573. self._minimumdescent = minimumdescent
  574. def set_text(self, s):
  575. "Set the text of this area as a string."
  576. self._text.set_text(s)
  577. self.stale = True
  578. def get_text(self):
  579. "Returns the string representation of this area's text"
  580. return self._text.get_text()
  581. def set_multilinebaseline(self, t):
  582. """
  583. Set multilinebaseline .
  584. If True, baseline for multiline text is
  585. adjusted so that it is (approximatedly) center-aligned with
  586. singleline text.
  587. """
  588. self._multilinebaseline = t
  589. self.stale = True
  590. def get_multilinebaseline(self):
  591. """
  592. get multilinebaseline .
  593. """
  594. return self._multilinebaseline
  595. def set_minimumdescent(self, t):
  596. """
  597. Set minimumdescent .
  598. If True, extent of the single line text is adjusted so that
  599. it has minimum descent of "p"
  600. """
  601. self._minimumdescent = t
  602. self.stale = True
  603. def get_minimumdescent(self):
  604. """
  605. get minimumdescent.
  606. """
  607. return self._minimumdescent
  608. def set_transform(self, t):
  609. """
  610. set_transform is ignored.
  611. """
  612. pass
  613. def set_offset(self, xy):
  614. """
  615. Set the offset of the container.
  616. Parameters
  617. ----------
  618. xy : (float, float)
  619. The (x,y) coordinates of the offset in display units.
  620. """
  621. self._offset = xy
  622. self.offset_transform.clear()
  623. self.offset_transform.translate(xy[0], xy[1])
  624. self.stale = True
  625. def get_offset(self):
  626. """
  627. return offset of the container.
  628. """
  629. return self._offset
  630. def get_window_extent(self, renderer):
  631. '''
  632. get the bounding box in display space.
  633. '''
  634. w, h, xd, yd = self.get_extent(renderer)
  635. ox, oy = self.get_offset() # w, h, xd, yd)
  636. return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
  637. def get_extent(self, renderer):
  638. clean_line, ismath = self._text.is_math_text(self._text._text)
  639. _, h_, d_ = renderer.get_text_width_height_descent(
  640. "lp", self._text._fontproperties, ismath=False)
  641. bbox, info, d = self._text._get_layout(renderer)
  642. w, h = bbox.width, bbox.height
  643. line = info[-1][0] # last line
  644. self._baseline_transform.clear()
  645. if len(info) > 1 and self._multilinebaseline:
  646. d_new = 0.5 * h - 0.5 * (h_ - d_)
  647. self._baseline_transform.translate(0, d - d_new)
  648. d = d_new
  649. else: # single line
  650. h_d = max(h_ - d_, h - d)
  651. if self.get_minimumdescent():
  652. ## to have a minimum descent, #i.e., "l" and "p" have same
  653. ## descents.
  654. d = max(d, d_)
  655. #else:
  656. # d = d
  657. h = h_d + d
  658. return w, h, 0., d
  659. def draw(self, renderer):
  660. """
  661. Draw the children
  662. """
  663. self._text.draw(renderer)
  664. bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  665. self.stale = False
  666. class AuxTransformBox(OffsetBox):
  667. """
  668. Offset Box with the aux_transform. Its children will be
  669. transformed with the aux_transform first then will be
  670. offseted. The absolute coordinate of the aux_transform is meaning
  671. as it will be automatically adjust so that the left-lower corner
  672. of the bounding box of children will be set to (0,0) before the
  673. offset transform.
  674. It is similar to drawing area, except that the extent of the box
  675. is not predetermined but calculated from the window extent of its
  676. children. Furthermore, the extent of the children will be
  677. calculated in the transformed coordinate.
  678. """
  679. def __init__(self, aux_transform):
  680. self.aux_transform = aux_transform
  681. OffsetBox.__init__(self)
  682. self.offset_transform = mtransforms.Affine2D()
  683. self.offset_transform.clear()
  684. self.offset_transform.translate(0, 0)
  685. # ref_offset_transform is used to make the offset_transform is
  686. # always reference to the lower-left corner of the bbox of its
  687. # children.
  688. self.ref_offset_transform = mtransforms.Affine2D()
  689. self.ref_offset_transform.clear()
  690. def add_artist(self, a):
  691. 'Add any :class:`~matplotlib.artist.Artist` to the container box'
  692. self._children.append(a)
  693. a.set_transform(self.get_transform())
  694. self.stale = True
  695. def get_transform(self):
  696. """
  697. Return the :class:`~matplotlib.transforms.Transform` applied
  698. to the children
  699. """
  700. return self.aux_transform + \
  701. self.ref_offset_transform + \
  702. self.offset_transform
  703. def set_transform(self, t):
  704. """
  705. set_transform is ignored.
  706. """
  707. pass
  708. def set_offset(self, xy):
  709. """
  710. Set the offset of the container.
  711. Parameters
  712. ----------
  713. xy : (float, float)
  714. The (x,y) coordinates of the offset in display units.
  715. """
  716. self._offset = xy
  717. self.offset_transform.clear()
  718. self.offset_transform.translate(xy[0], xy[1])
  719. self.stale = True
  720. def get_offset(self):
  721. """
  722. return offset of the container.
  723. """
  724. return self._offset
  725. def get_window_extent(self, renderer):
  726. '''
  727. get the bounding box in display space.
  728. '''
  729. w, h, xd, yd = self.get_extent(renderer)
  730. ox, oy = self.get_offset() # w, h, xd, yd)
  731. return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
  732. def get_extent(self, renderer):
  733. # clear the offset transforms
  734. _off = self.offset_transform.to_values() # to be restored later
  735. self.ref_offset_transform.clear()
  736. self.offset_transform.clear()
  737. # calculate the extent
  738. bboxes = [c.get_window_extent(renderer) for c in self._children]
  739. ub = mtransforms.Bbox.union(bboxes)
  740. # adjust ref_offset_tansform
  741. self.ref_offset_transform.translate(-ub.x0, -ub.y0)
  742. # restor offset transform
  743. mtx = self.offset_transform.matrix_from_values(*_off)
  744. self.offset_transform.set_matrix(mtx)
  745. return ub.width, ub.height, 0., 0.
  746. def draw(self, renderer):
  747. """
  748. Draw the children
  749. """
  750. for c in self._children:
  751. c.draw(renderer)
  752. bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  753. self.stale = False
  754. class AnchoredOffsetbox(OffsetBox):
  755. """
  756. An offset box placed according to the legend location
  757. loc. AnchoredOffsetbox has a single child. When multiple children
  758. is needed, use other OffsetBox class to enclose them. By default,
  759. the offset box is anchored against its parent axes. You may
  760. explicitly specify the bbox_to_anchor.
  761. """
  762. zorder = 5 # zorder of the legend
  763. # Location codes
  764. codes = {'upper right': 1,
  765. 'upper left': 2,
  766. 'lower left': 3,
  767. 'lower right': 4,
  768. 'right': 5,
  769. 'center left': 6,
  770. 'center right': 7,
  771. 'lower center': 8,
  772. 'upper center': 9,
  773. 'center': 10,
  774. }
  775. def __init__(self, loc,
  776. pad=0.4, borderpad=0.5,
  777. child=None, prop=None, frameon=True,
  778. bbox_to_anchor=None,
  779. bbox_transform=None,
  780. **kwargs):
  781. """
  782. loc is a string or an integer specifying the legend location.
  783. The valid location codes are::
  784. 'upper right' : 1,
  785. 'upper left' : 2,
  786. 'lower left' : 3,
  787. 'lower right' : 4,
  788. 'right' : 5, (same as 'center right', for back-compatibility)
  789. 'center left' : 6,
  790. 'center right' : 7,
  791. 'lower center' : 8,
  792. 'upper center' : 9,
  793. 'center' : 10,
  794. pad : pad around the child for drawing a frame. given in
  795. fraction of fontsize.
  796. borderpad : pad between offsetbox frame and the bbox_to_anchor,
  797. child : OffsetBox instance that will be anchored.
  798. prop : font property. This is only used as a reference for paddings.
  799. frameon : draw a frame box if True.
  800. bbox_to_anchor : bbox to anchor. Use self.axes.bbox if None.
  801. bbox_transform : with which the bbox_to_anchor will be transformed.
  802. """
  803. super().__init__(**kwargs)
  804. self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
  805. self.set_child(child)
  806. if isinstance(loc, str):
  807. try:
  808. loc = self.codes[loc]
  809. except KeyError:
  810. raise ValueError('Unrecognized location "%s". Valid '
  811. 'locations are\n\t%s\n'
  812. % (loc, '\n\t'.join(self.codes)))
  813. self.loc = loc
  814. self.borderpad = borderpad
  815. self.pad = pad
  816. if prop is None:
  817. self.prop = FontProperties(size=rcParams["legend.fontsize"])
  818. elif isinstance(prop, dict):
  819. self.prop = FontProperties(**prop)
  820. if "size" not in prop:
  821. self.prop.set_size(rcParams["legend.fontsize"])
  822. else:
  823. self.prop = prop
  824. self.patch = FancyBboxPatch(
  825. xy=(0.0, 0.0), width=1., height=1.,
  826. facecolor='w', edgecolor='k',
  827. mutation_scale=self.prop.get_size_in_points(),
  828. snap=True
  829. )
  830. self.patch.set_boxstyle("square", pad=0)
  831. self._drawFrame = frameon
  832. def set_child(self, child):
  833. "set the child to be anchored"
  834. self._child = child
  835. if child is not None:
  836. child.axes = self.axes
  837. self.stale = True
  838. def get_child(self):
  839. "return the child"
  840. return self._child
  841. def get_children(self):
  842. "return the list of children"
  843. return [self._child]
  844. def get_extent(self, renderer):
  845. """
  846. return the extent of the artist. The extent of the child
  847. added with the pad is returned
  848. """
  849. w, h, xd, yd = self.get_child().get_extent(renderer)
  850. fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
  851. pad = self.pad * fontsize
  852. return w + 2 * pad, h + 2 * pad, xd + pad, yd + pad
  853. def get_bbox_to_anchor(self):
  854. """
  855. return the bbox that the legend will be anchored
  856. """
  857. if self._bbox_to_anchor is None:
  858. return self.axes.bbox
  859. else:
  860. transform = self._bbox_to_anchor_transform
  861. if transform is None:
  862. return self._bbox_to_anchor
  863. else:
  864. return TransformedBbox(self._bbox_to_anchor,
  865. transform)
  866. def set_bbox_to_anchor(self, bbox, transform=None):
  867. """
  868. set the bbox that the child will be anchored.
  869. *bbox* can be a Bbox instance, a list of [left, bottom, width,
  870. height], or a list of [left, bottom] where the width and
  871. height will be assumed to be zero. The bbox will be
  872. transformed to display coordinate by the given transform.
  873. """
  874. if bbox is None or isinstance(bbox, BboxBase):
  875. self._bbox_to_anchor = bbox
  876. else:
  877. try:
  878. l = len(bbox)
  879. except TypeError:
  880. raise ValueError("Invalid argument for bbox : %s" % str(bbox))
  881. if l == 2:
  882. bbox = [bbox[0], bbox[1], 0, 0]
  883. self._bbox_to_anchor = Bbox.from_bounds(*bbox)
  884. self._bbox_to_anchor_transform = transform
  885. self.stale = True
  886. def get_window_extent(self, renderer):
  887. '''
  888. get the bounding box in display space.
  889. '''
  890. self._update_offset_func(renderer)
  891. w, h, xd, yd = self.get_extent(renderer)
  892. ox, oy = self.get_offset(w, h, xd, yd, renderer)
  893. return Bbox.from_bounds(ox - xd, oy - yd, w, h)
  894. def _update_offset_func(self, renderer, fontsize=None):
  895. """
  896. Update the offset func which depends on the dpi of the
  897. renderer (because of the padding).
  898. """
  899. if fontsize is None:
  900. fontsize = renderer.points_to_pixels(
  901. self.prop.get_size_in_points())
  902. def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self):
  903. bbox = Bbox.from_bounds(0, 0, w, h)
  904. borderpad = self.borderpad * fontsize
  905. bbox_to_anchor = self.get_bbox_to_anchor()
  906. x0, y0 = self._get_anchored_bbox(self.loc,
  907. bbox,
  908. bbox_to_anchor,
  909. borderpad)
  910. return x0 + xd, y0 + yd
  911. self.set_offset(_offset)
  912. def update_frame(self, bbox, fontsize=None):
  913. self.patch.set_bounds(bbox.x0, bbox.y0,
  914. bbox.width, bbox.height)
  915. if fontsize:
  916. self.patch.set_mutation_scale(fontsize)
  917. def draw(self, renderer):
  918. "draw the artist"
  919. if not self.get_visible():
  920. return
  921. fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
  922. self._update_offset_func(renderer, fontsize)
  923. if self._drawFrame:
  924. # update the location and size of the legend
  925. bbox = self.get_window_extent(renderer)
  926. self.update_frame(bbox, fontsize)
  927. self.patch.draw(renderer)
  928. width, height, xdescent, ydescent = self.get_extent(renderer)
  929. px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
  930. self.get_child().set_offset((px, py))
  931. self.get_child().draw(renderer)
  932. self.stale = False
  933. def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad):
  934. """
  935. return the position of the bbox anchored at the parentbbox
  936. with the loc code, with the borderpad.
  937. """
  938. assert loc in range(1, 11) # called only internally
  939. BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
  940. anchor_coefs = {UR: "NE",
  941. UL: "NW",
  942. LL: "SW",
  943. LR: "SE",
  944. R: "E",
  945. CL: "W",
  946. CR: "E",
  947. LC: "S",
  948. UC: "N",
  949. C: "C"}
  950. c = anchor_coefs[loc]
  951. container = parentbbox.padded(-borderpad)
  952. anchored_box = bbox.anchored(c, container=container)
  953. return anchored_box.x0, anchored_box.y0
  954. class AnchoredText(AnchoredOffsetbox):
  955. """
  956. AnchoredOffsetbox with Text.
  957. """
  958. def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs):
  959. """
  960. Parameters
  961. ----------
  962. s : string
  963. Text.
  964. loc : str
  965. Location code.
  966. pad : float, optional
  967. Pad between the text and the frame as fraction of the font
  968. size.
  969. borderpad : float, optional
  970. Pad between the frame and the axes (or *bbox_to_anchor*).
  971. prop : dictionary, optional, default: None
  972. Dictionary of keyword parameters to be passed to the
  973. `~matplotlib.text.Text` instance contained inside AnchoredText.
  974. Notes
  975. -----
  976. Other keyword parameters of `AnchoredOffsetbox` are also
  977. allowed.
  978. """
  979. if prop is None:
  980. prop = {}
  981. badkwargs = {'ha', 'horizontalalignment', 'va', 'verticalalignment'}
  982. if badkwargs & set(prop):
  983. warnings.warn("Mixing horizontalalignment or verticalalignment "
  984. "with AnchoredText is not supported.")
  985. self.txt = TextArea(s, textprops=prop, minimumdescent=False)
  986. fp = self.txt._text.get_fontproperties()
  987. super().__init__(
  988. loc, pad=pad, borderpad=borderpad, child=self.txt, prop=fp,
  989. **kwargs)
  990. class OffsetImage(OffsetBox):
  991. def __init__(self, arr,
  992. zoom=1,
  993. cmap=None,
  994. norm=None,
  995. interpolation=None,
  996. origin=None,
  997. filternorm=1,
  998. filterrad=4.0,
  999. resample=False,
  1000. dpi_cor=True,
  1001. **kwargs
  1002. ):
  1003. OffsetBox.__init__(self)
  1004. self._dpi_cor = dpi_cor
  1005. self.image = BboxImage(bbox=self.get_window_extent,
  1006. cmap=cmap,
  1007. norm=norm,
  1008. interpolation=interpolation,
  1009. origin=origin,
  1010. filternorm=filternorm,
  1011. filterrad=filterrad,
  1012. resample=resample,
  1013. **kwargs
  1014. )
  1015. self._children = [self.image]
  1016. self.set_zoom(zoom)
  1017. self.set_data(arr)
  1018. def set_data(self, arr):
  1019. self._data = np.asarray(arr)
  1020. self.image.set_data(self._data)
  1021. self.stale = True
  1022. def get_data(self):
  1023. return self._data
  1024. def set_zoom(self, zoom):
  1025. self._zoom = zoom
  1026. self.stale = True
  1027. def get_zoom(self):
  1028. return self._zoom
  1029. # def set_axes(self, axes):
  1030. # self.image.set_axes(axes)
  1031. # martist.Artist.set_axes(self, axes)
  1032. # def set_offset(self, xy):
  1033. # """
  1034. # Set the offset of the container.
  1035. #
  1036. # Parameters
  1037. # ----------
  1038. # xy : (float, float)
  1039. # The (x,y) coordinates of the offset in display units.
  1040. # """
  1041. # self._offset = xy
  1042. # self.offset_transform.clear()
  1043. # self.offset_transform.translate(xy[0], xy[1])
  1044. def get_offset(self):
  1045. """
  1046. return offset of the container.
  1047. """
  1048. return self._offset
  1049. def get_children(self):
  1050. return [self.image]
  1051. def get_window_extent(self, renderer):
  1052. '''
  1053. get the bounding box in display space.
  1054. '''
  1055. w, h, xd, yd = self.get_extent(renderer)
  1056. ox, oy = self.get_offset()
  1057. return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
  1058. def get_extent(self, renderer):
  1059. if self._dpi_cor: # True, do correction
  1060. dpi_cor = renderer.points_to_pixels(1.)
  1061. else:
  1062. dpi_cor = 1.
  1063. zoom = self.get_zoom()
  1064. data = self.get_data()
  1065. ny, nx = data.shape[:2]
  1066. w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom
  1067. return w, h, 0, 0
  1068. def draw(self, renderer):
  1069. """
  1070. Draw the children
  1071. """
  1072. self.image.draw(renderer)
  1073. # bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
  1074. self.stale = False
  1075. class AnnotationBbox(martist.Artist, _AnnotationBase):
  1076. """
  1077. Annotation-like class, but with offsetbox instead of Text.
  1078. """
  1079. zorder = 3
  1080. def __str__(self):
  1081. return "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1])
  1082. @docstring.dedent_interpd
  1083. def __init__(self, offsetbox, xy,
  1084. xybox=None,
  1085. xycoords='data',
  1086. boxcoords=None,
  1087. frameon=True, pad=0.4, # BboxPatch
  1088. annotation_clip=None,
  1089. box_alignment=(0.5, 0.5),
  1090. bboxprops=None,
  1091. arrowprops=None,
  1092. fontsize=None,
  1093. **kwargs):
  1094. """
  1095. *offsetbox* : OffsetBox instance
  1096. *xycoords* : same as Annotation but can be a tuple of two
  1097. strings which are interpreted as x and y coordinates.
  1098. *boxcoords* : similar to textcoords as Annotation but can be a
  1099. tuple of two strings which are interpreted as x and y
  1100. coordinates.
  1101. *box_alignment* : a tuple of two floats for a vertical and
  1102. horizontal alignment of the offset box w.r.t. the *boxcoords*.
  1103. The lower-left corner is (0.0) and upper-right corner is (1.1).
  1104. other parameters are identical to that of Annotation.
  1105. """
  1106. martist.Artist.__init__(self, **kwargs)
  1107. _AnnotationBase.__init__(self,
  1108. xy,
  1109. xycoords=xycoords,
  1110. annotation_clip=annotation_clip)
  1111. self.offsetbox = offsetbox
  1112. self.arrowprops = arrowprops
  1113. self.set_fontsize(fontsize)
  1114. if xybox is None:
  1115. self.xybox = xy
  1116. else:
  1117. self.xybox = xybox
  1118. if boxcoords is None:
  1119. self.boxcoords = xycoords
  1120. else:
  1121. self.boxcoords = boxcoords
  1122. if arrowprops is not None:
  1123. self._arrow_relpos = self.arrowprops.pop("relpos", (0.5, 0.5))
  1124. self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
  1125. **self.arrowprops)
  1126. else:
  1127. self._arrow_relpos = None
  1128. self.arrow_patch = None
  1129. #self._fw, self._fh = 0., 0. # for alignment
  1130. self._box_alignment = box_alignment
  1131. # frame
  1132. self.patch = FancyBboxPatch(
  1133. xy=(0.0, 0.0), width=1., height=1.,
  1134. facecolor='w', edgecolor='k',
  1135. mutation_scale=self.prop.get_size_in_points(),
  1136. snap=True
  1137. )
  1138. self.patch.set_boxstyle("square", pad=pad)
  1139. if bboxprops:
  1140. self.patch.set(**bboxprops)
  1141. self._drawFrame = frameon
  1142. @property
  1143. def xyann(self):
  1144. return self.xybox
  1145. @xyann.setter
  1146. def xyann(self, xyann):
  1147. self.xybox = xyann
  1148. self.stale = True
  1149. @property
  1150. def anncoords(self):
  1151. return self.boxcoords
  1152. @anncoords.setter
  1153. def anncoords(self, coords):
  1154. self.boxcoords = coords
  1155. self.stale = True
  1156. def contains(self, event):
  1157. t, tinfo = self.offsetbox.contains(event)
  1158. #if self.arrow_patch is not None:
  1159. # a,ainfo=self.arrow_patch.contains(event)
  1160. # t = t or a
  1161. # self.arrow_patch is currently not checked as this can be a line - JJ
  1162. return t, tinfo
  1163. def get_children(self):
  1164. children = [self.offsetbox, self.patch]
  1165. if self.arrow_patch:
  1166. children.append(self.arrow_patch)
  1167. return children
  1168. def set_figure(self, fig):
  1169. if self.arrow_patch is not None:
  1170. self.arrow_patch.set_figure(fig)
  1171. self.offsetbox.set_figure(fig)
  1172. martist.Artist.set_figure(self, fig)
  1173. def set_fontsize(self, s=None):
  1174. """
  1175. set fontsize in points
  1176. """
  1177. if s is None:
  1178. s = rcParams["legend.fontsize"]
  1179. self.prop = FontProperties(size=s)
  1180. self.stale = True
  1181. def get_fontsize(self, s=None):
  1182. """
  1183. return fontsize in points
  1184. """
  1185. return self.prop.get_size_in_points()
  1186. def update_positions(self, renderer):
  1187. """
  1188. Update the pixel positions of the annotated point and the text.
  1189. """
  1190. xy_pixel = self._get_position_xy(renderer)
  1191. self._update_position_xybox(renderer, xy_pixel)
  1192. mutation_scale = renderer.points_to_pixels(self.get_fontsize())
  1193. self.patch.set_mutation_scale(mutation_scale)
  1194. if self.arrow_patch:
  1195. self.arrow_patch.set_mutation_scale(mutation_scale)
  1196. def _update_position_xybox(self, renderer, xy_pixel):
  1197. """
  1198. Update the pixel positions of the annotation text and the arrow
  1199. patch.
  1200. """
  1201. x, y = self.xybox
  1202. if isinstance(self.boxcoords, tuple):
  1203. xcoord, ycoord = self.boxcoords
  1204. x1, y1 = self._get_xy(renderer, x, y, xcoord)
  1205. x2, y2 = self._get_xy(renderer, x, y, ycoord)
  1206. ox0, oy0 = x1, y2
  1207. else:
  1208. ox0, oy0 = self._get_xy(renderer, x, y, self.boxcoords)
  1209. w, h, xd, yd = self.offsetbox.get_extent(renderer)
  1210. _fw, _fh = self._box_alignment
  1211. self.offsetbox.set_offset((ox0 - _fw * w + xd, oy0 - _fh * h + yd))
  1212. # update patch position
  1213. bbox = self.offsetbox.get_window_extent(renderer)
  1214. #self.offsetbox.set_offset((ox0-_fw*w, oy0-_fh*h))
  1215. self.patch.set_bounds(bbox.x0, bbox.y0,
  1216. bbox.width, bbox.height)
  1217. x, y = xy_pixel
  1218. ox1, oy1 = x, y
  1219. if self.arrowprops:
  1220. x0, y0 = x, y
  1221. d = self.arrowprops.copy()
  1222. # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key.
  1223. # adjust the starting point of the arrow relative to
  1224. # the textbox.
  1225. # TODO : Rotation needs to be accounted.
  1226. relpos = self._arrow_relpos
  1227. ox0 = bbox.x0 + bbox.width * relpos[0]
  1228. oy0 = bbox.y0 + bbox.height * relpos[1]
  1229. # The arrow will be drawn from (ox0, oy0) to (ox1,
  1230. # oy1). It will be first clipped by patchA and patchB.
  1231. # Then it will be shrunk by shrinkA and shrinkB
  1232. # (in points). If patch A is not set, self.bbox_patch
  1233. # is used.
  1234. self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
  1235. fs = self.prop.get_size_in_points()
  1236. mutation_scale = d.pop("mutation_scale", fs)
  1237. mutation_scale = renderer.points_to_pixels(mutation_scale)
  1238. self.arrow_patch.set_mutation_scale(mutation_scale)
  1239. patchA = d.pop("patchA", self.patch)
  1240. self.arrow_patch.set_patchA(patchA)
  1241. def draw(self, renderer):
  1242. """
  1243. Draw the :class:`Annotation` object to the given *renderer*.
  1244. """
  1245. if renderer is not None:
  1246. self._renderer = renderer
  1247. if not self.get_visible():
  1248. return
  1249. xy_pixel = self._get_position_xy(renderer)
  1250. if not self._check_xy(renderer, xy_pixel):
  1251. return
  1252. self.update_positions(renderer)
  1253. if self.arrow_patch is not None:
  1254. if self.arrow_patch.figure is None and self.figure is not None:
  1255. self.arrow_patch.figure = self.figure
  1256. self.arrow_patch.draw(renderer)
  1257. if self._drawFrame:
  1258. self.patch.draw(renderer)
  1259. self.offsetbox.draw(renderer)
  1260. self.stale = False
  1261. class DraggableBase(object):
  1262. """
  1263. helper code for a draggable artist (legend, offsetbox)
  1264. The derived class must override following two method.
  1265. def save_offset(self):
  1266. pass
  1267. def update_offset(self, dx, dy):
  1268. pass
  1269. *save_offset* is called when the object is picked for dragging and it
  1270. is meant to save reference position of the artist.
  1271. *update_offset* is called during the dragging. dx and dy is the pixel
  1272. offset from the point where the mouse drag started.
  1273. Optionally you may override following two methods.
  1274. def artist_picker(self, artist, evt):
  1275. return self.ref_artist.contains(evt)
  1276. def finalize_offset(self):
  1277. pass
  1278. *artist_picker* is a picker method that will be
  1279. used. *finalize_offset* is called when the mouse is released. In
  1280. current implementation of DraggableLegend and DraggableAnnotation,
  1281. *update_offset* places the artists simply in display
  1282. coordinates. And *finalize_offset* recalculate their position in
  1283. the normalized axes coordinate and set a relavant attribute.
  1284. """
  1285. def __init__(self, ref_artist, use_blit=False):
  1286. self.ref_artist = ref_artist
  1287. self.got_artist = False
  1288. self.canvas = self.ref_artist.figure.canvas
  1289. self._use_blit = use_blit and self.canvas.supports_blit
  1290. c2 = self.canvas.mpl_connect('pick_event', self.on_pick)
  1291. c3 = self.canvas.mpl_connect('button_release_event', self.on_release)
  1292. ref_artist.set_picker(self.artist_picker)
  1293. self.cids = [c2, c3]
  1294. def on_motion(self, evt):
  1295. if self._check_still_parented() and self.got_artist:
  1296. dx = evt.x - self.mouse_x
  1297. dy = evt.y - self.mouse_y
  1298. self.update_offset(dx, dy)
  1299. self.canvas.draw()
  1300. def on_motion_blit(self, evt):
  1301. if self._check_still_parented() and self.got_artist:
  1302. dx = evt.x - self.mouse_x
  1303. dy = evt.y - self.mouse_y
  1304. self.update_offset(dx, dy)
  1305. self.canvas.restore_region(self.background)
  1306. self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
  1307. self.canvas.blit(self.ref_artist.figure.bbox)
  1308. def on_pick(self, evt):
  1309. if self._check_still_parented() and evt.artist == self.ref_artist:
  1310. self.mouse_x = evt.mouseevent.x
  1311. self.mouse_y = evt.mouseevent.y
  1312. self.got_artist = True
  1313. if self._use_blit:
  1314. self.ref_artist.set_animated(True)
  1315. self.canvas.draw()
  1316. self.background = self.canvas.copy_from_bbox(
  1317. self.ref_artist.figure.bbox)
  1318. self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
  1319. self.canvas.blit(self.ref_artist.figure.bbox)
  1320. self._c1 = self.canvas.mpl_connect('motion_notify_event',
  1321. self.on_motion_blit)
  1322. else:
  1323. self._c1 = self.canvas.mpl_connect('motion_notify_event',
  1324. self.on_motion)
  1325. self.save_offset()
  1326. def on_release(self, event):
  1327. if self._check_still_parented() and self.got_artist:
  1328. self.finalize_offset()
  1329. self.got_artist = False
  1330. self.canvas.mpl_disconnect(self._c1)
  1331. if self._use_blit:
  1332. self.ref_artist.set_animated(False)
  1333. def _check_still_parented(self):
  1334. if self.ref_artist.figure is None:
  1335. self.disconnect()
  1336. return False
  1337. else:
  1338. return True
  1339. def disconnect(self):
  1340. """disconnect the callbacks"""
  1341. for cid in self.cids:
  1342. self.canvas.mpl_disconnect(cid)
  1343. try:
  1344. c1 = self._c1
  1345. except AttributeError:
  1346. pass
  1347. else:
  1348. self.canvas.mpl_disconnect(c1)
  1349. def artist_picker(self, artist, evt):
  1350. return self.ref_artist.contains(evt)
  1351. def save_offset(self):
  1352. pass
  1353. def update_offset(self, dx, dy):
  1354. pass
  1355. def finalize_offset(self):
  1356. pass
  1357. class DraggableOffsetBox(DraggableBase):
  1358. def __init__(self, ref_artist, offsetbox, use_blit=False):
  1359. DraggableBase.__init__(self, ref_artist, use_blit=use_blit)
  1360. self.offsetbox = offsetbox
  1361. def save_offset(self):
  1362. offsetbox = self.offsetbox
  1363. renderer = offsetbox.figure._cachedRenderer
  1364. w, h, xd, yd = offsetbox.get_extent(renderer)
  1365. offset = offsetbox.get_offset(w, h, xd, yd, renderer)
  1366. self.offsetbox_x, self.offsetbox_y = offset
  1367. self.offsetbox.set_offset(offset)
  1368. def update_offset(self, dx, dy):
  1369. loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy
  1370. self.offsetbox.set_offset(loc_in_canvas)
  1371. def get_loc_in_canvas(self):
  1372. offsetbox = self.offsetbox
  1373. renderer = offsetbox.figure._cachedRenderer
  1374. w, h, xd, yd = offsetbox.get_extent(renderer)
  1375. ox, oy = offsetbox._offset
  1376. loc_in_canvas = (ox - xd, oy - yd)
  1377. return loc_in_canvas
  1378. class DraggableAnnotation(DraggableBase):
  1379. def __init__(self, annotation, use_blit=False):
  1380. DraggableBase.__init__(self, annotation, use_blit=use_blit)
  1381. self.annotation = annotation
  1382. def save_offset(self):
  1383. ann = self.annotation
  1384. self.ox, self.oy = ann.get_transform().transform(ann.xyann)
  1385. def update_offset(self, dx, dy):
  1386. ann = self.annotation
  1387. ann.xyann = ann.get_transform().inverted().transform(
  1388. (self.ox + dx, self.oy + dy))
  1389. if __name__ == "__main__":
  1390. import matplotlib.pyplot as plt
  1391. fig = plt.figure(1)
  1392. fig.clf()
  1393. ax = plt.subplot(121)
  1394. #txt = ax.text(0.5, 0.5, "Test", size=30, ha="center", color="w")
  1395. kwargs = dict()
  1396. a = np.arange(256).reshape(16, 16) / 256.
  1397. myimage = OffsetImage(a,
  1398. zoom=2,
  1399. norm=None,
  1400. origin=None,
  1401. **kwargs
  1402. )
  1403. ax.add_artist(myimage)
  1404. myimage.set_offset((100, 100))
  1405. myimage2 = OffsetImage(a,
  1406. zoom=2,
  1407. norm=None,
  1408. origin=None,
  1409. **kwargs
  1410. )
  1411. ann = AnnotationBbox(myimage2, (0.5, 0.5),
  1412. xybox=(30, 30),
  1413. xycoords='data',
  1414. boxcoords="offset points",
  1415. frameon=True, pad=0.4, # BboxPatch
  1416. bboxprops=dict(boxstyle="round", fc="y"),
  1417. fontsize=None,
  1418. arrowprops=dict(arrowstyle="->"),
  1419. )
  1420. ax.add_artist(ann)
  1421. plt.draw()
  1422. plt.show()