test_offsetbox.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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 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
  11. from matplotlib.offsetbox import (
  12. AnchoredOffsetbox, AnnotationBbox, DrawingArea, OffsetImage, TextArea,
  13. _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. bg = mpatches.Rectangle((0, 0), size, size,
  27. facecolor='#CCCCCC',
  28. edgecolor='None',
  29. linewidth=0)
  30. line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
  31. color='black',
  32. linewidth=10)
  33. anchored_box = AnchoredOffsetbox(
  34. loc='center',
  35. child=da,
  36. pad=0.,
  37. frameon=False,
  38. bbox_to_anchor=(.5, .5),
  39. bbox_transform=ax.transAxes,
  40. borderpad=0.)
  41. da.add_artist(bg)
  42. da.add_artist(line)
  43. ax.add_artist(anchored_box)
  44. ax.set_xlim((0, 1))
  45. ax.set_ylim((0, 1))
  46. def test_offsetbox_clip_children():
  47. # - create a plot
  48. # - put an AnchoredOffsetbox with a child DrawingArea
  49. # at the center of the axes
  50. # - give the DrawingArea a gray background
  51. # - put a black line across the bounds of the DrawingArea
  52. # - see that the black line is clipped to the edges of
  53. # the DrawingArea.
  54. fig, ax = plt.subplots()
  55. size = 100
  56. da = DrawingArea(size, size, clip=True)
  57. bg = mpatches.Rectangle((0, 0), size, size,
  58. facecolor='#CCCCCC',
  59. edgecolor='None',
  60. linewidth=0)
  61. line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
  62. color='black',
  63. linewidth=10)
  64. anchored_box = AnchoredOffsetbox(
  65. loc='center',
  66. child=da,
  67. pad=0.,
  68. frameon=False,
  69. bbox_to_anchor=(.5, .5),
  70. bbox_transform=ax.transAxes,
  71. borderpad=0.)
  72. da.add_artist(bg)
  73. da.add_artist(line)
  74. ax.add_artist(anchored_box)
  75. fig.canvas.draw()
  76. assert not fig.stale
  77. da.clip_children = True
  78. assert fig.stale
  79. def test_offsetbox_loc_codes():
  80. # Check that valid string location codes all work with an AnchoredOffsetbox
  81. codes = {'upper right': 1,
  82. 'upper left': 2,
  83. 'lower left': 3,
  84. 'lower right': 4,
  85. 'right': 5,
  86. 'center left': 6,
  87. 'center right': 7,
  88. 'lower center': 8,
  89. 'upper center': 9,
  90. 'center': 10,
  91. }
  92. fig, ax = plt.subplots()
  93. da = DrawingArea(100, 100)
  94. for code in codes:
  95. anchored_box = AnchoredOffsetbox(loc=code, child=da)
  96. ax.add_artist(anchored_box)
  97. fig.canvas.draw()
  98. def test_expand_with_tight_layout():
  99. # Check issue reported in #10476, and updated due to #10784
  100. fig, ax = plt.subplots()
  101. d1 = [1, 2]
  102. d2 = [2, 1]
  103. ax.plot(d1, label='series 1')
  104. ax.plot(d2, label='series 2')
  105. ax.legend(ncol=2, mode='expand')
  106. fig.tight_layout() # where the crash used to happen
  107. @pytest.mark.parametrize('wd_list',
  108. ([(150, 1)], [(150, 1)]*3, [(0.1, 1)], [(0.1, 1)]*2))
  109. @pytest.mark.parametrize('total', (250, 100, 0, -1, None))
  110. @pytest.mark.parametrize('sep', (250, 1, 0, -1))
  111. @pytest.mark.parametrize('mode', ("expand", "fixed", "equal"))
  112. def test_get_packed_offsets(wd_list, total, sep, mode):
  113. # Check a (rather arbitrary) set of parameters due to successive similar
  114. # issue tickets (at least #10476 and #10784) related to corner cases
  115. # triggered inside this function when calling higher-level functions
  116. # (e.g. `Axes.legend`).
  117. # These are just some additional smoke tests. The output is untested.
  118. _get_packed_offsets(wd_list, total, sep, mode=mode)
  119. _Params = namedtuple('_params', 'wd_list, total, sep, expected')
  120. @pytest.mark.parametrize('wd_list, total, sep, expected', [
  121. _Params( # total=None
  122. [(3, 0), (1, 0), (2, 0)], total=None, sep=1, expected=(8, [0, 4, 6])),
  123. _Params( # total larger than required
  124. [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 4, 6])),
  125. _Params( # total smaller than required
  126. [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 4, 6])),
  127. ])
  128. def test_get_packed_offsets_fixed(wd_list, total, sep, expected):
  129. result = _get_packed_offsets(wd_list, total, sep, mode='fixed')
  130. assert result[0] == expected[0]
  131. assert_allclose(result[1], expected[1])
  132. @pytest.mark.parametrize('wd_list, total, sep, expected', [
  133. _Params( # total=None (implicit 1)
  134. [(.1, 0)] * 3, total=None, sep=None, expected=(1, [0, .45, .9])),
  135. _Params( # total larger than sum of widths
  136. [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 5, 8])),
  137. _Params( # total smaller sum of widths: overlapping boxes
  138. [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 2.5, 3])),
  139. ])
  140. def test_get_packed_offsets_expand(wd_list, total, sep, expected):
  141. result = _get_packed_offsets(wd_list, total, sep, mode='expand')
  142. assert result[0] == expected[0]
  143. assert_allclose(result[1], expected[1])
  144. @pytest.mark.parametrize('wd_list, total, sep, expected', [
  145. _Params( # total larger than required
  146. [(3, 0), (2, 0), (1, 0)], total=6, sep=None, expected=(6, [0, 2, 4])),
  147. _Params( # total smaller sum of widths: overlapping boxes
  148. [(3, 0), (2, 0), (1, 0), (.5, 0)], total=2, sep=None,
  149. expected=(2, [0, 0.5, 1, 1.5])),
  150. _Params( # total larger than required
  151. [(.5, 0), (1, 0), (.2, 0)], total=None, sep=1,
  152. expected=(6, [0, 2, 4])),
  153. # the case total=None, sep=None is tested separately below
  154. ])
  155. def test_get_packed_offsets_equal(wd_list, total, sep, expected):
  156. result = _get_packed_offsets(wd_list, total, sep, mode='equal')
  157. assert result[0] == expected[0]
  158. assert_allclose(result[1], expected[1])
  159. def test_get_packed_offsets_equal_total_none_sep_none():
  160. with pytest.raises(ValueError):
  161. _get_packed_offsets([(1, 0)] * 3, total=None, sep=None, mode='equal')
  162. @pytest.mark.parametrize('child_type', ['draw', 'image', 'text'])
  163. @pytest.mark.parametrize('boxcoords',
  164. ['axes fraction', 'axes pixels', 'axes points',
  165. 'data'])
  166. def test_picking(child_type, boxcoords):
  167. # These all take up approximately the same area.
  168. if child_type == 'draw':
  169. picking_child = DrawingArea(5, 5)
  170. picking_child.add_artist(mpatches.Rectangle((0, 0), 5, 5, linewidth=0))
  171. elif child_type == 'image':
  172. im = np.ones((5, 5))
  173. im[2, 2] = 0
  174. picking_child = OffsetImage(im)
  175. elif child_type == 'text':
  176. picking_child = TextArea('\N{Black Square}', textprops={'fontsize': 5})
  177. else:
  178. assert False, f'Unknown picking child type {child_type}'
  179. fig, ax = plt.subplots()
  180. ab = AnnotationBbox(picking_child, (0.5, 0.5), boxcoords=boxcoords)
  181. ab.set_picker(True)
  182. ax.add_artist(ab)
  183. calls = []
  184. fig.canvas.mpl_connect('pick_event', lambda event: calls.append(event))
  185. # Annotation should be picked by an event occurring at its center.
  186. if boxcoords == 'axes points':
  187. x, y = ax.transAxes.transform_point((0, 0))
  188. x += 0.5 * fig.dpi / 72
  189. y += 0.5 * fig.dpi / 72
  190. elif boxcoords == 'axes pixels':
  191. x, y = ax.transAxes.transform_point((0, 0))
  192. x += 0.5
  193. y += 0.5
  194. else:
  195. x, y = ax.transAxes.transform_point((0.5, 0.5))
  196. fig.canvas.draw()
  197. calls.clear()
  198. fig.canvas.button_press_event(x, y, MouseButton.LEFT)
  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. fig.canvas.button_press_event(x, y, MouseButton.LEFT)
  207. assert len(calls) == 0
  208. def test_annotationbbox_extents():
  209. plt.rcParams.update(plt.rcParamsDefault)
  210. fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
  211. ax.axis([0, 1, 0, 1])
  212. an1 = ax.annotate("Annotation", xy=(.9, .9), xytext=(1.1, 1.1),
  213. arrowprops=dict(arrowstyle="->"), clip_on=False,
  214. va="baseline", ha="left")
  215. da = DrawingArea(20, 20, 0, 0, clip=True)
  216. p = mpatches.Circle((-10, 30), 32)
  217. da.add_artist(p)
  218. ab3 = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
  219. boxcoords="axes fraction", box_alignment=(0., .5),
  220. arrowprops=dict(arrowstyle="->"))
  221. ax.add_artist(ab3)
  222. im = OffsetImage(np.random.rand(10, 10), zoom=3)
  223. im.image.axes = ax
  224. ab6 = AnnotationBbox(im, (0.5, -.3), xybox=(0, 75),
  225. xycoords='axes fraction',
  226. boxcoords="offset points", pad=0.3,
  227. arrowprops=dict(arrowstyle="->"))
  228. ax.add_artist(ab6)
  229. fig.canvas.draw()
  230. renderer = fig.canvas.get_renderer()
  231. # Test Annotation
  232. bb1w = an1.get_window_extent(renderer)
  233. bb1e = an1.get_tightbbox(renderer)
  234. target1 = [332.9, 242.8, 467.0, 298.9]
  235. assert_allclose(bb1w.extents, target1, atol=2)
  236. assert_allclose(bb1e.extents, target1, atol=2)
  237. # Test AnnotationBbox
  238. bb3w = ab3.get_window_extent(renderer)
  239. bb3e = ab3.get_tightbbox(renderer)
  240. target3 = [-17.6, 129.0, 200.7, 167.9]
  241. assert_allclose(bb3w.extents, target3, atol=2)
  242. assert_allclose(bb3e.extents, target3, atol=2)
  243. bb6w = ab6.get_window_extent(renderer)
  244. bb6e = ab6.get_tightbbox(renderer)
  245. target6 = [180.0, -32.0, 230.0, 92.9]
  246. assert_allclose(bb6w.extents, target6, atol=2)
  247. assert_allclose(bb6e.extents, target6, atol=2)
  248. # Test bbox_inches='tight'
  249. buf = io.BytesIO()
  250. fig.savefig(buf, bbox_inches='tight')
  251. buf.seek(0)
  252. shape = plt.imread(buf).shape
  253. targetshape = (350, 504, 4)
  254. assert_allclose(shape, targetshape, atol=2)
  255. # Simple smoke test for tight_layout, to make sure it does not error out.
  256. fig.canvas.draw()
  257. fig.tight_layout()
  258. fig.canvas.draw()