test_tightlayout.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import warnings
  2. import numpy as np
  3. from numpy.testing import assert_array_equal
  4. import pytest
  5. import matplotlib as mpl
  6. from matplotlib.testing.decorators import image_comparison
  7. import matplotlib.pyplot as plt
  8. from matplotlib.offsetbox import AnchoredOffsetbox, DrawingArea
  9. from matplotlib.patches import Rectangle
  10. pytestmark = [
  11. pytest.mark.usefixtures('text_placeholders')
  12. ]
  13. def example_plot(ax, fontsize=12):
  14. ax.plot([1, 2])
  15. ax.locator_params(nbins=3)
  16. ax.set_xlabel('x-label', fontsize=fontsize)
  17. ax.set_ylabel('y-label', fontsize=fontsize)
  18. ax.set_title('Title', fontsize=fontsize)
  19. @image_comparison(['tight_layout1'], style='mpl20')
  20. def test_tight_layout1():
  21. """Test tight_layout for a single subplot."""
  22. fig, ax = plt.subplots()
  23. example_plot(ax, fontsize=24)
  24. plt.tight_layout()
  25. @image_comparison(['tight_layout2'], style='mpl20')
  26. def test_tight_layout2():
  27. """Test tight_layout for multiple subplots."""
  28. fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)
  29. example_plot(ax1)
  30. example_plot(ax2)
  31. example_plot(ax3)
  32. example_plot(ax4)
  33. plt.tight_layout()
  34. @image_comparison(['tight_layout3'], style='mpl20')
  35. def test_tight_layout3():
  36. """Test tight_layout for multiple subplots."""
  37. ax1 = plt.subplot(221)
  38. ax2 = plt.subplot(223)
  39. ax3 = plt.subplot(122)
  40. example_plot(ax1)
  41. example_plot(ax2)
  42. example_plot(ax3)
  43. plt.tight_layout()
  44. @image_comparison(['tight_layout4'], style='mpl20')
  45. def test_tight_layout4():
  46. """Test tight_layout for subplot2grid."""
  47. ax1 = plt.subplot2grid((3, 3), (0, 0))
  48. ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2)
  49. ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=2, rowspan=2)
  50. ax4 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)
  51. example_plot(ax1)
  52. example_plot(ax2)
  53. example_plot(ax3)
  54. example_plot(ax4)
  55. plt.tight_layout()
  56. @image_comparison(['tight_layout5'], style='mpl20')
  57. def test_tight_layout5():
  58. """Test tight_layout for image."""
  59. ax = plt.subplot()
  60. arr = np.arange(100).reshape((10, 10))
  61. ax.imshow(arr, interpolation="none")
  62. plt.tight_layout()
  63. @image_comparison(['tight_layout6'], style='mpl20')
  64. def test_tight_layout6():
  65. """Test tight_layout for gridspec."""
  66. # This raises warnings since tight layout cannot
  67. # do this fully automatically. But the test is
  68. # correct since the layout is manually edited
  69. with warnings.catch_warnings():
  70. warnings.simplefilter("ignore", UserWarning)
  71. fig = plt.figure()
  72. gs1 = mpl.gridspec.GridSpec(2, 1)
  73. ax1 = fig.add_subplot(gs1[0])
  74. ax2 = fig.add_subplot(gs1[1])
  75. example_plot(ax1)
  76. example_plot(ax2)
  77. gs1.tight_layout(fig, rect=[0, 0, 0.5, 1])
  78. gs2 = mpl.gridspec.GridSpec(3, 1)
  79. for ss in gs2:
  80. ax = fig.add_subplot(ss)
  81. example_plot(ax)
  82. ax.set_title("")
  83. ax.set_xlabel("")
  84. ax.set_xlabel("x-label", fontsize=12)
  85. gs2.tight_layout(fig, rect=[0.5, 0, 1, 1], h_pad=0.45)
  86. top = min(gs1.top, gs2.top)
  87. bottom = max(gs1.bottom, gs2.bottom)
  88. gs1.tight_layout(fig, rect=[None, 0 + (bottom-gs1.bottom),
  89. 0.5, 1 - (gs1.top-top)])
  90. gs2.tight_layout(fig, rect=[0.5, 0 + (bottom-gs2.bottom),
  91. None, 1 - (gs2.top-top)],
  92. h_pad=0.45)
  93. @image_comparison(['tight_layout7'], style='mpl20')
  94. def test_tight_layout7():
  95. # tight layout with left and right titles
  96. fontsize = 24
  97. fig, ax = plt.subplots()
  98. ax.plot([1, 2])
  99. ax.locator_params(nbins=3)
  100. ax.set_xlabel('x-label', fontsize=fontsize)
  101. ax.set_ylabel('y-label', fontsize=fontsize)
  102. ax.set_title('Left Title', loc='left', fontsize=fontsize)
  103. ax.set_title('Right Title', loc='right', fontsize=fontsize)
  104. plt.tight_layout()
  105. @image_comparison(['tight_layout8'], style='mpl20', tol=0.005)
  106. def test_tight_layout8():
  107. """Test automatic use of tight_layout."""
  108. fig = plt.figure()
  109. fig.set_layout_engine(layout='tight', pad=0.1)
  110. ax = fig.add_subplot()
  111. example_plot(ax, fontsize=24)
  112. fig.draw_without_rendering()
  113. @image_comparison(['tight_layout9'], style='mpl20')
  114. def test_tight_layout9():
  115. # Test tight_layout for non-visible subplots
  116. # GH 8244
  117. f, axarr = plt.subplots(2, 2)
  118. axarr[1][1].set_visible(False)
  119. plt.tight_layout()
  120. def test_outward_ticks():
  121. """Test automatic use of tight_layout."""
  122. fig = plt.figure()
  123. ax = fig.add_subplot(221)
  124. ax.xaxis.set_tick_params(tickdir='out', length=16, width=3)
  125. ax.yaxis.set_tick_params(tickdir='out', length=16, width=3)
  126. ax.xaxis.set_tick_params(
  127. tickdir='out', length=32, width=3, tick1On=True, which='minor')
  128. ax.yaxis.set_tick_params(
  129. tickdir='out', length=32, width=3, tick1On=True, which='minor')
  130. ax.xaxis.set_ticks([0], minor=True)
  131. ax.yaxis.set_ticks([0], minor=True)
  132. ax = fig.add_subplot(222)
  133. ax.xaxis.set_tick_params(tickdir='in', length=32, width=3)
  134. ax.yaxis.set_tick_params(tickdir='in', length=32, width=3)
  135. ax = fig.add_subplot(223)
  136. ax.xaxis.set_tick_params(tickdir='inout', length=32, width=3)
  137. ax.yaxis.set_tick_params(tickdir='inout', length=32, width=3)
  138. ax = fig.add_subplot(224)
  139. ax.xaxis.set_tick_params(tickdir='out', length=32, width=3)
  140. ax.yaxis.set_tick_params(tickdir='out', length=32, width=3)
  141. plt.tight_layout()
  142. # These values were obtained after visual checking that they correspond
  143. # to a tight layouting that did take the ticks into account.
  144. expected = [
  145. [[0.092, 0.605], [0.433, 0.933]],
  146. [[0.581, 0.605], [0.922, 0.933]],
  147. [[0.092, 0.138], [0.433, 0.466]],
  148. [[0.581, 0.138], [0.922, 0.466]],
  149. ]
  150. for nn, ax in enumerate(fig.axes):
  151. assert_array_equal(np.round(ax.get_position().get_points(), 3),
  152. expected[nn])
  153. def add_offsetboxes(ax, size=10, margin=.1, color='black'):
  154. """
  155. Surround ax with OffsetBoxes
  156. """
  157. m, mp = margin, 1+margin
  158. anchor_points = [(-m, -m), (-m, .5), (-m, mp),
  159. (.5, mp), (mp, mp), (mp, .5),
  160. (mp, -m), (.5, -m)]
  161. for point in anchor_points:
  162. da = DrawingArea(size, size)
  163. background = Rectangle((0, 0), width=size,
  164. height=size,
  165. facecolor=color,
  166. edgecolor='None',
  167. linewidth=0,
  168. antialiased=False)
  169. da.add_artist(background)
  170. anchored_box = AnchoredOffsetbox(
  171. loc='center',
  172. child=da,
  173. pad=0.,
  174. frameon=False,
  175. bbox_to_anchor=point,
  176. bbox_transform=ax.transAxes,
  177. borderpad=0.)
  178. ax.add_artist(anchored_box)
  179. def test_tight_layout_offsetboxes():
  180. # 0.
  181. # - Create 4 subplots
  182. # - Plot a diagonal line on them
  183. # - Use tight_layout
  184. #
  185. # 1.
  186. # - Same 4 subplots
  187. # - Surround each plot with 7 boxes
  188. # - Use tight_layout
  189. # - See that the squares are included in the tight_layout and that the squares do
  190. # not overlap
  191. #
  192. # 2.
  193. # - Make the squares around the Axes invisible
  194. # - See that the invisible squares do not affect the tight_layout
  195. rows = cols = 2
  196. colors = ['red', 'blue', 'green', 'yellow']
  197. x = y = [0, 1]
  198. def _subplots(with_boxes):
  199. fig, axs = plt.subplots(rows, cols)
  200. for ax, color in zip(axs.flat, colors):
  201. ax.plot(x, y, color=color)
  202. if with_boxes:
  203. add_offsetboxes(ax, 20, color=color)
  204. return fig, axs
  205. # 0.
  206. fig0, axs0 = _subplots(False)
  207. fig0.tight_layout()
  208. # 1.
  209. fig1, axs1 = _subplots(True)
  210. fig1.tight_layout()
  211. # The AnchoredOffsetbox should be added to the bounding of the Axes, causing them to
  212. # be smaller than the plain figure.
  213. for ax0, ax1 in zip(axs0.flat, axs1.flat):
  214. bbox0 = ax0.get_position()
  215. bbox1 = ax1.get_position()
  216. assert bbox1.x0 > bbox0.x0
  217. assert bbox1.x1 < bbox0.x1
  218. assert bbox1.y0 > bbox0.y0
  219. assert bbox1.y1 < bbox0.y1
  220. # No AnchoredOffsetbox should overlap with another.
  221. bboxes = []
  222. for ax1 in axs1.flat:
  223. for child in ax1.get_children():
  224. if not isinstance(child, AnchoredOffsetbox):
  225. continue
  226. bbox = child.get_window_extent()
  227. for other_bbox in bboxes:
  228. assert not bbox.overlaps(other_bbox)
  229. bboxes.append(bbox)
  230. # 2.
  231. fig2, axs2 = _subplots(True)
  232. for ax in axs2.flat:
  233. for child in ax.get_children():
  234. if isinstance(child, AnchoredOffsetbox):
  235. child.set_visible(False)
  236. fig2.tight_layout()
  237. # The invisible AnchoredOffsetbox should not count for tight layout, so it should
  238. # look the same as when they were never added.
  239. for ax0, ax2 in zip(axs0.flat, axs2.flat):
  240. bbox0 = ax0.get_position()
  241. bbox2 = ax2.get_position()
  242. assert_array_equal(bbox2.get_points(), bbox0.get_points())
  243. def test_empty_layout():
  244. """Test that tight layout doesn't cause an error when there are no Axes."""
  245. fig = plt.gcf()
  246. fig.tight_layout()
  247. @pytest.mark.parametrize("label", ["xlabel", "ylabel"])
  248. def test_verybig_decorators(label):
  249. """Test that no warning emitted when xlabel/ylabel too big."""
  250. fig, ax = plt.subplots(figsize=(3, 2))
  251. ax.set(**{label: 'a' * 100})
  252. def test_big_decorators_horizontal():
  253. """Test that doesn't warn when xlabel too big."""
  254. fig, axs = plt.subplots(1, 2, figsize=(3, 2))
  255. axs[0].set_xlabel('a' * 30)
  256. axs[1].set_xlabel('b' * 30)
  257. def test_big_decorators_vertical():
  258. """Test that doesn't warn when ylabel too big."""
  259. fig, axs = plt.subplots(2, 1, figsize=(3, 2))
  260. axs[0].set_ylabel('a' * 20)
  261. axs[1].set_ylabel('b' * 20)
  262. def test_badsubplotgrid():
  263. # test that we get warning for mismatched subplot grids, not than an error
  264. plt.subplot2grid((4, 5), (0, 0))
  265. # this is the bad entry:
  266. plt.subplot2grid((5, 5), (0, 3), colspan=3, rowspan=5)
  267. with pytest.warns(UserWarning):
  268. plt.tight_layout()
  269. def test_collapsed():
  270. # test that if the amount of space required to make all the axes
  271. # decorations fit would mean that the actual Axes would end up with size
  272. # zero (i.e. margins add up to more than the available width) that a call
  273. # to tight_layout will not get applied:
  274. fig, ax = plt.subplots(tight_layout=True)
  275. ax.set_xlim([0, 1])
  276. ax.set_ylim([0, 1])
  277. ax.annotate('BIG LONG STRING', xy=(1.25, 2), xytext=(10.5, 1.75),
  278. annotation_clip=False)
  279. p1 = ax.get_position()
  280. with pytest.warns(UserWarning):
  281. plt.tight_layout()
  282. p2 = ax.get_position()
  283. assert p1.width == p2.width
  284. # test that passing a rect doesn't crash...
  285. with pytest.warns(UserWarning):
  286. plt.tight_layout(rect=[0, 0, 0.8, 0.8])
  287. def test_suptitle():
  288. fig, ax = plt.subplots(tight_layout=True)
  289. st = fig.suptitle("foo")
  290. t = ax.set_title("bar")
  291. fig.canvas.draw()
  292. assert st.get_window_extent().y0 > t.get_window_extent().y1
  293. @pytest.mark.backend("pdf")
  294. def test_non_agg_renderer(monkeypatch, recwarn):
  295. unpatched_init = mpl.backend_bases.RendererBase.__init__
  296. def __init__(self, *args, **kwargs):
  297. # Check that we don't instantiate any other renderer than a pdf
  298. # renderer to perform pdf tight layout.
  299. assert isinstance(self, mpl.backends.backend_pdf.RendererPdf)
  300. unpatched_init(self, *args, **kwargs)
  301. monkeypatch.setattr(mpl.backend_bases.RendererBase, "__init__", __init__)
  302. fig, ax = plt.subplots()
  303. fig.tight_layout()
  304. def test_manual_colorbar():
  305. # This should warn, but not raise
  306. fig, axes = plt.subplots(1, 2)
  307. pts = axes[1].scatter([0, 1], [0, 1], c=[1, 5])
  308. ax_rect = axes[1].get_position()
  309. cax = fig.add_axes(
  310. [ax_rect.x1 + 0.005, ax_rect.y0, 0.015, ax_rect.height]
  311. )
  312. fig.colorbar(pts, cax=cax)
  313. with pytest.warns(UserWarning, match="This figure includes Axes"):
  314. fig.tight_layout()
  315. def test_clipped_to_axes():
  316. # Ensure that _fully_clipped_to_axes() returns True under default
  317. # conditions for all projection types. Axes.get_tightbbox()
  318. # uses this to skip artists in layout calculations.
  319. arr = np.arange(100).reshape((10, 10))
  320. fig = plt.figure(figsize=(6, 2))
  321. ax1 = fig.add_subplot(131, projection='rectilinear')
  322. ax2 = fig.add_subplot(132, projection='mollweide')
  323. ax3 = fig.add_subplot(133, projection='polar')
  324. for ax in (ax1, ax2, ax3):
  325. # Default conditions (clipped by ax.bbox or ax.patch)
  326. ax.grid(False)
  327. h, = ax.plot(arr[:, 0])
  328. m = ax.pcolor(arr)
  329. assert h._fully_clipped_to_axes()
  330. assert m._fully_clipped_to_axes()
  331. # Non-default conditions (not clipped by ax.patch)
  332. rect = Rectangle((0, 0), 0.5, 0.5, transform=ax.transAxes)
  333. h.set_clip_path(rect)
  334. m.set_clip_path(rect.get_path(), rect.get_transform())
  335. assert not h._fully_clipped_to_axes()
  336. assert not m._fully_clipped_to_axes()
  337. def test_tight_pads():
  338. fig, ax = plt.subplots()
  339. with pytest.warns(PendingDeprecationWarning,
  340. match='will be deprecated'):
  341. fig.set_tight_layout({'pad': 0.15})
  342. fig.draw_without_rendering()
  343. def test_tight_kwargs():
  344. fig, ax = plt.subplots(tight_layout={'pad': 0.15})
  345. fig.draw_without_rendering()
  346. def test_tight_toggle():
  347. fig, ax = plt.subplots()
  348. with pytest.warns(PendingDeprecationWarning):
  349. fig.set_tight_layout(True)
  350. assert fig.get_tight_layout()
  351. fig.set_tight_layout(False)
  352. assert not fig.get_tight_layout()
  353. fig.set_tight_layout(True)
  354. assert fig.get_tight_layout()