test_offsetbox.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. from collections import namedtuple
  2. import io
  3. import numpy as np
  4. from numpy.testing import assert_allclose
  5. import pytest
  6. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  7. import matplotlib.pyplot as plt
  8. import matplotlib.patches as mpatches
  9. import matplotlib.lines as mlines
  10. from matplotlib.backend_bases import MouseButton, MouseEvent
  11. from matplotlib.offsetbox import (
  12. AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, HPacker,
  13. OffsetBox, OffsetImage, PaddedBox, TextArea, VPacker, _get_packed_offsets)
  14. @image_comparison(['offsetbox_clipping'], remove_text=True)
  15. def test_offsetbox_clipping():
  16. # - create a plot
  17. # - put an AnchoredOffsetbox with a child DrawingArea
  18. # at the center of the axes
  19. # - give the DrawingArea a gray background
  20. # - put a black line across the bounds of the DrawingArea
  21. # - see that the black line is clipped to the edges of
  22. # the DrawingArea.
  23. fig, ax = plt.subplots()
  24. size = 100
  25. da = DrawingArea(size, size, clip=True)
  26. assert da.clip_children
  27. bg = mpatches.Rectangle((0, 0), size, size,
  28. facecolor='#CCCCCC',
  29. edgecolor='None',
  30. linewidth=0)
  31. line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
  32. color='black',
  33. linewidth=10)
  34. anchored_box = AnchoredOffsetbox(
  35. loc='center',
  36. child=da,
  37. pad=0.,
  38. frameon=False,
  39. bbox_to_anchor=(.5, .5),
  40. bbox_transform=ax.transAxes,
  41. borderpad=0.)
  42. da.add_artist(bg)
  43. da.add_artist(line)
  44. ax.add_artist(anchored_box)
  45. ax.set_xlim((0, 1))
  46. ax.set_ylim((0, 1))
  47. def test_offsetbox_clip_children():
  48. # - create a plot
  49. # - put an AnchoredOffsetbox with a child DrawingArea
  50. # at the center of the axes
  51. # - give the DrawingArea a gray background
  52. # - put a black line across the bounds of the DrawingArea
  53. # - see that the black line is clipped to the edges of
  54. # the DrawingArea.
  55. fig, ax = plt.subplots()
  56. size = 100
  57. da = DrawingArea(size, size, clip=True)
  58. bg = mpatches.Rectangle((0, 0), size, size,
  59. facecolor='#CCCCCC',
  60. edgecolor='None',
  61. linewidth=0)
  62. line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
  63. color='black',
  64. linewidth=10)
  65. anchored_box = AnchoredOffsetbox(
  66. loc='center',
  67. child=da,
  68. pad=0.,
  69. frameon=False,
  70. bbox_to_anchor=(.5, .5),
  71. bbox_transform=ax.transAxes,
  72. borderpad=0.)
  73. da.add_artist(bg)
  74. da.add_artist(line)
  75. ax.add_artist(anchored_box)
  76. fig.canvas.draw()
  77. assert not fig.stale
  78. da.clip_children = True
  79. assert fig.stale
  80. def test_offsetbox_loc_codes():
  81. # Check that valid string location codes all work with an AnchoredOffsetbox
  82. codes = {'upper right': 1,
  83. 'upper left': 2,
  84. 'lower left': 3,
  85. 'lower right': 4,
  86. 'right': 5,
  87. 'center left': 6,
  88. 'center right': 7,
  89. 'lower center': 8,
  90. 'upper center': 9,
  91. 'center': 10,
  92. }
  93. fig, ax = plt.subplots()
  94. da = DrawingArea(100, 100)
  95. for code in codes:
  96. anchored_box = AnchoredOffsetbox(loc=code, child=da)
  97. ax.add_artist(anchored_box)
  98. fig.canvas.draw()
  99. def test_expand_with_tight_layout():
  100. # Check issue reported in #10476, and updated due to #10784
  101. fig, ax = plt.subplots()
  102. d1 = [1, 2]
  103. d2 = [2, 1]
  104. ax.plot(d1, label='series 1')
  105. ax.plot(d2, label='series 2')
  106. ax.legend(ncols=2, mode='expand')
  107. fig.tight_layout() # where the crash used to happen
  108. @pytest.mark.parametrize('widths',
  109. ([150], [150, 150, 150], [0.1], [0.1, 0.1]))
  110. @pytest.mark.parametrize('total', (250, 100, 0, -1, None))
  111. @pytest.mark.parametrize('sep', (250, 1, 0, -1))
  112. @pytest.mark.parametrize('mode', ("expand", "fixed", "equal"))
  113. def test_get_packed_offsets(widths, total, sep, mode):
  114. # Check a (rather arbitrary) set of parameters due to successive similar
  115. # issue tickets (at least #10476 and #10784) related to corner cases
  116. # triggered inside this function when calling higher-level functions
  117. # (e.g. `Axes.legend`).
  118. # These are just some additional smoke tests. The output is untested.
  119. _get_packed_offsets(widths, total, sep, mode=mode)
  120. _Params = namedtuple('_Params', 'wd_list, total, sep, expected')
  121. @pytest.mark.parametrize('widths, total, sep, expected', [
  122. _Params( # total=None
  123. [3, 1, 2], total=None, sep=1, expected=(8, [0, 4, 6])),
  124. _Params( # total larger than required
  125. [3, 1, 2], total=10, sep=1, expected=(10, [0, 4, 6])),
  126. _Params( # total smaller than required
  127. [3, 1, 2], total=5, sep=1, expected=(5, [0, 4, 6])),
  128. ])
  129. def test_get_packed_offsets_fixed(widths, total, sep, expected):
  130. result = _get_packed_offsets(widths, total, sep, mode='fixed')
  131. assert result[0] == expected[0]
  132. assert_allclose(result[1], expected[1])
  133. @pytest.mark.parametrize('widths, total, sep, expected', [
  134. _Params( # total=None (implicit 1)
  135. [.1, .1, .1], total=None, sep=None, expected=(1, [0, .45, .9])),
  136. _Params( # total larger than sum of widths
  137. [3, 1, 2], total=10, sep=1, expected=(10, [0, 5, 8])),
  138. _Params( # total smaller sum of widths: overlapping boxes
  139. [3, 1, 2], total=5, sep=1, expected=(5, [0, 2.5, 3])),
  140. ])
  141. def test_get_packed_offsets_expand(widths, total, sep, expected):
  142. result = _get_packed_offsets(widths, total, sep, mode='expand')
  143. assert result[0] == expected[0]
  144. assert_allclose(result[1], expected[1])
  145. @pytest.mark.parametrize('widths, total, sep, expected', [
  146. _Params( # total larger than required
  147. [3, 2, 1], total=6, sep=None, expected=(6, [0, 2, 4])),
  148. _Params( # total smaller sum of widths: overlapping boxes
  149. [3, 2, 1, .5], total=2, sep=None, expected=(2, [0, 0.5, 1, 1.5])),
  150. _Params( # total larger than required
  151. [.5, 1, .2], total=None, sep=1, expected=(6, [0, 2, 4])),
  152. # the case total=None, sep=None is tested separately below
  153. ])
  154. def test_get_packed_offsets_equal(widths, total, sep, expected):
  155. result = _get_packed_offsets(widths, total, sep, mode='equal')
  156. assert result[0] == expected[0]
  157. assert_allclose(result[1], expected[1])
  158. def test_get_packed_offsets_equal_total_none_sep_none():
  159. with pytest.raises(ValueError):
  160. _get_packed_offsets([1, 1, 1], total=None, sep=None, mode='equal')
  161. @pytest.mark.parametrize('child_type', ['draw', 'image', 'text'])
  162. @pytest.mark.parametrize('boxcoords',
  163. ['axes fraction', 'axes pixels', 'axes points',
  164. 'data'])
  165. def test_picking(child_type, boxcoords):
  166. # These all take up approximately the same area.
  167. if child_type == 'draw':
  168. picking_child = DrawingArea(5, 5)
  169. picking_child.add_artist(mpatches.Rectangle((0, 0), 5, 5, linewidth=0))
  170. elif child_type == 'image':
  171. im = np.ones((5, 5))
  172. im[2, 2] = 0
  173. picking_child = OffsetImage(im)
  174. elif child_type == 'text':
  175. picking_child = TextArea('\N{Black Square}', textprops={'fontsize': 5})
  176. else:
  177. assert False, f'Unknown picking child type {child_type}'
  178. fig, ax = plt.subplots()
  179. ab = AnnotationBbox(picking_child, (0.5, 0.5), boxcoords=boxcoords)
  180. ab.set_picker(True)
  181. ax.add_artist(ab)
  182. calls = []
  183. fig.canvas.mpl_connect('pick_event', lambda event: calls.append(event))
  184. # Annotation should be picked by an event occurring at its center.
  185. if boxcoords == 'axes points':
  186. x, y = ax.transAxes.transform_point((0, 0))
  187. x += 0.5 * fig.dpi / 72
  188. y += 0.5 * fig.dpi / 72
  189. elif boxcoords == 'axes pixels':
  190. x, y = ax.transAxes.transform_point((0, 0))
  191. x += 0.5
  192. y += 0.5
  193. else:
  194. x, y = ax.transAxes.transform_point((0.5, 0.5))
  195. fig.canvas.draw()
  196. calls.clear()
  197. MouseEvent(
  198. "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
  199. assert len(calls) == 1 and calls[0].artist == ab
  200. # Annotation should *not* be picked by an event at its original center
  201. # point when the limits have changed enough to hide the *xy* point.
  202. ax.set_xlim(-1, 0)
  203. ax.set_ylim(-1, 0)
  204. fig.canvas.draw()
  205. calls.clear()
  206. MouseEvent(
  207. "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
  208. assert len(calls) == 0
  209. @image_comparison(['anchoredtext_align.png'], remove_text=True, style='mpl20')
  210. def test_anchoredtext_horizontal_alignment():
  211. fig, ax = plt.subplots()
  212. text0 = AnchoredText("test\ntest long text", loc="center left",
  213. pad=0.2, prop={"ha": "left"})
  214. ax.add_artist(text0)
  215. text1 = AnchoredText("test\ntest long text", loc="center",
  216. pad=0.2, prop={"ha": "center"})
  217. ax.add_artist(text1)
  218. text2 = AnchoredText("test\ntest long text", loc="center right",
  219. pad=0.2, prop={"ha": "right"})
  220. ax.add_artist(text2)
  221. @pytest.mark.parametrize("extent_kind", ["window_extent", "tightbbox"])
  222. def test_annotationbbox_extents(extent_kind):
  223. plt.rcParams.update(plt.rcParamsDefault)
  224. fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
  225. ax.axis([0, 1, 0, 1])
  226. an1 = ax.annotate("Annotation", xy=(.9, .9), xytext=(1.1, 1.1),
  227. arrowprops=dict(arrowstyle="->"), clip_on=False,
  228. va="baseline", ha="left")
  229. da = DrawingArea(20, 20, 0, 0, clip=True)
  230. p = mpatches.Circle((-10, 30), 32)
  231. da.add_artist(p)
  232. ab3 = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
  233. boxcoords="axes fraction", box_alignment=(0., .5),
  234. arrowprops=dict(arrowstyle="->"))
  235. ax.add_artist(ab3)
  236. im = OffsetImage(np.random.rand(10, 10), zoom=3)
  237. im.image.axes = ax
  238. ab6 = AnnotationBbox(im, (0.5, -.3), xybox=(0, 75),
  239. xycoords='axes fraction',
  240. boxcoords="offset points", pad=0.3,
  241. arrowprops=dict(arrowstyle="->"))
  242. ax.add_artist(ab6)
  243. # Test Annotation
  244. bb1 = getattr(an1, f"get_{extent_kind}")()
  245. target1 = [332.9, 242.8, 467.0, 298.9]
  246. assert_allclose(bb1.extents, target1, atol=2)
  247. # Test AnnotationBbox
  248. bb3 = getattr(ab3, f"get_{extent_kind}")()
  249. target3 = [-17.6, 129.0, 200.7, 167.9]
  250. assert_allclose(bb3.extents, target3, atol=2)
  251. bb6 = getattr(ab6, f"get_{extent_kind}")()
  252. target6 = [180.0, -32.0, 230.0, 92.9]
  253. assert_allclose(bb6.extents, target6, atol=2)
  254. # Test bbox_inches='tight'
  255. buf = io.BytesIO()
  256. fig.savefig(buf, bbox_inches='tight')
  257. buf.seek(0)
  258. shape = plt.imread(buf).shape
  259. targetshape = (350, 504, 4)
  260. assert_allclose(shape, targetshape, atol=2)
  261. # Simple smoke test for tight_layout, to make sure it does not error out.
  262. fig.canvas.draw()
  263. fig.tight_layout()
  264. fig.canvas.draw()
  265. def test_zorder():
  266. assert OffsetBox(zorder=42).zorder == 42
  267. def test_arrowprops_copied():
  268. da = DrawingArea(20, 20, 0, 0, clip=True)
  269. arrowprops = {"arrowstyle": "->", "relpos": (.3, .7)}
  270. ab = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
  271. boxcoords="axes fraction", box_alignment=(0., .5),
  272. arrowprops=arrowprops)
  273. assert ab.arrowprops is not ab
  274. assert arrowprops["relpos"] == (.3, .7)
  275. @pytest.mark.parametrize("align", ["baseline", "bottom", "top",
  276. "left", "right", "center"])
  277. def test_packers(align):
  278. # set the DPI to match points to make the math easier below
  279. fig = plt.figure(dpi=72)
  280. renderer = fig.canvas.get_renderer()
  281. x1, y1 = 10, 30
  282. x2, y2 = 20, 60
  283. r1 = DrawingArea(x1, y1)
  284. r2 = DrawingArea(x2, y2)
  285. # HPacker
  286. hpacker = HPacker(children=[r1, r2], align=align)
  287. hpacker.draw(renderer)
  288. bbox = hpacker.get_bbox(renderer)
  289. px, py = hpacker.get_offset(bbox, renderer)
  290. # width, height, xdescent, ydescent
  291. assert_allclose(bbox.bounds, (0, 0, x1 + x2, max(y1, y2)))
  292. # internal element placement
  293. if align in ("baseline", "left", "bottom"):
  294. y_height = 0
  295. elif align in ("right", "top"):
  296. y_height = y2 - y1
  297. elif align == "center":
  298. y_height = (y2 - y1) / 2
  299. # x-offsets, y-offsets
  300. assert_allclose([child.get_offset() for child in hpacker.get_children()],
  301. [(px, py + y_height), (px + x1, py)])
  302. # VPacker
  303. vpacker = VPacker(children=[r1, r2], align=align)
  304. vpacker.draw(renderer)
  305. bbox = vpacker.get_bbox(renderer)
  306. px, py = vpacker.get_offset(bbox, renderer)
  307. # width, height, xdescent, ydescent
  308. assert_allclose(bbox.bounds, (0, -max(y1, y2), max(x1, x2), y1 + y2))
  309. # internal element placement
  310. if align in ("baseline", "left", "bottom"):
  311. x_height = 0
  312. elif align in ("right", "top"):
  313. x_height = x2 - x1
  314. elif align == "center":
  315. x_height = (x2 - x1) / 2
  316. # x-offsets, y-offsets
  317. assert_allclose([child.get_offset() for child in vpacker.get_children()],
  318. [(px + x_height, py), (px, py - y2)])
  319. def test_paddedbox_default_values():
  320. # smoke test paddedbox for correct default value
  321. fig, ax = plt.subplots()
  322. at = AnchoredText("foo", 'upper left')
  323. pb = PaddedBox(at, patch_attrs={'facecolor': 'r'}, draw_frame=True)
  324. ax.add_artist(pb)
  325. fig.draw_without_rendering()
  326. def test_annotationbbox_properties():
  327. ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
  328. xycoords='data')
  329. assert ab.xyann == (0.5, 0.5) # xy if xybox not given
  330. assert ab.anncoords == 'data' # xycoords if boxcoords not given
  331. ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
  332. xybox=(-0.2, 0.4), xycoords='data',
  333. boxcoords='axes fraction')
  334. assert ab.xyann == (-0.2, 0.4) # xybox if given
  335. assert ab.anncoords == 'axes fraction' # boxcoords if given
  336. def test_textarea_properties():
  337. ta = TextArea('Foo')
  338. assert ta.get_text() == 'Foo'
  339. assert not ta.get_multilinebaseline()
  340. ta.set_text('Bar')
  341. ta.set_multilinebaseline(True)
  342. assert ta.get_text() == 'Bar'
  343. assert ta.get_multilinebaseline()
  344. @check_figures_equal()
  345. def test_textarea_set_text(fig_test, fig_ref):
  346. ax_ref = fig_ref.add_subplot()
  347. text0 = AnchoredText("Foo", "upper left")
  348. ax_ref.add_artist(text0)
  349. ax_test = fig_test.add_subplot()
  350. text1 = AnchoredText("Bar", "upper left")
  351. ax_test.add_artist(text1)
  352. text1.txt.set_text("Foo")
  353. @image_comparison(['paddedbox.png'], remove_text=True, style='mpl20')
  354. def test_paddedbox():
  355. fig, ax = plt.subplots()
  356. ta = TextArea("foo")
  357. pb = PaddedBox(ta, pad=5, patch_attrs={'facecolor': 'r'}, draw_frame=True)
  358. ab = AnchoredOffsetbox('upper left', child=pb)
  359. ax.add_artist(ab)
  360. ta = TextArea("bar")
  361. pb = PaddedBox(ta, pad=10, patch_attrs={'facecolor': 'b'})
  362. ab = AnchoredOffsetbox('upper right', child=pb)
  363. ax.add_artist(ab)
  364. ta = TextArea("foobar")
  365. pb = PaddedBox(ta, pad=15, draw_frame=True)
  366. ab = AnchoredOffsetbox('lower right', child=pb)
  367. ax.add_artist(ab)
  368. def test_remove_draggable():
  369. fig, ax = plt.subplots()
  370. an = ax.annotate("foo", (.5, .5))
  371. an.draggable(True)
  372. an.remove()
  373. MouseEvent("button_release_event", fig.canvas, 1, 1)._process()