test_marker.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from matplotlib import markers
  4. from matplotlib.path import Path
  5. from matplotlib.testing.decorators import check_figures_equal
  6. from matplotlib.transforms import Affine2D
  7. import pytest
  8. def test_marker_fillstyle():
  9. marker_style = markers.MarkerStyle(marker='o', fillstyle='none')
  10. assert marker_style.get_fillstyle() == 'none'
  11. assert not marker_style.is_filled()
  12. @pytest.mark.parametrize('marker', [
  13. 'o',
  14. 'x',
  15. '',
  16. 'None',
  17. r'$\frac{1}{2}$',
  18. "$\u266B$",
  19. 1,
  20. markers.TICKLEFT,
  21. [[-1, 0], [1, 0]],
  22. np.array([[-1, 0], [1, 0]]),
  23. Path([[0, 0], [1, 0]], [Path.MOVETO, Path.LINETO]),
  24. (5, 0), # a pentagon
  25. (7, 1), # a 7-pointed star
  26. (5, 2), # asterisk
  27. (5, 0, 10), # a pentagon, rotated by 10 degrees
  28. (7, 1, 10), # a 7-pointed star, rotated by 10 degrees
  29. (5, 2, 10), # asterisk, rotated by 10 degrees
  30. markers.MarkerStyle('o'),
  31. ])
  32. def test_markers_valid(marker):
  33. # Checking this doesn't fail.
  34. markers.MarkerStyle(marker)
  35. @pytest.mark.parametrize('marker', [
  36. 'square', # arbitrary string
  37. np.array([[-0.5, 0, 1, 2, 3]]), # 1D array
  38. (1,),
  39. (5, 3), # second parameter of tuple must be 0, 1, or 2
  40. (1, 2, 3, 4),
  41. ])
  42. def test_markers_invalid(marker):
  43. with pytest.raises(ValueError):
  44. markers.MarkerStyle(marker)
  45. class UnsnappedMarkerStyle(markers.MarkerStyle):
  46. """
  47. A MarkerStyle where the snap threshold is force-disabled.
  48. This is used to compare to polygon/star/asterisk markers which do not have
  49. any snap threshold set.
  50. """
  51. def _recache(self):
  52. super()._recache()
  53. self._snap_threshold = None
  54. @check_figures_equal()
  55. def test_poly_marker(fig_test, fig_ref):
  56. ax_test = fig_test.add_subplot()
  57. ax_ref = fig_ref.add_subplot()
  58. # Note, some reference sizes must be different because they have unit
  59. # *length*, while polygon markers are inscribed in a circle of unit
  60. # *radius*. This introduces a factor of np.sqrt(2), but since size is
  61. # squared, that becomes 2.
  62. size = 20**2
  63. # Squares
  64. ax_test.scatter([0], [0], marker=(4, 0, 45), s=size)
  65. ax_ref.scatter([0], [0], marker='s', s=size/2)
  66. # Diamonds, with and without rotation argument
  67. ax_test.scatter([1], [1], marker=(4, 0), s=size)
  68. ax_ref.scatter([1], [1], marker=UnsnappedMarkerStyle('D'), s=size/2)
  69. ax_test.scatter([1], [1.5], marker=(4, 0, 0), s=size)
  70. ax_ref.scatter([1], [1.5], marker=UnsnappedMarkerStyle('D'), s=size/2)
  71. # Pentagon, with and without rotation argument
  72. ax_test.scatter([2], [2], marker=(5, 0), s=size)
  73. ax_ref.scatter([2], [2], marker=UnsnappedMarkerStyle('p'), s=size)
  74. ax_test.scatter([2], [2.5], marker=(5, 0, 0), s=size)
  75. ax_ref.scatter([2], [2.5], marker=UnsnappedMarkerStyle('p'), s=size)
  76. # Hexagon, with and without rotation argument
  77. ax_test.scatter([3], [3], marker=(6, 0), s=size)
  78. ax_ref.scatter([3], [3], marker='h', s=size)
  79. ax_test.scatter([3], [3.5], marker=(6, 0, 0), s=size)
  80. ax_ref.scatter([3], [3.5], marker='h', s=size)
  81. # Rotated hexagon
  82. ax_test.scatter([4], [4], marker=(6, 0, 30), s=size)
  83. ax_ref.scatter([4], [4], marker='H', s=size)
  84. # Octagons
  85. ax_test.scatter([5], [5], marker=(8, 0, 22.5), s=size)
  86. ax_ref.scatter([5], [5], marker=UnsnappedMarkerStyle('8'), s=size)
  87. ax_test.set(xlim=(-0.5, 5.5), ylim=(-0.5, 5.5))
  88. ax_ref.set(xlim=(-0.5, 5.5), ylim=(-0.5, 5.5))
  89. def test_star_marker():
  90. # We don't really have a strict equivalent to this marker, so we'll just do
  91. # a smoke test.
  92. size = 20**2
  93. fig, ax = plt.subplots()
  94. ax.scatter([0], [0], marker=(5, 1), s=size)
  95. ax.scatter([1], [1], marker=(5, 1, 0), s=size)
  96. ax.set(xlim=(-0.5, 0.5), ylim=(-0.5, 1.5))
  97. # The asterisk marker is really a star with 0-size inner circle, so the ends
  98. # are corners and get a slight bevel. The reference markers are just singular
  99. # lines without corners, so they have no bevel, and we need to add a slight
  100. # tolerance.
  101. @check_figures_equal(tol=1.45)
  102. def test_asterisk_marker(fig_test, fig_ref, request):
  103. ax_test = fig_test.add_subplot()
  104. ax_ref = fig_ref.add_subplot()
  105. # Note, some reference sizes must be different because they have unit
  106. # *length*, while asterisk markers are inscribed in a circle of unit
  107. # *radius*. This introduces a factor of np.sqrt(2), but since size is
  108. # squared, that becomes 2.
  109. size = 20**2
  110. def draw_ref_marker(y, style, size):
  111. # As noted above, every line is doubled. Due to antialiasing, these
  112. # doubled lines make a slight difference in the .png results.
  113. ax_ref.scatter([y], [y], marker=UnsnappedMarkerStyle(style), s=size)
  114. if request.getfixturevalue('ext') == 'png':
  115. ax_ref.scatter([y], [y], marker=UnsnappedMarkerStyle(style),
  116. s=size)
  117. # Plus
  118. ax_test.scatter([0], [0], marker=(4, 2), s=size)
  119. draw_ref_marker(0, '+', size)
  120. ax_test.scatter([0.5], [0.5], marker=(4, 2, 0), s=size)
  121. draw_ref_marker(0.5, '+', size)
  122. # Cross
  123. ax_test.scatter([1], [1], marker=(4, 2, 45), s=size)
  124. draw_ref_marker(1, 'x', size/2)
  125. ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
  126. ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
  127. # The bullet mathtext marker is not quite a circle, so this is not a perfect match, but
  128. # it is close enough to confirm that the text-based marker is centred correctly. But we
  129. # still need a small tolerance to work around that difference.
  130. @check_figures_equal(extensions=['png'], tol=1.86)
  131. def test_text_marker(fig_ref, fig_test):
  132. ax_ref = fig_ref.add_subplot()
  133. ax_test = fig_test.add_subplot()
  134. ax_ref.plot(0, 0, marker=r'o', markersize=100, markeredgewidth=0)
  135. ax_test.plot(0, 0, marker=r'$\bullet$', markersize=100, markeredgewidth=0)
  136. @check_figures_equal()
  137. def test_marker_clipping(fig_ref, fig_test):
  138. # Plotting multiple markers can trigger different optimized paths in
  139. # backends, so compare single markers vs multiple to ensure they are
  140. # clipped correctly.
  141. marker_count = len(markers.MarkerStyle.markers)
  142. marker_size = 50
  143. ncol = 7
  144. nrow = marker_count // ncol + 1
  145. width = 2 * marker_size * ncol
  146. height = 2 * marker_size * nrow * 2
  147. fig_ref.set_size_inches((width / fig_ref.dpi, height / fig_ref.dpi))
  148. ax_ref = fig_ref.add_axes([0, 0, 1, 1])
  149. fig_test.set_size_inches((width / fig_test.dpi, height / fig_ref.dpi))
  150. ax_test = fig_test.add_axes([0, 0, 1, 1])
  151. for i, marker in enumerate(markers.MarkerStyle.markers):
  152. x = i % ncol
  153. y = i // ncol * 2
  154. # Singular markers per call.
  155. ax_ref.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3)
  156. ax_ref.plot(x, y, c='k',
  157. marker=marker, markersize=marker_size, markeredgewidth=10,
  158. fillstyle='full', markerfacecolor='white')
  159. ax_ref.plot(x, y + 1, c='k',
  160. marker=marker, markersize=marker_size, markeredgewidth=10,
  161. fillstyle='full', markerfacecolor='white')
  162. # Multiple markers in a single call.
  163. ax_test.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3,
  164. marker=marker, markersize=marker_size, markeredgewidth=10,
  165. fillstyle='full', markerfacecolor='white')
  166. ax_ref.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
  167. ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
  168. ax_ref.axis('off')
  169. ax_test.axis('off')
  170. def test_marker_init_transforms():
  171. """Test that initializing marker with transform is a simple addition."""
  172. marker = markers.MarkerStyle("o")
  173. t = Affine2D().translate(1, 1)
  174. t_marker = markers.MarkerStyle("o", transform=t)
  175. assert marker.get_transform() + t == t_marker.get_transform()
  176. def test_marker_init_joinstyle():
  177. marker = markers.MarkerStyle("*")
  178. styled_marker = markers.MarkerStyle("*", joinstyle="round")
  179. assert styled_marker.get_joinstyle() == "round"
  180. assert marker.get_joinstyle() != "round"
  181. def test_marker_init_captyle():
  182. marker = markers.MarkerStyle("*")
  183. styled_marker = markers.MarkerStyle("*", capstyle="round")
  184. assert styled_marker.get_capstyle() == "round"
  185. assert marker.get_capstyle() != "round"
  186. @pytest.mark.parametrize("marker,transform,expected", [
  187. (markers.MarkerStyle("o"), Affine2D().translate(1, 1),
  188. Affine2D().translate(1, 1)),
  189. (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
  190. Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
  191. (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)),
  192. Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
  193. (markers.MarkerStyle(
  194. markers.TICKLEFT, transform=Affine2D().translate(1, 1)),
  195. Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
  196. ])
  197. def test_marker_transformed(marker, transform, expected):
  198. new_marker = marker.transformed(transform)
  199. assert new_marker is not marker
  200. assert new_marker.get_user_transform() == expected
  201. assert marker._user_transform is not new_marker._user_transform
  202. def test_marker_rotated_invalid():
  203. marker = markers.MarkerStyle("o")
  204. with pytest.raises(ValueError):
  205. new_marker = marker.rotated()
  206. with pytest.raises(ValueError):
  207. new_marker = marker.rotated(deg=10, rad=10)
  208. @pytest.mark.parametrize("marker,deg,rad,expected", [
  209. (markers.MarkerStyle("o"), 10, None, Affine2D().rotate_deg(10)),
  210. (markers.MarkerStyle("o"), None, 0.01, Affine2D().rotate(0.01)),
  211. (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
  212. 10, None, Affine2D().translate(1, 1).rotate_deg(10)),
  213. (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
  214. None, 0.01, Affine2D().translate(1, 1).rotate(0.01)),
  215. (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)),
  216. 10, None, Affine2D().translate(1, 1).rotate_deg(10)),
  217. (markers.MarkerStyle(
  218. markers.TICKLEFT, transform=Affine2D().translate(1, 1)),
  219. 10, None, Affine2D().translate(1, 1).rotate_deg(10)),
  220. ])
  221. def test_marker_rotated(marker, deg, rad, expected):
  222. new_marker = marker.rotated(deg=deg, rad=rad)
  223. assert new_marker is not marker
  224. assert new_marker.get_user_transform() == expected
  225. assert marker._user_transform is not new_marker._user_transform
  226. def test_marker_scaled():
  227. marker = markers.MarkerStyle("1")
  228. new_marker = marker.scaled(2)
  229. assert new_marker is not marker
  230. assert new_marker.get_user_transform() == Affine2D().scale(2)
  231. assert marker._user_transform is not new_marker._user_transform
  232. new_marker = marker.scaled(2, 3)
  233. assert new_marker is not marker
  234. assert new_marker.get_user_transform() == Affine2D().scale(2, 3)
  235. assert marker._user_transform is not new_marker._user_transform
  236. marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1))
  237. new_marker = marker.scaled(2)
  238. assert new_marker is not marker
  239. expected = Affine2D().translate(1, 1).scale(2)
  240. assert new_marker.get_user_transform() == expected
  241. assert marker._user_transform is not new_marker._user_transform
  242. def test_alt_transform():
  243. m1 = markers.MarkerStyle("o", "left")
  244. m2 = markers.MarkerStyle("o", "left", Affine2D().rotate_deg(90))
  245. assert m1.get_alt_transform().rotate_deg(90) == m2.get_alt_transform()