test_patches.py 33 KB

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