test_widgets.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. from types import SimpleNamespace
  2. import matplotlib.widgets as widgets
  3. import matplotlib.pyplot as plt
  4. from matplotlib.testing.decorators import image_comparison
  5. from numpy.testing import assert_allclose
  6. import pytest
  7. def get_ax():
  8. fig, ax = plt.subplots(1, 1)
  9. ax.plot([0, 200], [0, 200])
  10. ax.set_aspect(1.0)
  11. ax.figure.canvas.draw()
  12. return ax
  13. def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1):
  14. """
  15. *name*
  16. the event name
  17. *canvas*
  18. the FigureCanvas instance generating the event
  19. *guiEvent*
  20. the GUI event that triggered the matplotlib event
  21. *x*
  22. x position - pixels from left of canvas
  23. *y*
  24. y position - pixels from bottom of canvas
  25. *inaxes*
  26. the :class:`~matplotlib.axes.Axes` instance if mouse is over axes
  27. *xdata*
  28. x coord of mouse in data coords
  29. *ydata*
  30. y coord of mouse in data coords
  31. *button*
  32. button pressed None, 1, 2, 3, 'up', 'down' (up and down are used
  33. for scroll events)
  34. *key*
  35. the key depressed when the mouse event triggered (see
  36. :class:`KeyEvent`)
  37. *step*
  38. number of scroll steps (positive for 'up', negative for 'down')
  39. """
  40. event = SimpleNamespace()
  41. event.button = button
  42. ax = tool.ax
  43. event.x, event.y = ax.transData.transform([(xdata, ydata),
  44. (xdata, ydata)])[00]
  45. event.xdata, event.ydata = xdata, ydata
  46. event.inaxes = ax
  47. event.canvas = ax.figure.canvas
  48. event.key = key
  49. event.step = step
  50. event.guiEvent = None
  51. event.name = 'Custom'
  52. func = getattr(tool, etype)
  53. func(event)
  54. def check_rectangle(**kwargs):
  55. ax = get_ax()
  56. def onselect(epress, erelease):
  57. ax._got_onselect = True
  58. assert epress.xdata == 100
  59. assert epress.ydata == 100
  60. assert erelease.xdata == 199
  61. assert erelease.ydata == 199
  62. tool = widgets.RectangleSelector(ax, onselect, **kwargs)
  63. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  64. do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
  65. # purposely drag outside of axis for release
  66. do_event(tool, 'release', xdata=250, ydata=250, button=1)
  67. if kwargs.get('drawtype', None) not in ['line', 'none']:
  68. assert_allclose(tool.geometry,
  69. [[100., 100, 199, 199, 100],
  70. [100, 199, 199, 100, 100]],
  71. err_msg=tool.geometry)
  72. assert ax._got_onselect
  73. def test_rectangle_selector():
  74. check_rectangle()
  75. check_rectangle(drawtype='line', useblit=False)
  76. check_rectangle(useblit=True, button=1)
  77. check_rectangle(drawtype='none', minspanx=10, minspany=10)
  78. check_rectangle(minspanx=10, minspany=10, spancoords='pixels')
  79. check_rectangle(rectprops=dict(fill=True))
  80. def test_ellipse():
  81. """For ellipse, test out the key modifiers"""
  82. ax = get_ax()
  83. def onselect(epress, erelease):
  84. pass
  85. tool = widgets.EllipseSelector(ax, onselect=onselect,
  86. maxdist=10, interactive=True)
  87. tool.extents = (100, 150, 100, 150)
  88. # drag the rectangle
  89. do_event(tool, 'press', xdata=10, ydata=10, button=1,
  90. key=' ')
  91. do_event(tool, 'onmove', xdata=30, ydata=30, button=1)
  92. do_event(tool, 'release', xdata=30, ydata=30, button=1)
  93. assert tool.extents == (120, 170, 120, 170)
  94. # create from center
  95. do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1,
  96. key='control')
  97. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  98. do_event(tool, 'onmove', xdata=125, ydata=125, button=1)
  99. do_event(tool, 'release', xdata=125, ydata=125, button=1)
  100. do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1,
  101. key='control')
  102. assert tool.extents == (75, 125, 75, 125)
  103. # create a square
  104. do_event(tool, 'on_key_press', xdata=10, ydata=10, button=1,
  105. key='shift')
  106. do_event(tool, 'press', xdata=10, ydata=10, button=1)
  107. do_event(tool, 'onmove', xdata=35, ydata=30, button=1)
  108. do_event(tool, 'release', xdata=35, ydata=30, button=1)
  109. do_event(tool, 'on_key_release', xdata=10, ydata=10, button=1,
  110. key='shift')
  111. extents = [int(e) for e in tool.extents]
  112. assert extents == [10, 35, 10, 34]
  113. # create a square from center
  114. do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1,
  115. key='ctrl+shift')
  116. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  117. do_event(tool, 'onmove', xdata=125, ydata=130, button=1)
  118. do_event(tool, 'release', xdata=125, ydata=130, button=1)
  119. do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1,
  120. key='ctrl+shift')
  121. extents = [int(e) for e in tool.extents]
  122. assert extents == [70, 129, 70, 130]
  123. assert tool.geometry.shape == (2, 73)
  124. assert_allclose(tool.geometry[:, 0], [70., 100])
  125. def test_rectangle_handles():
  126. ax = get_ax()
  127. def onselect(epress, erelease):
  128. pass
  129. tool = widgets.RectangleSelector(ax, onselect=onselect,
  130. maxdist=10, interactive=True)
  131. tool.extents = (100, 150, 100, 150)
  132. assert tool.corners == (
  133. (100, 150, 150, 100), (100, 100, 150, 150))
  134. assert tool.extents == (100, 150, 100, 150)
  135. assert tool.edge_centers == (
  136. (100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))
  137. assert tool.extents == (100, 150, 100, 150)
  138. # grab a corner and move it
  139. do_event(tool, 'press', xdata=100, ydata=100)
  140. do_event(tool, 'onmove', xdata=120, ydata=120)
  141. do_event(tool, 'release', xdata=120, ydata=120)
  142. assert tool.extents == (120, 150, 120, 150)
  143. # grab the center and move it
  144. do_event(tool, 'press', xdata=132, ydata=132)
  145. do_event(tool, 'onmove', xdata=120, ydata=120)
  146. do_event(tool, 'release', xdata=120, ydata=120)
  147. assert tool.extents == (108, 138, 108, 138)
  148. # create a new rectangle
  149. do_event(tool, 'press', xdata=10, ydata=10)
  150. do_event(tool, 'onmove', xdata=100, ydata=100)
  151. do_event(tool, 'release', xdata=100, ydata=100)
  152. assert tool.extents == (10, 100, 10, 100)
  153. def check_span(*args, **kwargs):
  154. ax = get_ax()
  155. def onselect(vmin, vmax):
  156. ax._got_onselect = True
  157. assert vmin == 100
  158. assert vmax == 150
  159. def onmove(vmin, vmax):
  160. assert vmin == 100
  161. assert vmax == 125
  162. ax._got_on_move = True
  163. if 'onmove_callback' in kwargs:
  164. kwargs['onmove_callback'] = onmove
  165. tool = widgets.SpanSelector(ax, onselect, *args, **kwargs)
  166. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  167. do_event(tool, 'onmove', xdata=125, ydata=125, button=1)
  168. do_event(tool, 'release', xdata=150, ydata=150, button=1)
  169. assert ax._got_onselect
  170. if 'onmove_callback' in kwargs:
  171. assert ax._got_on_move
  172. def test_span_selector():
  173. check_span('horizontal', minspan=10, useblit=True)
  174. check_span('vertical', onmove_callback=True, button=1)
  175. check_span('horizontal', rectprops=dict(fill=True))
  176. def check_lasso_selector(**kwargs):
  177. ax = get_ax()
  178. def onselect(verts):
  179. ax._got_onselect = True
  180. assert verts == [(100, 100), (125, 125), (150, 150)]
  181. tool = widgets.LassoSelector(ax, onselect, **kwargs)
  182. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  183. do_event(tool, 'onmove', xdata=125, ydata=125, button=1)
  184. do_event(tool, 'release', xdata=150, ydata=150, button=1)
  185. assert ax._got_onselect
  186. def test_lasso_selector():
  187. check_lasso_selector()
  188. check_lasso_selector(useblit=False, lineprops=dict(color='red'))
  189. check_lasso_selector(useblit=True, button=1)
  190. def test_CheckButtons():
  191. ax = get_ax()
  192. check = widgets.CheckButtons(ax, ('a', 'b', 'c'), (True, False, True))
  193. assert check.get_status() == [True, False, True]
  194. check.set_active(0)
  195. assert check.get_status() == [False, False, True]
  196. cid = check.on_clicked(lambda: None)
  197. check.disconnect(cid)
  198. @image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True)
  199. def test_check_radio_buttons_image():
  200. # Remove this line when this test image is regenerated.
  201. plt.rcParams['text.kerning_factor'] = 6
  202. get_ax()
  203. plt.subplots_adjust(left=0.3)
  204. rax1 = plt.axes([0.05, 0.7, 0.15, 0.15])
  205. rax2 = plt.axes([0.05, 0.2, 0.15, 0.15])
  206. widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3'))
  207. widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'),
  208. (False, True, True))
  209. @image_comparison(['check_bunch_of_radio_buttons.png'],
  210. style='mpl20', remove_text=True)
  211. def test_check_bunch_of_radio_buttons():
  212. rax = plt.axes([0.05, 0.1, 0.15, 0.7])
  213. widgets.RadioButtons(rax, ('B1', 'B2', 'B3', 'B4', 'B5', 'B6',
  214. 'B7', 'B8', 'B9', 'B10', 'B11', 'B12',
  215. 'B13', 'B14', 'B15'))
  216. def test_slider_slidermin_slidermax_invalid():
  217. fig, ax = plt.subplots()
  218. # test min/max with floats
  219. with pytest.raises(ValueError):
  220. widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  221. slidermin=10.0)
  222. with pytest.raises(ValueError):
  223. widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  224. slidermax=10.0)
  225. def test_slider_slidermin_slidermax():
  226. fig, ax = plt.subplots()
  227. slider_ = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  228. valinit=5.0)
  229. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  230. valinit=1.0, slidermin=slider_)
  231. assert slider.val == slider_.val
  232. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  233. valinit=10.0, slidermax=slider_)
  234. assert slider.val == slider_.val
  235. def test_slider_valmin_valmax():
  236. fig, ax = plt.subplots()
  237. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  238. valinit=-10.0)
  239. assert slider.val == slider.valmin
  240. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  241. valinit=25.0)
  242. assert slider.val == slider.valmax
  243. def test_slider_horizontal_vertical():
  244. fig, ax = plt.subplots()
  245. slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=24,
  246. valinit=12, orientation='horizontal')
  247. slider.set_val(10)
  248. assert slider.val == 10
  249. # check the dimension of the slider patch in axes units
  250. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  251. assert_allclose(box.bounds, [0, 0, 10/24, 1])
  252. fig, ax = plt.subplots()
  253. slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=24,
  254. valinit=12, orientation='vertical')
  255. slider.set_val(10)
  256. assert slider.val == 10
  257. # check the dimension of the slider patch in axes units
  258. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  259. assert_allclose(box.bounds, [0, 0, 1, 10/24])
  260. def check_polygon_selector(event_sequence, expected_result, selections_count):
  261. """Helper function to test Polygon Selector
  262. Parameters
  263. ----------
  264. event_sequence : list of tuples (etype, dict())
  265. A sequence of events to perform. The sequence is a list of tuples
  266. where the first element of the tuple is an etype (e.g., 'onmove',
  267. 'press', etc.), and the second element of the tuple is a dictionary of
  268. the arguments for the event (e.g., xdata=5, key='shift', etc.).
  269. expected_result : list of vertices (xdata, ydata)
  270. The list of vertices that are expected to result from the event
  271. sequence.
  272. selections_count : int
  273. Wait for the tool to call its `onselect` function `selections_count`
  274. times, before comparing the result to the `expected_result`
  275. """
  276. ax = get_ax()
  277. ax._selections_count = 0
  278. def onselect(vertices):
  279. ax._selections_count += 1
  280. ax._current_result = vertices
  281. tool = widgets.PolygonSelector(ax, onselect)
  282. for (etype, event_args) in event_sequence:
  283. do_event(tool, etype, **event_args)
  284. assert ax._selections_count == selections_count
  285. assert ax._current_result == expected_result
  286. def polygon_place_vertex(xdata, ydata):
  287. return [('onmove', dict(xdata=xdata, ydata=ydata)),
  288. ('press', dict(xdata=xdata, ydata=ydata)),
  289. ('release', dict(xdata=xdata, ydata=ydata))]
  290. def test_polygon_selector():
  291. # Simple polygon
  292. expected_result = [(50, 50), (150, 50), (50, 150)]
  293. event_sequence = (polygon_place_vertex(50, 50)
  294. + polygon_place_vertex(150, 50)
  295. + polygon_place_vertex(50, 150)
  296. + polygon_place_vertex(50, 50))
  297. check_polygon_selector(event_sequence, expected_result, 1)
  298. # Move first vertex before completing the polygon.
  299. expected_result = [(75, 50), (150, 50), (50, 150)]
  300. event_sequence = (polygon_place_vertex(50, 50)
  301. + polygon_place_vertex(150, 50)
  302. + [('on_key_press', dict(key='control')),
  303. ('onmove', dict(xdata=50, ydata=50)),
  304. ('press', dict(xdata=50, ydata=50)),
  305. ('onmove', dict(xdata=75, ydata=50)),
  306. ('release', dict(xdata=75, ydata=50)),
  307. ('on_key_release', dict(key='control'))]
  308. + polygon_place_vertex(50, 150)
  309. + polygon_place_vertex(75, 50))
  310. check_polygon_selector(event_sequence, expected_result, 1)
  311. # Move first two vertices at once before completing the polygon.
  312. expected_result = [(50, 75), (150, 75), (50, 150)]
  313. event_sequence = (polygon_place_vertex(50, 50)
  314. + polygon_place_vertex(150, 50)
  315. + [('on_key_press', dict(key='shift')),
  316. ('onmove', dict(xdata=100, ydata=100)),
  317. ('press', dict(xdata=100, ydata=100)),
  318. ('onmove', dict(xdata=100, ydata=125)),
  319. ('release', dict(xdata=100, ydata=125)),
  320. ('on_key_release', dict(key='shift'))]
  321. + polygon_place_vertex(50, 150)
  322. + polygon_place_vertex(50, 75))
  323. check_polygon_selector(event_sequence, expected_result, 1)
  324. # Move first vertex after completing the polygon.
  325. expected_result = [(75, 50), (150, 50), (50, 150)]
  326. event_sequence = (polygon_place_vertex(50, 50)
  327. + polygon_place_vertex(150, 50)
  328. + polygon_place_vertex(50, 150)
  329. + polygon_place_vertex(50, 50)
  330. + [('onmove', dict(xdata=50, ydata=50)),
  331. ('press', dict(xdata=50, ydata=50)),
  332. ('onmove', dict(xdata=75, ydata=50)),
  333. ('release', dict(xdata=75, ydata=50))])
  334. check_polygon_selector(event_sequence, expected_result, 2)
  335. # Move all vertices after completing the polygon.
  336. expected_result = [(75, 75), (175, 75), (75, 175)]
  337. event_sequence = (polygon_place_vertex(50, 50)
  338. + polygon_place_vertex(150, 50)
  339. + polygon_place_vertex(50, 150)
  340. + polygon_place_vertex(50, 50)
  341. + [('on_key_press', dict(key='shift')),
  342. ('onmove', dict(xdata=100, ydata=100)),
  343. ('press', dict(xdata=100, ydata=100)),
  344. ('onmove', dict(xdata=125, ydata=125)),
  345. ('release', dict(xdata=125, ydata=125)),
  346. ('on_key_release', dict(key='shift'))])
  347. check_polygon_selector(event_sequence, expected_result, 2)
  348. # Try to move a vertex and move all before placing any vertices.
  349. expected_result = [(50, 50), (150, 50), (50, 150)]
  350. event_sequence = ([('on_key_press', dict(key='control')),
  351. ('onmove', dict(xdata=100, ydata=100)),
  352. ('press', dict(xdata=100, ydata=100)),
  353. ('onmove', dict(xdata=125, ydata=125)),
  354. ('release', dict(xdata=125, ydata=125)),
  355. ('on_key_release', dict(key='control')),
  356. ('on_key_press', dict(key='shift')),
  357. ('onmove', dict(xdata=100, ydata=100)),
  358. ('press', dict(xdata=100, ydata=100)),
  359. ('onmove', dict(xdata=125, ydata=125)),
  360. ('release', dict(xdata=125, ydata=125)),
  361. ('on_key_release', dict(key='shift'))]
  362. + polygon_place_vertex(50, 50)
  363. + polygon_place_vertex(150, 50)
  364. + polygon_place_vertex(50, 150)
  365. + polygon_place_vertex(50, 50))
  366. check_polygon_selector(event_sequence, expected_result, 1)
  367. # Try to place vertex out-of-bounds, then reset, and start a new polygon.
  368. expected_result = [(50, 50), (150, 50), (50, 150)]
  369. event_sequence = (polygon_place_vertex(50, 50)
  370. + polygon_place_vertex(250, 50)
  371. + [('on_key_press', dict(key='escape')),
  372. ('on_key_release', dict(key='escape'))]
  373. + polygon_place_vertex(50, 50)
  374. + polygon_place_vertex(150, 50)
  375. + polygon_place_vertex(50, 150)
  376. + polygon_place_vertex(50, 50))
  377. check_polygon_selector(event_sequence, expected_result, 1)