test_patches.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  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. import matplotlib as mpl
  8. from matplotlib.patches import (Annulus, Ellipse, Patch, Polygon, Rectangle,
  9. FancyArrowPatch, FancyArrow, BoxStyle, Arc)
  10. from matplotlib.testing.decorators import image_comparison, check_figures_equal
  11. from matplotlib.transforms import Bbox
  12. import matplotlib.pyplot as plt
  13. from matplotlib import (
  14. collections as mcollections, colors as mcolors, patches as mpatches,
  15. path as mpath, transforms as mtransforms, rcParams)
  16. import sys
  17. on_win = (sys.platform == 'win32')
  18. def test_Polygon_close():
  19. #: GitHub issue #1018 identified a bug in the Polygon handling
  20. #: of the closed attribute; the path was not getting closed
  21. #: when set_xy was used to set the vertices.
  22. # open set of vertices:
  23. xy = [[0, 0], [0, 1], [1, 1]]
  24. # closed set:
  25. xyclosed = xy + [[0, 0]]
  26. # start with open path and close it:
  27. p = Polygon(xy, closed=True)
  28. assert p.get_closed()
  29. assert_array_equal(p.get_xy(), xyclosed)
  30. p.set_xy(xy)
  31. assert_array_equal(p.get_xy(), xyclosed)
  32. # start with closed path and open it:
  33. p = Polygon(xyclosed, closed=False)
  34. assert_array_equal(p.get_xy(), xy)
  35. p.set_xy(xyclosed)
  36. assert_array_equal(p.get_xy(), xy)
  37. # start with open path and leave it open:
  38. p = Polygon(xy, closed=False)
  39. assert not p.get_closed()
  40. assert_array_equal(p.get_xy(), xy)
  41. p.set_xy(xy)
  42. assert_array_equal(p.get_xy(), xy)
  43. # start with closed path and leave it closed:
  44. p = Polygon(xyclosed, closed=True)
  45. assert_array_equal(p.get_xy(), xyclosed)
  46. p.set_xy(xyclosed)
  47. assert_array_equal(p.get_xy(), xyclosed)
  48. def test_corner_center():
  49. loc = [10, 20]
  50. width = 1
  51. height = 2
  52. # Rectangle
  53. # No rotation
  54. corners = ((10, 20), (11, 20), (11, 22), (10, 22))
  55. rect = Rectangle(loc, width, height)
  56. assert_array_equal(rect.get_corners(), corners)
  57. assert_array_equal(rect.get_center(), (10.5, 21))
  58. # 90 deg rotation
  59. corners_rot = ((10, 20), (10, 21), (8, 21), (8, 20))
  60. rect.set_angle(90)
  61. assert_array_equal(rect.get_corners(), corners_rot)
  62. assert_array_equal(rect.get_center(), (9, 20.5))
  63. # Rotation not a multiple of 90 deg
  64. theta = 33
  65. t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta))
  66. corners_rot = t.transform(corners)
  67. rect.set_angle(theta)
  68. assert_almost_equal(rect.get_corners(), corners_rot)
  69. # Ellipse
  70. loc = [loc[0] + width / 2,
  71. loc[1] + height / 2]
  72. ellipse = Ellipse(loc, width, height)
  73. # No rotation
  74. assert_array_equal(ellipse.get_corners(), corners)
  75. # 90 deg rotation
  76. corners_rot = ((11.5, 20.5), (11.5, 21.5), (9.5, 21.5), (9.5, 20.5))
  77. ellipse.set_angle(90)
  78. assert_array_equal(ellipse.get_corners(), corners_rot)
  79. # Rotation shouldn't change ellipse center
  80. assert_array_equal(ellipse.get_center(), loc)
  81. # Rotation not a multiple of 90 deg
  82. theta = 33
  83. t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta))
  84. corners_rot = t.transform(corners)
  85. ellipse.set_angle(theta)
  86. assert_almost_equal(ellipse.get_corners(), corners_rot)
  87. def test_ellipse_vertices():
  88. # expect 0 for 0 ellipse width, height
  89. ellipse = Ellipse(xy=(0, 0), width=0, height=0, angle=0)
  90. assert_almost_equal(
  91. ellipse.get_vertices(),
  92. [(0.0, 0.0), (0.0, 0.0)],
  93. )
  94. assert_almost_equal(
  95. ellipse.get_co_vertices(),
  96. [(0.0, 0.0), (0.0, 0.0)],
  97. )
  98. ellipse = Ellipse(xy=(0, 0), width=2, height=1, angle=30)
  99. assert_almost_equal(
  100. ellipse.get_vertices(),
  101. [
  102. (
  103. ellipse.center[0] + ellipse.width / 4 * np.sqrt(3),
  104. ellipse.center[1] + ellipse.width / 4,
  105. ),
  106. (
  107. ellipse.center[0] - ellipse.width / 4 * np.sqrt(3),
  108. ellipse.center[1] - ellipse.width / 4,
  109. ),
  110. ],
  111. )
  112. assert_almost_equal(
  113. ellipse.get_co_vertices(),
  114. [
  115. (
  116. ellipse.center[0] - ellipse.height / 4,
  117. ellipse.center[1] + ellipse.height / 4 * np.sqrt(3),
  118. ),
  119. (
  120. ellipse.center[0] + ellipse.height / 4,
  121. ellipse.center[1] - ellipse.height / 4 * np.sqrt(3),
  122. ),
  123. ],
  124. )
  125. v1, v2 = np.array(ellipse.get_vertices())
  126. np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
  127. v1, v2 = np.array(ellipse.get_co_vertices())
  128. np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
  129. ellipse = Ellipse(xy=(2.252, -10.859), width=2.265, height=1.98, angle=68.78)
  130. v1, v2 = np.array(ellipse.get_vertices())
  131. np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
  132. v1, v2 = np.array(ellipse.get_co_vertices())
  133. np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
  134. def test_rotate_rect():
  135. loc = np.asarray([1.0, 2.0])
  136. width = 2
  137. height = 3
  138. angle = 30.0
  139. # A rotated rectangle
  140. rect1 = Rectangle(loc, width, height, angle=angle)
  141. # A non-rotated rectangle
  142. rect2 = Rectangle(loc, width, height)
  143. # Set up an explicit rotation matrix (in radians)
  144. angle_rad = np.pi * angle / 180.0
  145. rotation_matrix = np.array([[np.cos(angle_rad), -np.sin(angle_rad)],
  146. [np.sin(angle_rad), np.cos(angle_rad)]])
  147. # Translate to origin, rotate each vertex, and then translate back
  148. new_verts = np.inner(rotation_matrix, rect2.get_verts() - loc).T + loc
  149. # They should be the same
  150. assert_almost_equal(rect1.get_verts(), new_verts)
  151. @check_figures_equal(extensions=['png'])
  152. def test_rotate_rect_draw(fig_test, fig_ref):
  153. ax_test = fig_test.add_subplot()
  154. ax_ref = fig_ref.add_subplot()
  155. loc = (0, 0)
  156. width, height = (1, 1)
  157. angle = 30
  158. rect_ref = Rectangle(loc, width, height, angle=angle)
  159. ax_ref.add_patch(rect_ref)
  160. assert rect_ref.get_angle() == angle
  161. # Check that when the angle is updated after adding to an Axes, that the
  162. # patch is marked stale and redrawn in the correct location
  163. rect_test = Rectangle(loc, width, height)
  164. assert rect_test.get_angle() == 0
  165. ax_test.add_patch(rect_test)
  166. rect_test.set_angle(angle)
  167. assert rect_test.get_angle() == angle
  168. @check_figures_equal(extensions=['png'])
  169. def test_dash_offset_patch_draw(fig_test, fig_ref):
  170. ax_test = fig_test.add_subplot()
  171. ax_ref = fig_ref.add_subplot()
  172. loc = (0.1, 0.1)
  173. width, height = (0.8, 0.8)
  174. rect_ref = Rectangle(loc, width, height, linewidth=3, edgecolor='b',
  175. linestyle=(0, [6, 6]))
  176. # fill the line gaps using a linestyle (0, [0, 6, 6, 0]), which is
  177. # equivalent to (6, [6, 6]) but has 0 dash offset
  178. rect_ref2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r',
  179. linestyle=(0, [0, 6, 6, 0]))
  180. assert rect_ref.get_linestyle() == (0, [6, 6])
  181. assert rect_ref2.get_linestyle() == (0, [0, 6, 6, 0])
  182. ax_ref.add_patch(rect_ref)
  183. ax_ref.add_patch(rect_ref2)
  184. # Check that the dash offset of the rect is the same if we pass it in the
  185. # init method and if we create two rects with appropriate onoff sequence
  186. # of linestyle.
  187. rect_test = Rectangle(loc, width, height, linewidth=3, edgecolor='b',
  188. linestyle=(0, [6, 6]))
  189. rect_test2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r',
  190. linestyle=(6, [6, 6]))
  191. assert rect_test.get_linestyle() == (0, [6, 6])
  192. assert rect_test2.get_linestyle() == (6, [6, 6])
  193. ax_test.add_patch(rect_test)
  194. ax_test.add_patch(rect_test2)
  195. def test_negative_rect():
  196. # These two rectangles have the same vertices, but starting from a
  197. # different point. (We also drop the last vertex, which is a duplicate.)
  198. pos_vertices = Rectangle((-3, -2), 3, 2).get_verts()[:-1]
  199. neg_vertices = Rectangle((0, 0), -3, -2).get_verts()[:-1]
  200. assert_array_equal(np.roll(neg_vertices, 2, 0), pos_vertices)
  201. @image_comparison(['clip_to_bbox'])
  202. def test_clip_to_bbox():
  203. fig, ax = plt.subplots()
  204. ax.set_xlim([-18, 20])
  205. ax.set_ylim([-150, 100])
  206. path = mpath.Path.unit_regular_star(8).deepcopy()
  207. path.vertices *= [10, 100]
  208. path.vertices -= [5, 25]
  209. path2 = mpath.Path.unit_circle().deepcopy()
  210. path2.vertices *= [10, 100]
  211. path2.vertices += [10, -25]
  212. combined = mpath.Path.make_compound_path(path, path2)
  213. patch = mpatches.PathPatch(
  214. combined, alpha=0.5, facecolor='coral', edgecolor='none')
  215. ax.add_patch(patch)
  216. bbox = mtransforms.Bbox([[-12, -77.5], [50, -110]])
  217. result_path = combined.clip_to_bbox(bbox)
  218. result_patch = mpatches.PathPatch(
  219. result_path, alpha=0.5, facecolor='green', lw=4, edgecolor='black')
  220. ax.add_patch(result_patch)
  221. @image_comparison(['patch_alpha_coloring'], remove_text=True)
  222. def test_patch_alpha_coloring():
  223. """
  224. Test checks that the patch and collection are rendered with the specified
  225. alpha values in their facecolor and edgecolor.
  226. """
  227. star = mpath.Path.unit_regular_star(6)
  228. circle = mpath.Path.unit_circle()
  229. # concatenate the star with an internal cutout of the circle
  230. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  231. codes = np.concatenate([circle.codes, star.codes])
  232. cut_star1 = mpath.Path(verts, codes)
  233. cut_star2 = mpath.Path(verts + 1, codes)
  234. ax = plt.axes()
  235. col = mcollections.PathCollection([cut_star2],
  236. linewidth=5, linestyles='dashdot',
  237. facecolor=(1, 0, 0, 0.5),
  238. edgecolor=(0, 0, 1, 0.75))
  239. ax.add_collection(col)
  240. patch = mpatches.PathPatch(cut_star1,
  241. linewidth=5, linestyle='dashdot',
  242. facecolor=(1, 0, 0, 0.5),
  243. edgecolor=(0, 0, 1, 0.75))
  244. ax.add_patch(patch)
  245. ax.set_xlim(-1, 2)
  246. ax.set_ylim(-1, 2)
  247. @image_comparison(['patch_alpha_override'], remove_text=True)
  248. def test_patch_alpha_override():
  249. #: Test checks that specifying an alpha attribute for a patch or
  250. #: collection will override any alpha component of the facecolor
  251. #: or edgecolor.
  252. star = mpath.Path.unit_regular_star(6)
  253. circle = mpath.Path.unit_circle()
  254. # concatenate the star with an internal cutout of the circle
  255. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  256. codes = np.concatenate([circle.codes, star.codes])
  257. cut_star1 = mpath.Path(verts, codes)
  258. cut_star2 = mpath.Path(verts + 1, codes)
  259. ax = plt.axes()
  260. col = mcollections.PathCollection([cut_star2],
  261. linewidth=5, linestyles='dashdot',
  262. alpha=0.25,
  263. facecolor=(1, 0, 0, 0.5),
  264. edgecolor=(0, 0, 1, 0.75))
  265. ax.add_collection(col)
  266. patch = mpatches.PathPatch(cut_star1,
  267. linewidth=5, linestyle='dashdot',
  268. alpha=0.25,
  269. facecolor=(1, 0, 0, 0.5),
  270. edgecolor=(0, 0, 1, 0.75))
  271. ax.add_patch(patch)
  272. ax.set_xlim(-1, 2)
  273. ax.set_ylim(-1, 2)
  274. @mpl.style.context('default')
  275. def test_patch_color_none():
  276. # Make sure the alpha kwarg does not override 'none' facecolor.
  277. # Addresses issue #7478.
  278. c = plt.Circle((0, 0), 1, facecolor='none', alpha=1)
  279. assert c.get_facecolor()[0] == 0
  280. @image_comparison(['patch_custom_linestyle'], remove_text=True)
  281. def test_patch_custom_linestyle():
  282. #: A test to check that patches and collections accept custom dash
  283. #: patterns as linestyle and that they display correctly.
  284. star = mpath.Path.unit_regular_star(6)
  285. circle = mpath.Path.unit_circle()
  286. # concatenate the star with an internal cutout of the circle
  287. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  288. codes = np.concatenate([circle.codes, star.codes])
  289. cut_star1 = mpath.Path(verts, codes)
  290. cut_star2 = mpath.Path(verts + 1, codes)
  291. ax = plt.axes()
  292. col = mcollections.PathCollection(
  293. [cut_star2],
  294. linewidth=5, linestyles=[(0, (5, 7, 10, 7))],
  295. facecolor=(1, 0, 0), edgecolor=(0, 0, 1))
  296. ax.add_collection(col)
  297. patch = mpatches.PathPatch(
  298. cut_star1,
  299. linewidth=5, linestyle=(0, (5, 7, 10, 7)),
  300. facecolor=(1, 0, 0), edgecolor=(0, 0, 1))
  301. ax.add_patch(patch)
  302. ax.set_xlim(-1, 2)
  303. ax.set_ylim(-1, 2)
  304. def test_patch_linestyle_accents():
  305. #: Test if linestyle can also be specified with short mnemonics like "--"
  306. #: c.f. GitHub issue #2136
  307. star = mpath.Path.unit_regular_star(6)
  308. circle = mpath.Path.unit_circle()
  309. # concatenate the star with an internal cutout of the circle
  310. verts = np.concatenate([circle.vertices, star.vertices[::-1]])
  311. codes = np.concatenate([circle.codes, star.codes])
  312. linestyles = ["-", "--", "-.", ":",
  313. "solid", "dashed", "dashdot", "dotted"]
  314. fig, ax = plt.subplots()
  315. for i, ls in enumerate(linestyles):
  316. star = mpath.Path(verts + i, codes)
  317. patch = mpatches.PathPatch(star,
  318. linewidth=3, linestyle=ls,
  319. facecolor=(1, 0, 0),
  320. edgecolor=(0, 0, 1))
  321. ax.add_patch(patch)
  322. ax.set_xlim([-1, i + 1])
  323. ax.set_ylim([-1, i + 1])
  324. fig.canvas.draw()
  325. @check_figures_equal(extensions=['png'])
  326. def test_patch_linestyle_none(fig_test, fig_ref):
  327. circle = mpath.Path.unit_circle()
  328. ax_test = fig_test.add_subplot()
  329. ax_ref = fig_ref.add_subplot()
  330. for i, ls in enumerate(['none', 'None', ' ', '']):
  331. path = mpath.Path(circle.vertices + i, circle.codes)
  332. patch = mpatches.PathPatch(path,
  333. linewidth=3, linestyle=ls,
  334. facecolor=(1, 0, 0),
  335. edgecolor=(0, 0, 1))
  336. ax_test.add_patch(patch)
  337. patch = mpatches.PathPatch(path,
  338. linewidth=3, linestyle='-',
  339. facecolor=(1, 0, 0),
  340. edgecolor='none')
  341. ax_ref.add_patch(patch)
  342. ax_test.set_xlim([-1, i + 1])
  343. ax_test.set_ylim([-1, i + 1])
  344. ax_ref.set_xlim([-1, i + 1])
  345. ax_ref.set_ylim([-1, i + 1])
  346. def test_wedge_movement():
  347. param_dict = {'center': ((0, 0), (1, 1), 'set_center'),
  348. 'r': (5, 8, 'set_radius'),
  349. 'width': (2, 3, 'set_width'),
  350. 'theta1': (0, 30, 'set_theta1'),
  351. 'theta2': (45, 50, 'set_theta2')}
  352. init_args = {k: v[0] for k, v in param_dict.items()}
  353. w = mpatches.Wedge(**init_args)
  354. for attr, (old_v, new_v, func) in param_dict.items():
  355. assert getattr(w, attr) == old_v
  356. getattr(w, func)(new_v)
  357. assert getattr(w, attr) == new_v
  358. # png needs tol>=0.06, pdf tol>=1.617
  359. @image_comparison(['wedge_range'], remove_text=True, tol=1.65 if on_win else 0)
  360. def test_wedge_range():
  361. ax = plt.axes()
  362. t1 = 2.313869244286224
  363. args = [[52.31386924, 232.31386924],
  364. [52.313869244286224, 232.31386924428622],
  365. [t1, t1 + 180.0],
  366. [0, 360],
  367. [90, 90 + 360],
  368. [-180, 180],
  369. [0, 380],
  370. [45, 46],
  371. [46, 45]]
  372. for i, (theta1, theta2) in enumerate(args):
  373. x = i % 3
  374. y = i // 3
  375. wedge = mpatches.Wedge((x * 3, y * 3), 1, theta1, theta2,
  376. facecolor='none', edgecolor='k', lw=3)
  377. ax.add_artist(wedge)
  378. ax.set_xlim(-2, 8)
  379. ax.set_ylim(-2, 9)
  380. def test_patch_str():
  381. """
  382. Check that patches have nice and working `str` representation.
  383. Note that the logic is that `__str__` is defined such that:
  384. str(eval(str(p))) == str(p)
  385. """
  386. p = mpatches.Circle(xy=(1, 2), radius=3)
  387. assert str(p) == 'Circle(xy=(1, 2), radius=3)'
  388. p = mpatches.Ellipse(xy=(1, 2), width=3, height=4, angle=5)
  389. assert str(p) == 'Ellipse(xy=(1, 2), width=3, height=4, angle=5)'
  390. p = mpatches.Rectangle(xy=(1, 2), width=3, height=4, angle=5)
  391. assert str(p) == 'Rectangle(xy=(1, 2), width=3, height=4, angle=5)'
  392. p = mpatches.Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)
  393. assert str(p) == 'Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)'
  394. p = mpatches.Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)
  395. expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)'
  396. assert str(p) == expected
  397. p = mpatches.Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)
  398. expected = "Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)"
  399. assert str(p) == expected
  400. p = mpatches.RegularPolygon((1, 2), 20, radius=5)
  401. assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)"
  402. p = mpatches.CirclePolygon(xy=(1, 2), radius=5, resolution=20)
  403. assert str(p) == "CirclePolygon((1, 2), radius=5, resolution=20)"
  404. p = mpatches.FancyBboxPatch((1, 2), width=3, height=4)
  405. assert str(p) == "FancyBboxPatch((1, 2), width=3, height=4)"
  406. # Further nice __str__ which cannot be `eval`uated:
  407. path = mpath.Path([(1, 2), (2, 2), (1, 2)], closed=True)
  408. p = mpatches.PathPatch(path)
  409. assert str(p) == "PathPatch3((1, 2) ...)"
  410. p = mpatches.Polygon(np.empty((0, 2)))
  411. assert str(p) == "Polygon0()"
  412. data = [[1, 2], [2, 2], [1, 2]]
  413. p = mpatches.Polygon(data)
  414. assert str(p) == "Polygon3((1, 2) ...)"
  415. p = mpatches.FancyArrowPatch(path=path)
  416. assert str(p)[:27] == "FancyArrowPatch(Path(array("
  417. p = mpatches.FancyArrowPatch((1, 2), (3, 4))
  418. assert str(p) == "FancyArrowPatch((1, 2)->(3, 4))"
  419. p = mpatches.ConnectionPatch((1, 2), (3, 4), 'data')
  420. assert str(p) == "ConnectionPatch((1, 2), (3, 4))"
  421. s = mpatches.Shadow(p, 1, 1)
  422. assert str(s) == "Shadow(ConnectionPatch((1, 2), (3, 4)))"
  423. # Not testing Arrow, FancyArrow here
  424. # because they seem to exist only for historical reasons.
  425. @image_comparison(['multi_color_hatch'], remove_text=True, style='default')
  426. def test_multi_color_hatch():
  427. fig, ax = plt.subplots()
  428. rects = ax.bar(range(5), range(1, 6))
  429. for i, rect in enumerate(rects):
  430. rect.set_facecolor('none')
  431. rect.set_edgecolor(f'C{i}')
  432. rect.set_hatch('/')
  433. ax.autoscale_view()
  434. ax.autoscale(False)
  435. for i in range(5):
  436. with mpl.style.context({'hatch.color': f'C{i}'}):
  437. r = Rectangle((i - .8 / 2, 5), .8, 1, hatch='//', fc='none')
  438. ax.add_patch(r)
  439. @image_comparison(['units_rectangle.png'])
  440. def test_units_rectangle():
  441. import matplotlib.testing.jpl_units as U
  442. U.register()
  443. p = mpatches.Rectangle((5*U.km, 6*U.km), 1*U.km, 2*U.km)
  444. fig, ax = plt.subplots()
  445. ax.add_patch(p)
  446. ax.set_xlim([4*U.km, 7*U.km])
  447. ax.set_ylim([5*U.km, 9*U.km])
  448. @image_comparison(['connection_patch.png'], style='mpl20', remove_text=True)
  449. def test_connection_patch():
  450. fig, (ax1, ax2) = plt.subplots(1, 2)
  451. con = mpatches.ConnectionPatch(xyA=(0.1, 0.1), xyB=(0.9, 0.9),
  452. coordsA='data', coordsB='data',
  453. axesA=ax2, axesB=ax1,
  454. arrowstyle="->")
  455. ax2.add_artist(con)
  456. xyA = (0.6, 1.0) # in axes coordinates
  457. xyB = (0.0, 0.2) # x in axes coordinates, y in data coordinates
  458. coordsA = "axes fraction"
  459. coordsB = ax2.get_yaxis_transform()
  460. con = mpatches.ConnectionPatch(xyA=xyA, xyB=xyB, coordsA=coordsA,
  461. coordsB=coordsB, arrowstyle="-")
  462. ax2.add_artist(con)
  463. @check_figures_equal(extensions=["png"])
  464. def test_connection_patch_fig(fig_test, fig_ref):
  465. # Test that connection patch can be added as figure artist, and that figure
  466. # pixels count negative values from the top right corner (this API may be
  467. # changed in the future).
  468. ax1, ax2 = fig_test.subplots(1, 2)
  469. con = mpatches.ConnectionPatch(
  470. xyA=(.3, .2), coordsA="data", axesA=ax1,
  471. xyB=(-30, -20), coordsB="figure pixels",
  472. arrowstyle="->", shrinkB=5)
  473. fig_test.add_artist(con)
  474. ax1, ax2 = fig_ref.subplots(1, 2)
  475. bb = fig_ref.bbox
  476. # Necessary so that pixel counts match on both sides.
  477. plt.rcParams["savefig.dpi"] = plt.rcParams["figure.dpi"]
  478. con = mpatches.ConnectionPatch(
  479. xyA=(.3, .2), coordsA="data", axesA=ax1,
  480. xyB=(bb.width - 30, bb.height - 20), coordsB="figure pixels",
  481. arrowstyle="->", shrinkB=5)
  482. fig_ref.add_artist(con)
  483. def test_datetime_rectangle():
  484. # Check that creating a rectangle with timedeltas doesn't fail
  485. from datetime import datetime, timedelta
  486. start = datetime(2017, 1, 1, 0, 0, 0)
  487. delta = timedelta(seconds=16)
  488. patch = mpatches.Rectangle((start, 0), delta, 1)
  489. fig, ax = plt.subplots()
  490. ax.add_patch(patch)
  491. def test_datetime_datetime_fails():
  492. from datetime import datetime
  493. start = datetime(2017, 1, 1, 0, 0, 0)
  494. dt_delta = datetime(1970, 1, 5) # Will be 5 days if units are done wrong.
  495. with pytest.raises(TypeError):
  496. mpatches.Rectangle((start, 0), dt_delta, 1)
  497. with pytest.raises(TypeError):
  498. mpatches.Rectangle((0, start), 1, dt_delta)
  499. def test_contains_point():
  500. ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0)
  501. points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
  502. path = ell.get_path()
  503. transform = ell.get_transform()
  504. radius = ell._process_radius(None)
  505. expected = np.array([path.contains_point(point,
  506. transform,
  507. radius) for point in points])
  508. result = np.array([ell.contains_point(point) for point in points])
  509. assert np.all(result == expected)
  510. def test_contains_points():
  511. ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0)
  512. points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
  513. path = ell.get_path()
  514. transform = ell.get_transform()
  515. radius = ell._process_radius(None)
  516. expected = path.contains_points(points, transform, radius)
  517. result = ell.contains_points(points)
  518. assert np.all(result == expected)
  519. # Currently fails with pdf/svg, probably because some parts assume a dpi of 72.
  520. @check_figures_equal(extensions=["png"])
  521. def test_shadow(fig_test, fig_ref):
  522. xy = np.array([.2, .3])
  523. dxy = np.array([.1, .2])
  524. # We need to work around the nonsensical (dpi-dependent) interpretation of
  525. # offsets by the Shadow class...
  526. plt.rcParams["savefig.dpi"] = "figure"
  527. # Test image.
  528. a1 = fig_test.subplots()
  529. rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
  530. shadow = mpatches.Shadow(rect, ox=dxy[0], oy=dxy[1])
  531. a1.add_patch(rect)
  532. a1.add_patch(shadow)
  533. # Reference image.
  534. a2 = fig_ref.subplots()
  535. rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
  536. shadow = mpatches.Rectangle(
  537. xy=xy + fig_ref.dpi / 72 * dxy, width=.5, height=.5,
  538. fc=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
  539. ec=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
  540. alpha=.5)
  541. a2.add_patch(shadow)
  542. a2.add_patch(rect)
  543. def test_fancyarrow_units():
  544. from datetime import datetime
  545. # Smoke test to check that FancyArrowPatch works with units
  546. dtime = datetime(2000, 1, 1)
  547. fig, ax = plt.subplots()
  548. arrow = FancyArrowPatch((0, dtime), (0.01, dtime))
  549. def test_fancyarrow_setdata():
  550. fig, ax = plt.subplots()
  551. arrow = ax.arrow(0, 0, 10, 10, head_length=5, head_width=1, width=.5)
  552. expected1 = np.array(
  553. [[13.54, 13.54],
  554. [10.35, 9.65],
  555. [10.18, 9.82],
  556. [0.18, -0.18],
  557. [-0.18, 0.18],
  558. [9.82, 10.18],
  559. [9.65, 10.35],
  560. [13.54, 13.54]]
  561. )
  562. assert np.allclose(expected1, np.round(arrow.verts, 2))
  563. expected2 = np.array(
  564. [[16.71, 16.71],
  565. [16.71, 15.29],
  566. [16.71, 15.29],
  567. [1.71, 0.29],
  568. [0.29, 1.71],
  569. [15.29, 16.71],
  570. [15.29, 16.71],
  571. [16.71, 16.71]]
  572. )
  573. arrow.set_data(
  574. x=1, y=1, dx=15, dy=15, width=2, head_width=2, head_length=1
  575. )
  576. assert np.allclose(expected2, np.round(arrow.verts, 2))
  577. @image_comparison(["large_arc.svg"], style="mpl20")
  578. def test_large_arc():
  579. fig, (ax1, ax2) = plt.subplots(1, 2)
  580. x = 210
  581. y = -2115
  582. diameter = 4261
  583. for ax in [ax1, ax2]:
  584. a = Arc((x, y), diameter, diameter, lw=2, color='k')
  585. ax.add_patch(a)
  586. ax.set_axis_off()
  587. ax.set_aspect('equal')
  588. # force the high accuracy case
  589. ax1.set_xlim(7, 8)
  590. ax1.set_ylim(5, 6)
  591. # force the low accuracy case
  592. ax2.set_xlim(-25000, 18000)
  593. ax2.set_ylim(-20000, 6600)
  594. @image_comparison(["all_quadrants_arcs.svg"], style="mpl20")
  595. def test_rotated_arcs():
  596. fig, ax_arr = plt.subplots(2, 2, squeeze=False, figsize=(10, 10))
  597. scale = 10_000_000
  598. diag_centers = ((-1, -1), (-1, 1), (1, 1), (1, -1))
  599. on_axis_centers = ((0, 1), (1, 0), (0, -1), (-1, 0))
  600. skews = ((2, 2), (2, 1/10), (2, 1/100), (2, 1/1000))
  601. for ax, (sx, sy) in zip(ax_arr.ravel(), skews):
  602. k = 0
  603. for prescale, centers in zip((1 - .0001, (1 - .0001) / np.sqrt(2)),
  604. (on_axis_centers, diag_centers)):
  605. for j, (x_sign, y_sign) in enumerate(centers, start=k):
  606. a = Arc(
  607. (x_sign * scale * prescale,
  608. y_sign * scale * prescale),
  609. scale * sx,
  610. scale * sy,
  611. lw=4,
  612. color=f"C{j}",
  613. zorder=1 + j,
  614. angle=np.rad2deg(np.arctan2(y_sign, x_sign)) % 360,
  615. label=f'big {j}',
  616. gid=f'big {j}'
  617. )
  618. ax.add_patch(a)
  619. k = j+1
  620. ax.set_xlim(-scale / 4000, scale / 4000)
  621. ax.set_ylim(-scale / 4000, scale / 4000)
  622. ax.axhline(0, color="k")
  623. ax.axvline(0, color="k")
  624. ax.set_axis_off()
  625. ax.set_aspect("equal")
  626. def test_fancyarrow_shape_error():
  627. with pytest.raises(ValueError, match="Got unknown shape: 'foo'"):
  628. FancyArrow(0, 0, 0.2, 0.2, shape='foo')
  629. @pytest.mark.parametrize('fmt, match', (
  630. ("foo", "Unknown style: 'foo'"),
  631. ("Round,foo", "Incorrect style argument: 'Round,foo'"),
  632. ))
  633. def test_boxstyle_errors(fmt, match):
  634. with pytest.raises(ValueError, match=match):
  635. BoxStyle(fmt)
  636. @image_comparison(baseline_images=['annulus'], extensions=['png'])
  637. def test_annulus():
  638. fig, ax = plt.subplots()
  639. cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
  640. ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
  641. fc='m', ec='b', alpha=0.5, hatch='xxx')
  642. ax.add_patch(cir)
  643. ax.add_patch(ell)
  644. ax.set_aspect('equal')
  645. @image_comparison(baseline_images=['annulus'], extensions=['png'])
  646. def test_annulus_setters():
  647. fig, ax = plt.subplots()
  648. cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
  649. ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
  650. fc='m', ec='b', alpha=0.5, hatch='xxx')
  651. ax.add_patch(cir)
  652. ax.add_patch(ell)
  653. ax.set_aspect('equal')
  654. cir.center = (0.5, 0.5)
  655. cir.radii = 0.2
  656. cir.width = 0.05
  657. ell.center = (0.5, 0.5)
  658. ell.radii = (0.5, 0.3)
  659. ell.width = 0.1
  660. ell.angle = 45
  661. @image_comparison(baseline_images=['annulus'], extensions=['png'])
  662. def test_annulus_setters2():
  663. fig, ax = plt.subplots()
  664. cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
  665. ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
  666. fc='m', ec='b', alpha=0.5, hatch='xxx')
  667. ax.add_patch(cir)
  668. ax.add_patch(ell)
  669. ax.set_aspect('equal')
  670. cir.center = (0.5, 0.5)
  671. cir.set_semimajor(0.2)
  672. cir.set_semiminor(0.2)
  673. assert cir.radii == (0.2, 0.2)
  674. cir.width = 0.05
  675. ell.center = (0.5, 0.5)
  676. ell.set_semimajor(0.5)
  677. ell.set_semiminor(0.3)
  678. assert ell.radii == (0.5, 0.3)
  679. ell.width = 0.1
  680. ell.angle = 45
  681. def test_degenerate_polygon():
  682. point = [0, 0]
  683. correct_extents = Bbox([point, point]).extents
  684. assert np.all(Polygon([point]).get_extents().extents == correct_extents)
  685. @pytest.mark.parametrize('kwarg', ('edgecolor', 'facecolor'))
  686. def test_color_override_warning(kwarg):
  687. with pytest.warns(UserWarning,
  688. match="Setting the 'color' property will override "
  689. "the edgecolor or facecolor properties."):
  690. Patch(color='black', **{kwarg: 'black'})
  691. def test_empty_verts():
  692. poly = Polygon(np.zeros((0, 2)))
  693. assert poly.get_verts() == []
  694. def test_default_antialiased():
  695. patch = Patch()
  696. patch.set_antialiased(not rcParams['patch.antialiased'])
  697. assert patch.get_antialiased() == (not rcParams['patch.antialiased'])
  698. # Check that None resets the state
  699. patch.set_antialiased(None)
  700. assert patch.get_antialiased() == rcParams['patch.antialiased']
  701. def test_default_linestyle():
  702. patch = Patch()
  703. patch.set_linestyle('--')
  704. patch.set_linestyle(None)
  705. assert patch.get_linestyle() == 'solid'
  706. def test_default_capstyle():
  707. patch = Patch()
  708. assert patch.get_capstyle() == 'butt'
  709. def test_default_joinstyle():
  710. patch = Patch()
  711. assert patch.get_joinstyle() == 'miter'
  712. @image_comparison(["autoscale_arc"], extensions=['png', 'svg'],
  713. style="mpl20", remove_text=True)
  714. def test_autoscale_arc():
  715. fig, axs = plt.subplots(1, 3, figsize=(4, 1))
  716. arc_lists = (
  717. [Arc((0, 0), 1, 1, theta1=0, theta2=90)],
  718. [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20)],
  719. [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20),
  720. Arc((0.5, 0.5), 2.5, 0.5, theta1=110, theta2=120),
  721. Arc((0.5, 0.5), 3.5, 0.5, theta1=210, theta2=220),
  722. Arc((0.5, 0.5), 4.5, 0.5, theta1=310, theta2=320)])
  723. for ax, arcs in zip(axs, arc_lists):
  724. for arc in arcs:
  725. ax.add_patch(arc)
  726. ax.autoscale()
  727. @check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps'])
  728. def test_arc_in_collection(fig_test, fig_ref):
  729. arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
  730. arc2 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
  731. col = mcollections.PatchCollection(patches=[arc2], facecolors='none',
  732. edgecolors='k')
  733. fig_ref.subplots().add_patch(arc1)
  734. fig_test.subplots().add_collection(col)
  735. @check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps'])
  736. def test_modifying_arc(fig_test, fig_ref):
  737. arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
  738. arc2 = Arc([.5, .5], 1.5, 1, theta1=0, theta2=60, angle=10)
  739. fig_ref.subplots().add_patch(arc1)
  740. fig_test.subplots().add_patch(arc2)
  741. arc2.set_width(.5)
  742. arc2.set_angle(20)