test_patches.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. """
  2. Tests specific to the patches module.
  3. """
  4. import numpy as np
  5. from numpy.testing import assert_almost_equal, assert_array_equal
  6. import pytest
  7. from matplotlib.cbook import MatplotlibDeprecationWarning
  8. from matplotlib.patches import Polygon, Rectangle, FancyArrowPatch
  9. from matplotlib.testing.decorators import image_comparison, check_figures_equal
  10. import matplotlib.pyplot as plt
  11. from matplotlib import (
  12. collections as mcollections, colors as mcolors, patches as mpatches,
  13. path as mpath, style as mstyle, transforms as mtransforms)
  14. import sys
  15. on_win = (sys.platform == 'win32')
  16. def test_Polygon_close():
  17. #: GitHub issue #1018 identified a bug in the Polygon handling
  18. #: of the closed attribute; the path was not getting closed
  19. #: when set_xy was used to set the vertices.
  20. # open set of vertices:
  21. xy = [[0, 0], [0, 1], [1, 1]]
  22. # closed set:
  23. xyclosed = xy + [[0, 0]]
  24. # start with open path and close it:
  25. p = Polygon(xy, closed=True)
  26. assert_array_equal(p.get_xy(), xyclosed)
  27. p.set_xy(xy)
  28. assert_array_equal(p.get_xy(), xyclosed)
  29. # start with closed path and open it:
  30. p = Polygon(xyclosed, closed=False)
  31. assert_array_equal(p.get_xy(), xy)
  32. p.set_xy(xyclosed)
  33. assert_array_equal(p.get_xy(), xy)
  34. # start with open path and leave it open:
  35. p = Polygon(xy, closed=False)
  36. assert_array_equal(p.get_xy(), xy)
  37. p.set_xy(xy)
  38. assert_array_equal(p.get_xy(), xy)
  39. # start with closed path and leave it closed:
  40. p = Polygon(xyclosed, closed=True)
  41. assert_array_equal(p.get_xy(), xyclosed)
  42. p.set_xy(xyclosed)
  43. assert_array_equal(p.get_xy(), xyclosed)
  44. def test_rotate_rect():
  45. loc = np.asarray([1.0, 2.0])
  46. width = 2
  47. height = 3
  48. angle = 30.0
  49. # A rotated rectangle
  50. rect1 = Rectangle(loc, width, height, angle=angle)
  51. # A non-rotated rectangle
  52. rect2 = Rectangle(loc, width, height)
  53. # Set up an explicit rotation matrix (in radians)
  54. angle_rad = np.pi * angle / 180.0
  55. rotation_matrix = np.array([[np.cos(angle_rad), -np.sin(angle_rad)],
  56. [np.sin(angle_rad), np.cos(angle_rad)]])
  57. # Translate to origin, rotate each vertex, and then translate back
  58. new_verts = np.inner(rotation_matrix, rect2.get_verts() - loc).T + loc
  59. # They should be the same
  60. assert_almost_equal(rect1.get_verts(), new_verts)
  61. def test_negative_rect():
  62. # These two rectangles have the same vertices, but starting from a
  63. # different point. (We also drop the last vertex, which is a duplicate.)
  64. pos_vertices = Rectangle((-3, -2), 3, 2).get_verts()[:-1]
  65. neg_vertices = Rectangle((0, 0), -3, -2).get_verts()[:-1]
  66. assert_array_equal(np.roll(neg_vertices, 2, 0), pos_vertices)
  67. @image_comparison(['clip_to_bbox'])
  68. def test_clip_to_bbox():
  69. fig = plt.figure()
  70. ax = fig.add_subplot(111)
  71. ax.set_xlim([-18, 20])
  72. ax.set_ylim([-150, 100])
  73. path = mpath.Path.unit_regular_star(8).deepcopy()
  74. path.vertices *= [10, 100]
  75. path.vertices -= [5, 25]
  76. path2 = mpath.Path.unit_circle().deepcopy()
  77. path2.vertices *= [10, 100]
  78. path2.vertices += [10, -25]
  79. combined = mpath.Path.make_compound_path(path, path2)
  80. patch = mpatches.PathPatch(
  81. combined, alpha=0.5, facecolor='coral', edgecolor='none')
  82. ax.add_patch(patch)
  83. bbox = mtransforms.Bbox([[-12, -77.5], [50, -110]])
  84. result_path = combined.clip_to_bbox(bbox)
  85. result_patch = mpatches.PathPatch(
  86. result_path, alpha=0.5, facecolor='green', lw=4, edgecolor='black')
  87. ax.add_patch(result_patch)
  88. @image_comparison(['patch_alpha_coloring'], remove_text=True)
  89. def test_patch_alpha_coloring():
  90. """
  91. Test checks that the patch and collection are rendered with the specified
  92. alpha values in their facecolor and edgecolor.
  93. """
  94. star = mpath.Path.unit_regular_star(6)
  95. circle = mpath.Path.unit_circle()
  96. # concatenate the star with an internal cutout of the circle
  97. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  98. codes = np.concatenate([circle.codes, star.codes])
  99. cut_star1 = mpath.Path(verts, codes)
  100. cut_star2 = mpath.Path(verts + 1, codes)
  101. ax = plt.axes()
  102. patch = mpatches.PathPatch(cut_star1,
  103. linewidth=5, linestyle='dashdot',
  104. facecolor=(1, 0, 0, 0.5),
  105. edgecolor=(0, 0, 1, 0.75))
  106. ax.add_patch(patch)
  107. col = mcollections.PathCollection([cut_star2],
  108. linewidth=5, linestyles='dashdot',
  109. facecolor=(1, 0, 0, 0.5),
  110. edgecolor=(0, 0, 1, 0.75))
  111. ax.add_collection(col)
  112. ax.set_xlim([-1, 2])
  113. ax.set_ylim([-1, 2])
  114. @image_comparison(['patch_alpha_override'], remove_text=True)
  115. def test_patch_alpha_override():
  116. #: Test checks that specifying an alpha attribute for a patch or
  117. #: collection will override any alpha component of the facecolor
  118. #: or edgecolor.
  119. star = mpath.Path.unit_regular_star(6)
  120. circle = mpath.Path.unit_circle()
  121. # concatenate the star with an internal cutout of the circle
  122. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  123. codes = np.concatenate([circle.codes, star.codes])
  124. cut_star1 = mpath.Path(verts, codes)
  125. cut_star2 = mpath.Path(verts + 1, codes)
  126. ax = plt.axes()
  127. patch = mpatches.PathPatch(cut_star1,
  128. linewidth=5, linestyle='dashdot',
  129. alpha=0.25,
  130. facecolor=(1, 0, 0, 0.5),
  131. edgecolor=(0, 0, 1, 0.75))
  132. ax.add_patch(patch)
  133. col = mcollections.PathCollection([cut_star2],
  134. linewidth=5, linestyles='dashdot',
  135. alpha=0.25,
  136. facecolor=(1, 0, 0, 0.5),
  137. edgecolor=(0, 0, 1, 0.75))
  138. ax.add_collection(col)
  139. ax.set_xlim([-1, 2])
  140. ax.set_ylim([-1, 2])
  141. @pytest.mark.style('default')
  142. def test_patch_color_none():
  143. # Make sure the alpha kwarg does not override 'none' facecolor.
  144. # Addresses issue #7478.
  145. c = plt.Circle((0, 0), 1, facecolor='none', alpha=1)
  146. assert c.get_facecolor()[0] == 0
  147. @image_comparison(['patch_custom_linestyle'], remove_text=True)
  148. def test_patch_custom_linestyle():
  149. #: A test to check that patches and collections accept custom dash
  150. #: patterns as linestyle and that they display correctly.
  151. star = mpath.Path.unit_regular_star(6)
  152. circle = mpath.Path.unit_circle()
  153. # concatenate the star with an internal cutout of the circle
  154. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  155. codes = np.concatenate([circle.codes, star.codes])
  156. cut_star1 = mpath.Path(verts, codes)
  157. cut_star2 = mpath.Path(verts + 1, codes)
  158. ax = plt.axes()
  159. patch = mpatches.PathPatch(cut_star1,
  160. linewidth=5, linestyle=(0.0, (5.0, 7.0, 10.0, 7.0)),
  161. facecolor=(1, 0, 0),
  162. edgecolor=(0, 0, 1))
  163. ax.add_patch(patch)
  164. col = mcollections.PathCollection([cut_star2],
  165. linewidth=5, linestyles=[(0.0, (5.0, 7.0, 10.0, 7.0))],
  166. facecolor=(1, 0, 0),
  167. edgecolor=(0, 0, 1))
  168. ax.add_collection(col)
  169. ax.set_xlim([-1, 2])
  170. ax.set_ylim([-1, 2])
  171. def test_patch_linestyle_accents():
  172. #: Test if linestyle can also be specified with short mnemonics like "--"
  173. #: c.f. GitHub issue #2136
  174. star = mpath.Path.unit_regular_star(6)
  175. circle = mpath.Path.unit_circle()
  176. # concatenate the star with an internal cutout of the circle
  177. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  178. codes = np.concatenate([circle.codes, star.codes])
  179. linestyles = ["-", "--", "-.", ":",
  180. "solid", "dashed", "dashdot", "dotted"]
  181. fig = plt.figure()
  182. ax = fig.add_subplot(1, 1, 1)
  183. for i, ls in enumerate(linestyles):
  184. star = mpath.Path(verts + i, codes)
  185. patch = mpatches.PathPatch(star,
  186. linewidth=3, linestyle=ls,
  187. facecolor=(1, 0, 0),
  188. edgecolor=(0, 0, 1))
  189. ax.add_patch(patch)
  190. ax.set_xlim([-1, i + 1])
  191. ax.set_ylim([-1, i + 1])
  192. fig.canvas.draw()
  193. def test_wedge_movement():
  194. param_dict = {'center': ((0, 0), (1, 1), 'set_center'),
  195. 'r': (5, 8, 'set_radius'),
  196. 'width': (2, 3, 'set_width'),
  197. 'theta1': (0, 30, 'set_theta1'),
  198. 'theta2': (45, 50, 'set_theta2')}
  199. init_args = {k: v[0] for k, v in param_dict.items()}
  200. w = mpatches.Wedge(**init_args)
  201. for attr, (old_v, new_v, func) in param_dict.items():
  202. assert getattr(w, attr) == old_v
  203. getattr(w, func)(new_v)
  204. assert getattr(w, attr) == new_v
  205. # png needs tol>=0.06, pdf tol>=1.617
  206. @image_comparison(['wedge_range'], remove_text=True, tol=1.65 if on_win else 0)
  207. def test_wedge_range():
  208. ax = plt.axes()
  209. t1 = 2.313869244286224
  210. args = [[52.31386924, 232.31386924],
  211. [52.313869244286224, 232.31386924428622],
  212. [t1, t1 + 180.0],
  213. [0, 360],
  214. [90, 90 + 360],
  215. [-180, 180],
  216. [0, 380],
  217. [45, 46],
  218. [46, 45]]
  219. for i, (theta1, theta2) in enumerate(args):
  220. x = i % 3
  221. y = i // 3
  222. wedge = mpatches.Wedge((x * 3, y * 3), 1, theta1, theta2,
  223. facecolor='none', edgecolor='k', lw=3)
  224. ax.add_artist(wedge)
  225. ax.set_xlim([-2, 8])
  226. ax.set_ylim([-2, 9])
  227. def test_patch_str():
  228. """
  229. Check that patches have nice and working `str` representation.
  230. Note that the logic is that `__str__` is defined such that:
  231. str(eval(str(p))) == str(p)
  232. """
  233. p = mpatches.Circle(xy=(1, 2), radius=3)
  234. assert str(p) == 'Circle(xy=(1, 2), radius=3)'
  235. p = mpatches.Ellipse(xy=(1, 2), width=3, height=4, angle=5)
  236. assert str(p) == 'Ellipse(xy=(1, 2), width=3, height=4, angle=5)'
  237. p = mpatches.Rectangle(xy=(1, 2), width=3, height=4, angle=5)
  238. assert str(p) == 'Rectangle(xy=(1, 2), width=3, height=4, angle=5)'
  239. p = mpatches.Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)
  240. assert str(p) == 'Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)'
  241. p = mpatches.Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)
  242. expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)'
  243. assert str(p) == expected
  244. p = mpatches.RegularPolygon((1, 2), 20, radius=5)
  245. assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)"
  246. p = mpatches.CirclePolygon(xy=(1, 2), radius=5, resolution=20)
  247. assert str(p) == "CirclePolygon((1, 2), radius=5, resolution=20)"
  248. p = mpatches.FancyBboxPatch((1, 2), width=3, height=4)
  249. assert str(p) == "FancyBboxPatch((1, 2), width=3, height=4)"
  250. # Further nice __str__ which cannot be `eval`uated:
  251. path_data = [([1, 2], mpath.Path.MOVETO), ([2, 2], mpath.Path.LINETO),
  252. ([1, 2], mpath.Path.CLOSEPOLY)]
  253. p = mpatches.PathPatch(mpath.Path(*zip(*path_data)))
  254. assert str(p) == "PathPatch3((1, 2) ...)"
  255. data = [[1, 2], [2, 2], [1, 2]]
  256. p = mpatches.Polygon(data)
  257. assert str(p) == "Polygon3((1, 2) ...)"
  258. p = mpatches.FancyArrowPatch(path=mpath.Path(*zip(*path_data)))
  259. assert str(p)[:27] == "FancyArrowPatch(Path(array("
  260. p = mpatches.FancyArrowPatch((1, 2), (3, 4))
  261. assert str(p) == "FancyArrowPatch((1, 2)->(3, 4))"
  262. p = mpatches.ConnectionPatch((1, 2), (3, 4), 'data')
  263. assert str(p) == "ConnectionPatch((1, 2), (3, 4))"
  264. s = mpatches.Shadow(p, 1, 1)
  265. assert str(s) == "Shadow(ConnectionPatch((1, 2), (3, 4)))"
  266. # Not testing Arrow, FancyArrow here
  267. # because they seem to exist only for historical reasons.
  268. @image_comparison(['multi_color_hatch'], remove_text=True, style='default')
  269. def test_multi_color_hatch():
  270. fig, ax = plt.subplots()
  271. rects = ax.bar(range(5), range(1, 6))
  272. for i, rect in enumerate(rects):
  273. rect.set_facecolor('none')
  274. rect.set_edgecolor('C{}'.format(i))
  275. rect.set_hatch('/')
  276. ax.autoscale_view()
  277. ax.autoscale(False)
  278. for i in range(5):
  279. with mstyle.context({'hatch.color': 'C{}'.format(i)}):
  280. r = Rectangle((i - .8 / 2, 5), .8, 1, hatch='//', fc='none')
  281. ax.add_patch(r)
  282. @image_comparison(['units_rectangle.png'])
  283. def test_units_rectangle():
  284. import matplotlib.testing.jpl_units as U
  285. U.register()
  286. p = mpatches.Rectangle((5*U.km, 6*U.km), 1*U.km, 2*U.km)
  287. fig, ax = plt.subplots()
  288. ax.add_patch(p)
  289. ax.set_xlim([4*U.km, 7*U.km])
  290. ax.set_ylim([5*U.km, 9*U.km])
  291. @image_comparison(['connection_patch.png'], style='mpl20', remove_text=True)
  292. def test_connection_patch():
  293. fig, (ax1, ax2) = plt.subplots(1, 2)
  294. con = mpatches.ConnectionPatch(xyA=(0.1, 0.1), xyB=(0.9, 0.9),
  295. coordsA='data', coordsB='data',
  296. axesA=ax2, axesB=ax1,
  297. arrowstyle="->")
  298. ax2.add_artist(con)
  299. xyA = (0.6, 1.0) # in axes coordinates
  300. xyB = (0.0, 0.2) # x in axes coordinates, y in data coordinates
  301. coordsA = "axes fraction"
  302. coordsB = ax2.get_yaxis_transform()
  303. con = mpatches.ConnectionPatch(xyA=xyA, xyB=xyB, coordsA=coordsA,
  304. coordsB=coordsB, arrowstyle="-")
  305. ax2.add_artist(con)
  306. def test_connection_patch_fig():
  307. # Test that connection patch can be added as figure artist
  308. fig, (ax1, ax2) = plt.subplots(1, 2)
  309. xy = (0.3, 0.2)
  310. con = mpatches.ConnectionPatch(xyA=xy, xyB=xy,
  311. coordsA="data", coordsB="data",
  312. axesA=ax1, axesB=ax2,
  313. arrowstyle="->", shrinkB=5)
  314. fig.add_artist(con)
  315. fig.canvas.draw()
  316. def test_datetime_rectangle():
  317. # Check that creating a rectangle with timedeltas doesn't fail
  318. from datetime import datetime, timedelta
  319. start = datetime(2017, 1, 1, 0, 0, 0)
  320. delta = timedelta(seconds=16)
  321. patch = mpatches.Rectangle((start, 0), delta, 1)
  322. fig, ax = plt.subplots()
  323. ax.add_patch(patch)
  324. def test_datetime_datetime_fails():
  325. from datetime import datetime
  326. start = datetime(2017, 1, 1, 0, 0, 0)
  327. dt_delta = datetime(1970, 1, 5) # Will be 5 days if units are done wrong
  328. with pytest.raises(TypeError):
  329. mpatches.Rectangle((start, 0), dt_delta, 1)
  330. with pytest.raises(TypeError):
  331. mpatches.Rectangle((0, start), 1, dt_delta)
  332. def test_contains_point():
  333. ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0, 0)
  334. points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
  335. path = ell.get_path()
  336. transform = ell.get_transform()
  337. radius = ell._process_radius(None)
  338. expected = np.array([path.contains_point(point,
  339. transform,
  340. radius) for point in points])
  341. result = np.array([ell.contains_point(point) for point in points])
  342. assert np.all(result == expected)
  343. def test_contains_points():
  344. ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0, 0)
  345. points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
  346. path = ell.get_path()
  347. transform = ell.get_transform()
  348. radius = ell._process_radius(None)
  349. expected = path.contains_points(points, transform, radius)
  350. result = ell.contains_points(points)
  351. assert np.all(result == expected)
  352. # Currently fails with pdf/svg, probably because some parts assume a dpi of 72.
  353. @check_figures_equal(extensions=["png"])
  354. def test_shadow(fig_test, fig_ref):
  355. xy = np.array([.2, .3])
  356. dxy = np.array([.1, .2])
  357. # We need to work around the nonsensical (dpi-dependent) interpretation of
  358. # offsets by the Shadow class...
  359. plt.rcParams["savefig.dpi"] = "figure"
  360. # Test image.
  361. a1 = fig_test.subplots()
  362. rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
  363. shadow = mpatches.Shadow(rect, ox=dxy[0], oy=dxy[1])
  364. a1.add_patch(rect)
  365. a1.add_patch(shadow)
  366. # Reference image.
  367. a2 = fig_ref.subplots()
  368. rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
  369. shadow = mpatches.Rectangle(
  370. xy=xy + fig_ref.dpi / 72 * dxy, width=.5, height=.5,
  371. fc=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
  372. ec=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
  373. alpha=.5)
  374. a2.add_patch(shadow)
  375. a2.add_patch(rect)
  376. def test_fancyarrow_units():
  377. from datetime import datetime
  378. # Smoke test to check that FancyArrowPatch works with units
  379. dtime = datetime(2000, 1, 1)
  380. fig, ax = plt.subplots()
  381. arrow = FancyArrowPatch((0, dtime), (0.01, dtime))
  382. ax.add_patch(arrow)