offsetbox.py 57 KB

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