test_widgets.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759
  1. import functools
  2. import io
  3. import operator
  4. from unittest import mock
  5. from matplotlib.backend_bases import MouseEvent
  6. import matplotlib.colors as mcolors
  7. import matplotlib.widgets as widgets
  8. import matplotlib.pyplot as plt
  9. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  10. from matplotlib.testing.widgets import (click_and_drag, do_event, get_ax,
  11. mock_event, noop)
  12. import numpy as np
  13. from numpy.testing import assert_allclose
  14. import pytest
  15. @pytest.fixture
  16. def ax():
  17. return get_ax()
  18. def test_save_blitted_widget_as_pdf():
  19. from matplotlib.widgets import CheckButtons, RadioButtons
  20. from matplotlib.cbook import _get_running_interactive_framework
  21. if _get_running_interactive_framework() not in ['headless', None]:
  22. pytest.xfail("Callback exceptions are not raised otherwise.")
  23. fig, ax = plt.subplots(
  24. nrows=2, ncols=2, figsize=(5, 2), width_ratios=[1, 2]
  25. )
  26. default_rb = RadioButtons(ax[0, 0], ['Apples', 'Oranges'])
  27. styled_rb = RadioButtons(
  28. ax[0, 1], ['Apples', 'Oranges'],
  29. label_props={'color': ['red', 'orange'],
  30. 'fontsize': [16, 20]},
  31. radio_props={'edgecolor': ['red', 'orange'],
  32. 'facecolor': ['mistyrose', 'peachpuff']}
  33. )
  34. default_cb = CheckButtons(ax[1, 0], ['Apples', 'Oranges'],
  35. actives=[True, True])
  36. styled_cb = CheckButtons(
  37. ax[1, 1], ['Apples', 'Oranges'],
  38. actives=[True, True],
  39. label_props={'color': ['red', 'orange'],
  40. 'fontsize': [16, 20]},
  41. frame_props={'edgecolor': ['red', 'orange'],
  42. 'facecolor': ['mistyrose', 'peachpuff']},
  43. check_props={'color': ['darkred', 'darkorange']}
  44. )
  45. ax[0, 0].set_title('Default')
  46. ax[0, 1].set_title('Stylized')
  47. # force an Agg render
  48. fig.canvas.draw()
  49. # force a pdf save
  50. with io.BytesIO() as result_after:
  51. fig.savefig(result_after, format='pdf')
  52. @pytest.mark.parametrize('kwargs', [
  53. dict(),
  54. dict(useblit=True, button=1),
  55. dict(minspanx=10, minspany=10, spancoords='pixels'),
  56. dict(props=dict(fill=True)),
  57. ])
  58. def test_rectangle_selector(ax, kwargs):
  59. onselect = mock.Mock(spec=noop, return_value=None)
  60. tool = widgets.RectangleSelector(ax, onselect=onselect, **kwargs)
  61. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  62. do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
  63. # purposely drag outside of axis for release
  64. do_event(tool, 'release', xdata=250, ydata=250, button=1)
  65. if kwargs.get('drawtype', None) not in ['line', 'none']:
  66. assert_allclose(tool.geometry,
  67. [[100., 100, 199, 199, 100],
  68. [100, 199, 199, 100, 100]],
  69. err_msg=tool.geometry)
  70. onselect.assert_called_once()
  71. (epress, erelease), kwargs = onselect.call_args
  72. assert epress.xdata == 100
  73. assert epress.ydata == 100
  74. assert erelease.xdata == 199
  75. assert erelease.ydata == 199
  76. assert kwargs == {}
  77. @pytest.mark.parametrize('spancoords', ['data', 'pixels'])
  78. @pytest.mark.parametrize('minspanx, x1', [[0, 10], [1, 10.5], [1, 11]])
  79. @pytest.mark.parametrize('minspany, y1', [[0, 10], [1, 10.5], [1, 11]])
  80. def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1):
  81. onselect = mock.Mock(spec=noop, return_value=None)
  82. x0, y0 = (10, 10)
  83. if spancoords == 'pixels':
  84. minspanx, minspany = (ax.transData.transform((x1, y1)) -
  85. ax.transData.transform((x0, y0)))
  86. tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=True,
  87. spancoords=spancoords,
  88. minspanx=minspanx, minspany=minspany)
  89. # Too small to create a selector
  90. click_and_drag(tool, start=(x0, x1), end=(y0, y1))
  91. assert not tool._selection_completed
  92. onselect.assert_not_called()
  93. click_and_drag(tool, start=(20, 20), end=(30, 30))
  94. assert tool._selection_completed
  95. onselect.assert_called_once()
  96. # Too small to create a selector. Should clear existing selector, and
  97. # trigger onselect because there was a preexisting selector
  98. onselect.reset_mock()
  99. click_and_drag(tool, start=(x0, y0), end=(x1, y1))
  100. assert not tool._selection_completed
  101. onselect.assert_called_once()
  102. (epress, erelease), kwargs = onselect.call_args
  103. assert epress.xdata == x0
  104. assert epress.ydata == y0
  105. assert erelease.xdata == x1
  106. assert erelease.ydata == y1
  107. assert kwargs == {}
  108. @pytest.mark.parametrize('drag_from_anywhere, new_center',
  109. [[True, (60, 75)],
  110. [False, (30, 20)]])
  111. def test_rectangle_drag(ax, drag_from_anywhere, new_center):
  112. tool = widgets.RectangleSelector(ax, interactive=True,
  113. drag_from_anywhere=drag_from_anywhere)
  114. # Create rectangle
  115. click_and_drag(tool, start=(0, 10), end=(100, 120))
  116. assert tool.center == (50, 65)
  117. # Drag inside rectangle, but away from centre handle
  118. #
  119. # If drag_from_anywhere == True, this will move the rectangle by (10, 10),
  120. # giving it a new center of (60, 75)
  121. #
  122. # If drag_from_anywhere == False, this will create a new rectangle with
  123. # center (30, 20)
  124. click_and_drag(tool, start=(25, 15), end=(35, 25))
  125. assert tool.center == new_center
  126. # Check that in both cases, dragging outside the rectangle draws a new
  127. # rectangle
  128. click_and_drag(tool, start=(175, 185), end=(185, 195))
  129. assert tool.center == (180, 190)
  130. def test_rectangle_selector_set_props_handle_props(ax):
  131. tool = widgets.RectangleSelector(ax, interactive=True,
  132. props=dict(facecolor='b', alpha=0.2),
  133. handle_props=dict(alpha=0.5))
  134. # Create rectangle
  135. click_and_drag(tool, start=(0, 10), end=(100, 120))
  136. artist = tool._selection_artist
  137. assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2)
  138. tool.set_props(facecolor='r', alpha=0.3)
  139. assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3)
  140. for artist in tool._handles_artists:
  141. assert artist.get_markeredgecolor() == 'black'
  142. assert artist.get_alpha() == 0.5
  143. tool.set_handle_props(markeredgecolor='r', alpha=0.3)
  144. for artist in tool._handles_artists:
  145. assert artist.get_markeredgecolor() == 'r'
  146. assert artist.get_alpha() == 0.3
  147. def test_rectangle_resize(ax):
  148. tool = widgets.RectangleSelector(ax, interactive=True)
  149. # Create rectangle
  150. click_and_drag(tool, start=(0, 10), end=(100, 120))
  151. assert tool.extents == (0.0, 100.0, 10.0, 120.0)
  152. # resize NE handle
  153. extents = tool.extents
  154. xdata, ydata = extents[1], extents[3]
  155. xdata_new, ydata_new = xdata + 10, ydata + 5
  156. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  157. assert tool.extents == (extents[0], xdata_new, extents[2], ydata_new)
  158. # resize E handle
  159. extents = tool.extents
  160. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  161. xdata_new, ydata_new = xdata + 10, ydata
  162. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  163. assert tool.extents == (extents[0], xdata_new, extents[2], extents[3])
  164. # resize W handle
  165. extents = tool.extents
  166. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  167. xdata_new, ydata_new = xdata + 15, ydata
  168. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  169. assert tool.extents == (xdata_new, extents[1], extents[2], extents[3])
  170. # resize SW handle
  171. extents = tool.extents
  172. xdata, ydata = extents[0], extents[2]
  173. xdata_new, ydata_new = xdata + 20, ydata + 25
  174. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  175. assert tool.extents == (xdata_new, extents[1], ydata_new, extents[3])
  176. def test_rectangle_add_state(ax):
  177. tool = widgets.RectangleSelector(ax, interactive=True)
  178. # Create rectangle
  179. click_and_drag(tool, start=(70, 65), end=(125, 130))
  180. with pytest.raises(ValueError):
  181. tool.add_state('unsupported_state')
  182. with pytest.raises(ValueError):
  183. tool.add_state('clear')
  184. tool.add_state('move')
  185. tool.add_state('square')
  186. tool.add_state('center')
  187. @pytest.mark.parametrize('add_state', [True, False])
  188. def test_rectangle_resize_center(ax, add_state):
  189. tool = widgets.RectangleSelector(ax, interactive=True)
  190. # Create rectangle
  191. click_and_drag(tool, start=(70, 65), end=(125, 130))
  192. assert tool.extents == (70.0, 125.0, 65.0, 130.0)
  193. if add_state:
  194. tool.add_state('center')
  195. use_key = None
  196. else:
  197. use_key = 'control'
  198. # resize NE handle
  199. extents = tool.extents
  200. xdata, ydata = extents[1], extents[3]
  201. xdiff, ydiff = 10, 5
  202. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  203. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  204. key=use_key)
  205. assert tool.extents == (extents[0] - xdiff, xdata_new,
  206. extents[2] - ydiff, ydata_new)
  207. # resize E handle
  208. extents = tool.extents
  209. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  210. xdiff = 10
  211. xdata_new, ydata_new = xdata + xdiff, ydata
  212. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  213. key=use_key)
  214. assert tool.extents == (extents[0] - xdiff, xdata_new,
  215. extents[2], extents[3])
  216. # resize E handle negative diff
  217. extents = tool.extents
  218. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  219. xdiff = -20
  220. xdata_new, ydata_new = xdata + xdiff, ydata
  221. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  222. key=use_key)
  223. assert tool.extents == (extents[0] - xdiff, xdata_new,
  224. extents[2], extents[3])
  225. # resize W handle
  226. extents = tool.extents
  227. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  228. xdiff = 15
  229. xdata_new, ydata_new = xdata + xdiff, ydata
  230. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  231. key=use_key)
  232. assert tool.extents == (xdata_new, extents[1] - xdiff,
  233. extents[2], extents[3])
  234. # resize W handle negative diff
  235. extents = tool.extents
  236. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  237. xdiff = -25
  238. xdata_new, ydata_new = xdata + xdiff, ydata
  239. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  240. key=use_key)
  241. assert tool.extents == (xdata_new, extents[1] - xdiff,
  242. extents[2], extents[3])
  243. # resize SW handle
  244. extents = tool.extents
  245. xdata, ydata = extents[0], extents[2]
  246. xdiff, ydiff = 20, 25
  247. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  248. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  249. key=use_key)
  250. assert tool.extents == (xdata_new, extents[1] - xdiff,
  251. ydata_new, extents[3] - ydiff)
  252. @pytest.mark.parametrize('add_state', [True, False])
  253. def test_rectangle_resize_square(ax, add_state):
  254. tool = widgets.RectangleSelector(ax, interactive=True)
  255. # Create rectangle
  256. click_and_drag(tool, start=(70, 65), end=(120, 115))
  257. assert tool.extents == (70.0, 120.0, 65.0, 115.0)
  258. if add_state:
  259. tool.add_state('square')
  260. use_key = None
  261. else:
  262. use_key = 'shift'
  263. # resize NE handle
  264. extents = tool.extents
  265. xdata, ydata = extents[1], extents[3]
  266. xdiff, ydiff = 10, 5
  267. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  268. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  269. key=use_key)
  270. assert tool.extents == (extents[0], xdata_new,
  271. extents[2], extents[3] + xdiff)
  272. # resize E handle
  273. extents = tool.extents
  274. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  275. xdiff = 10
  276. xdata_new, ydata_new = xdata + xdiff, ydata
  277. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  278. key=use_key)
  279. assert tool.extents == (extents[0], xdata_new,
  280. extents[2], extents[3] + xdiff)
  281. # resize E handle negative diff
  282. extents = tool.extents
  283. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  284. xdiff = -20
  285. xdata_new, ydata_new = xdata + xdiff, ydata
  286. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  287. key=use_key)
  288. assert tool.extents == (extents[0], xdata_new,
  289. extents[2], extents[3] + xdiff)
  290. # resize W handle
  291. extents = tool.extents
  292. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  293. xdiff = 15
  294. xdata_new, ydata_new = xdata + xdiff, ydata
  295. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  296. key=use_key)
  297. assert tool.extents == (xdata_new, extents[1],
  298. extents[2], extents[3] - xdiff)
  299. # resize W handle negative diff
  300. extents = tool.extents
  301. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  302. xdiff = -25
  303. xdata_new, ydata_new = xdata + xdiff, ydata
  304. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  305. key=use_key)
  306. assert tool.extents == (xdata_new, extents[1],
  307. extents[2], extents[3] - xdiff)
  308. # resize SW handle
  309. extents = tool.extents
  310. xdata, ydata = extents[0], extents[2]
  311. xdiff, ydiff = 20, 25
  312. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  313. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
  314. key=use_key)
  315. assert tool.extents == (extents[0] + ydiff, extents[1],
  316. ydata_new, extents[3])
  317. def test_rectangle_resize_square_center(ax):
  318. tool = widgets.RectangleSelector(ax, interactive=True)
  319. # Create rectangle
  320. click_and_drag(tool, start=(70, 65), end=(120, 115))
  321. tool.add_state('square')
  322. tool.add_state('center')
  323. assert_allclose(tool.extents, (70.0, 120.0, 65.0, 115.0))
  324. # resize NE handle
  325. extents = tool.extents
  326. xdata, ydata = extents[1], extents[3]
  327. xdiff, ydiff = 10, 5
  328. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  329. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  330. assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
  331. extents[2] - xdiff, extents[3] + xdiff))
  332. # resize E handle
  333. extents = tool.extents
  334. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  335. xdiff = 10
  336. xdata_new, ydata_new = xdata + xdiff, ydata
  337. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  338. assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
  339. extents[2] - xdiff, extents[3] + xdiff))
  340. # resize E handle negative diff
  341. extents = tool.extents
  342. xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2
  343. xdiff = -20
  344. xdata_new, ydata_new = xdata + xdiff, ydata
  345. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  346. assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new,
  347. extents[2] - xdiff, extents[3] + xdiff))
  348. # resize W handle
  349. extents = tool.extents
  350. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  351. xdiff = 5
  352. xdata_new, ydata_new = xdata + xdiff, ydata
  353. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  354. assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff,
  355. extents[2] + xdiff, extents[3] - xdiff))
  356. # resize W handle negative diff
  357. extents = tool.extents
  358. xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2
  359. xdiff = -25
  360. xdata_new, ydata_new = xdata + xdiff, ydata
  361. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  362. assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff,
  363. extents[2] + xdiff, extents[3] - xdiff))
  364. # resize SW handle
  365. extents = tool.extents
  366. xdata, ydata = extents[0], extents[2]
  367. xdiff, ydiff = 20, 25
  368. xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
  369. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  370. assert_allclose(tool.extents, (extents[0] + ydiff, extents[1] - ydiff,
  371. ydata_new, extents[3] - ydiff))
  372. @pytest.mark.parametrize('selector_class',
  373. [widgets.RectangleSelector, widgets.EllipseSelector])
  374. def test_rectangle_rotate(ax, selector_class):
  375. tool = selector_class(ax, interactive=True)
  376. # Draw rectangle
  377. click_and_drag(tool, start=(100, 100), end=(130, 140))
  378. assert tool.extents == (100, 130, 100, 140)
  379. assert len(tool._state) == 0
  380. # Rotate anticlockwise using top-right corner
  381. do_event(tool, 'on_key_press', key='r')
  382. assert tool._state == {'rotate'}
  383. assert len(tool._state) == 1
  384. click_and_drag(tool, start=(130, 140), end=(120, 145))
  385. do_event(tool, 'on_key_press', key='r')
  386. assert len(tool._state) == 0
  387. # Extents shouldn't change (as shape of rectangle hasn't changed)
  388. assert tool.extents == (100, 130, 100, 140)
  389. assert_allclose(tool.rotation, 25.56, atol=0.01)
  390. tool.rotation = 45
  391. assert tool.rotation == 45
  392. # Corners should move
  393. assert_allclose(tool.corners,
  394. np.array([[118.53, 139.75, 111.46, 90.25],
  395. [95.25, 116.46, 144.75, 123.54]]), atol=0.01)
  396. # Scale using top-right corner
  397. click_and_drag(tool, start=(110, 145), end=(110, 160))
  398. assert_allclose(tool.extents, (100, 139.75, 100, 151.82), atol=0.01)
  399. if selector_class == widgets.RectangleSelector:
  400. with pytest.raises(ValueError):
  401. tool._selection_artist.rotation_point = 'unvalid_value'
  402. def test_rectangle_add_remove_set(ax):
  403. tool = widgets.RectangleSelector(ax, interactive=True)
  404. # Draw rectangle
  405. click_and_drag(tool, start=(100, 100), end=(130, 140))
  406. assert tool.extents == (100, 130, 100, 140)
  407. assert len(tool._state) == 0
  408. for state in ['rotate', 'square', 'center']:
  409. tool.add_state(state)
  410. assert len(tool._state) == 1
  411. tool.remove_state(state)
  412. assert len(tool._state) == 0
  413. @pytest.mark.parametrize('use_data_coordinates', [False, True])
  414. def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates):
  415. ax.set_aspect(0.8)
  416. tool = widgets.RectangleSelector(ax, interactive=True,
  417. use_data_coordinates=use_data_coordinates)
  418. # Create rectangle
  419. click_and_drag(tool, start=(70, 65), end=(120, 115))
  420. assert tool.extents == (70.0, 120.0, 65.0, 115.0)
  421. tool.add_state('square')
  422. tool.add_state('center')
  423. if use_data_coordinates:
  424. # resize E handle
  425. extents = tool.extents
  426. xdata, ydata, width = extents[1], extents[3], extents[1] - extents[0]
  427. xdiff, ycenter = 10, extents[2] + (extents[3] - extents[2]) / 2
  428. xdata_new, ydata_new = xdata + xdiff, ydata
  429. ychange = width / 2 + xdiff
  430. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  431. assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
  432. ycenter - ychange, ycenter + ychange])
  433. else:
  434. # resize E handle
  435. extents = tool.extents
  436. xdata, ydata = extents[1], extents[3]
  437. xdiff = 10
  438. xdata_new, ydata_new = xdata + xdiff, ydata
  439. ychange = xdiff * 1 / tool._aspect_ratio_correction
  440. click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
  441. assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
  442. 46.25, 133.75])
  443. def test_ellipse(ax):
  444. """For ellipse, test out the key modifiers"""
  445. tool = widgets.EllipseSelector(ax, grab_range=10, interactive=True)
  446. tool.extents = (100, 150, 100, 150)
  447. # drag the rectangle
  448. click_and_drag(tool, start=(125, 125), end=(145, 145))
  449. assert tool.extents == (120, 170, 120, 170)
  450. # create from center
  451. click_and_drag(tool, start=(100, 100), end=(125, 125), key='control')
  452. assert tool.extents == (75, 125, 75, 125)
  453. # create a square
  454. click_and_drag(tool, start=(10, 10), end=(35, 30), key='shift')
  455. extents = [int(e) for e in tool.extents]
  456. assert extents == [10, 35, 10, 35]
  457. # create a square from center
  458. click_and_drag(tool, start=(100, 100), end=(125, 130), key='ctrl+shift')
  459. extents = [int(e) for e in tool.extents]
  460. assert extents == [70, 130, 70, 130]
  461. assert tool.geometry.shape == (2, 73)
  462. assert_allclose(tool.geometry[:, 0], [70., 100])
  463. def test_rectangle_handles(ax):
  464. tool = widgets.RectangleSelector(ax, grab_range=10, interactive=True,
  465. handle_props={'markerfacecolor': 'r',
  466. 'markeredgecolor': 'b'})
  467. tool.extents = (100, 150, 100, 150)
  468. assert_allclose(tool.corners, ((100, 150, 150, 100), (100, 100, 150, 150)))
  469. assert tool.extents == (100, 150, 100, 150)
  470. assert_allclose(tool.edge_centers,
  471. ((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150)))
  472. assert tool.extents == (100, 150, 100, 150)
  473. # grab a corner and move it
  474. click_and_drag(tool, start=(100, 100), end=(120, 120))
  475. assert tool.extents == (120, 150, 120, 150)
  476. # grab the center and move it
  477. click_and_drag(tool, start=(132, 132), end=(120, 120))
  478. assert tool.extents == (108, 138, 108, 138)
  479. # create a new rectangle
  480. click_and_drag(tool, start=(10, 10), end=(100, 100))
  481. assert tool.extents == (10, 100, 10, 100)
  482. # Check that marker_props worked.
  483. assert mcolors.same_color(
  484. tool._corner_handles.artists[0].get_markerfacecolor(), 'r')
  485. assert mcolors.same_color(
  486. tool._corner_handles.artists[0].get_markeredgecolor(), 'b')
  487. @pytest.mark.parametrize('interactive', [True, False])
  488. def test_rectangle_selector_onselect(ax, interactive):
  489. # check when press and release events take place at the same position
  490. onselect = mock.Mock(spec=noop, return_value=None)
  491. tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=interactive)
  492. # move outside of axis
  493. click_and_drag(tool, start=(100, 110), end=(150, 120))
  494. onselect.assert_called_once()
  495. assert tool.extents == (100.0, 150.0, 110.0, 120.0)
  496. onselect.reset_mock()
  497. click_and_drag(tool, start=(10, 100), end=(10, 100))
  498. onselect.assert_called_once()
  499. @pytest.mark.parametrize('ignore_event_outside', [True, False])
  500. def test_rectangle_selector_ignore_outside(ax, ignore_event_outside):
  501. onselect = mock.Mock(spec=noop, return_value=None)
  502. tool = widgets.RectangleSelector(ax, onselect=onselect,
  503. ignore_event_outside=ignore_event_outside)
  504. click_and_drag(tool, start=(100, 110), end=(150, 120))
  505. onselect.assert_called_once()
  506. assert tool.extents == (100.0, 150.0, 110.0, 120.0)
  507. onselect.reset_mock()
  508. # Trigger event outside of span
  509. click_and_drag(tool, start=(150, 150), end=(160, 160))
  510. if ignore_event_outside:
  511. # event have been ignored and span haven't changed.
  512. onselect.assert_not_called()
  513. assert tool.extents == (100.0, 150.0, 110.0, 120.0)
  514. else:
  515. # A new shape is created
  516. onselect.assert_called_once()
  517. assert tool.extents == (150.0, 160.0, 150.0, 160.0)
  518. @pytest.mark.parametrize('orientation, onmove_callback, kwargs', [
  519. ('horizontal', False, dict(minspan=10, useblit=True)),
  520. ('vertical', True, dict(button=1)),
  521. ('horizontal', False, dict(props=dict(fill=True))),
  522. ('horizontal', False, dict(interactive=True)),
  523. ])
  524. def test_span_selector(ax, orientation, onmove_callback, kwargs):
  525. onselect = mock.Mock(spec=noop, return_value=None)
  526. onmove = mock.Mock(spec=noop, return_value=None)
  527. if onmove_callback:
  528. kwargs['onmove_callback'] = onmove
  529. # While at it, also test that span selectors work in the presence of twin axes on
  530. # top of the axes that contain the selector. Note that we need to unforce the axes
  531. # aspect here, otherwise the twin axes forces the original axes' limits (to respect
  532. # aspect=1) which makes some of the values below go out of bounds.
  533. ax.set_aspect("auto")
  534. tax = ax.twinx()
  535. tool = widgets.SpanSelector(ax, onselect, orientation, **kwargs)
  536. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  537. # move outside of axis
  538. do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
  539. do_event(tool, 'release', xdata=250, ydata=250, button=1)
  540. onselect.assert_called_once_with(100, 199)
  541. if onmove_callback:
  542. onmove.assert_called_once_with(100, 199)
  543. @pytest.mark.parametrize('interactive', [True, False])
  544. def test_span_selector_onselect(ax, interactive):
  545. onselect = mock.Mock(spec=noop, return_value=None)
  546. tool = widgets.SpanSelector(ax, onselect, 'horizontal',
  547. interactive=interactive)
  548. # move outside of axis
  549. click_and_drag(tool, start=(100, 100), end=(150, 100))
  550. onselect.assert_called_once()
  551. assert tool.extents == (100, 150)
  552. onselect.reset_mock()
  553. click_and_drag(tool, start=(10, 100), end=(10, 100))
  554. onselect.assert_called_once()
  555. @pytest.mark.parametrize('ignore_event_outside', [True, False])
  556. def test_span_selector_ignore_outside(ax, ignore_event_outside):
  557. onselect = mock.Mock(spec=noop, return_value=None)
  558. onmove = mock.Mock(spec=noop, return_value=None)
  559. tool = widgets.SpanSelector(ax, onselect, 'horizontal',
  560. onmove_callback=onmove,
  561. ignore_event_outside=ignore_event_outside)
  562. click_and_drag(tool, start=(100, 100), end=(125, 125))
  563. onselect.assert_called_once()
  564. onmove.assert_called_once()
  565. assert tool.extents == (100, 125)
  566. onselect.reset_mock()
  567. onmove.reset_mock()
  568. # Trigger event outside of span
  569. click_and_drag(tool, start=(150, 150), end=(160, 160))
  570. if ignore_event_outside:
  571. # event have been ignored and span haven't changed.
  572. onselect.assert_not_called()
  573. onmove.assert_not_called()
  574. assert tool.extents == (100, 125)
  575. else:
  576. # A new shape is created
  577. onselect.assert_called_once()
  578. onmove.assert_called_once()
  579. assert tool.extents == (150, 160)
  580. @pytest.mark.parametrize('drag_from_anywhere', [True, False])
  581. def test_span_selector_drag(ax, drag_from_anywhere):
  582. # Create span
  583. tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal',
  584. interactive=True,
  585. drag_from_anywhere=drag_from_anywhere)
  586. click_and_drag(tool, start=(10, 10), end=(100, 120))
  587. assert tool.extents == (10, 100)
  588. # Drag inside span
  589. #
  590. # If drag_from_anywhere == True, this will move the span by 10,
  591. # giving new value extents = 20, 110
  592. #
  593. # If drag_from_anywhere == False, this will create a new span with
  594. # value extents = 25, 35
  595. click_and_drag(tool, start=(25, 15), end=(35, 25))
  596. if drag_from_anywhere:
  597. assert tool.extents == (20, 110)
  598. else:
  599. assert tool.extents == (25, 35)
  600. # Check that in both cases, dragging outside the span draws a new span
  601. click_and_drag(tool, start=(175, 185), end=(185, 195))
  602. assert tool.extents == (175, 185)
  603. def test_span_selector_direction(ax):
  604. tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal',
  605. interactive=True)
  606. assert tool.direction == 'horizontal'
  607. assert tool._edge_handles.direction == 'horizontal'
  608. with pytest.raises(ValueError):
  609. tool = widgets.SpanSelector(ax, onselect=noop,
  610. direction='invalid_direction')
  611. tool.direction = 'vertical'
  612. assert tool.direction == 'vertical'
  613. assert tool._edge_handles.direction == 'vertical'
  614. with pytest.raises(ValueError):
  615. tool.direction = 'invalid_string'
  616. def test_span_selector_set_props_handle_props(ax):
  617. tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal',
  618. interactive=True,
  619. props=dict(facecolor='b', alpha=0.2),
  620. handle_props=dict(alpha=0.5))
  621. # Create rectangle
  622. click_and_drag(tool, start=(0, 10), end=(100, 120))
  623. artist = tool._selection_artist
  624. assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2)
  625. tool.set_props(facecolor='r', alpha=0.3)
  626. assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3)
  627. for artist in tool._handles_artists:
  628. assert artist.get_color() == 'b'
  629. assert artist.get_alpha() == 0.5
  630. tool.set_handle_props(color='r', alpha=0.3)
  631. for artist in tool._handles_artists:
  632. assert artist.get_color() == 'r'
  633. assert artist.get_alpha() == 0.3
  634. @pytest.mark.parametrize('selector', ['span', 'rectangle'])
  635. def test_selector_clear(ax, selector):
  636. kwargs = dict(ax=ax, interactive=True)
  637. if selector == 'span':
  638. Selector = widgets.SpanSelector
  639. kwargs['direction'] = 'horizontal'
  640. kwargs['onselect'] = noop
  641. else:
  642. Selector = widgets.RectangleSelector
  643. tool = Selector(**kwargs)
  644. click_and_drag(tool, start=(10, 10), end=(100, 120))
  645. # press-release event outside the selector to clear the selector
  646. click_and_drag(tool, start=(130, 130), end=(130, 130))
  647. assert not tool._selection_completed
  648. kwargs['ignore_event_outside'] = True
  649. tool = Selector(**kwargs)
  650. assert tool.ignore_event_outside
  651. click_and_drag(tool, start=(10, 10), end=(100, 120))
  652. # press-release event outside the selector ignored
  653. click_and_drag(tool, start=(130, 130), end=(130, 130))
  654. assert tool._selection_completed
  655. do_event(tool, 'on_key_press', key='escape')
  656. assert not tool._selection_completed
  657. @pytest.mark.parametrize('selector', ['span', 'rectangle'])
  658. def test_selector_clear_method(ax, selector):
  659. if selector == 'span':
  660. tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal',
  661. interactive=True,
  662. ignore_event_outside=True)
  663. else:
  664. tool = widgets.RectangleSelector(ax, interactive=True)
  665. click_and_drag(tool, start=(10, 10), end=(100, 120))
  666. assert tool._selection_completed
  667. assert tool.get_visible()
  668. if selector == 'span':
  669. assert tool.extents == (10, 100)
  670. tool.clear()
  671. assert not tool._selection_completed
  672. assert not tool.get_visible()
  673. # Do another cycle of events to make sure we can
  674. click_and_drag(tool, start=(10, 10), end=(50, 120))
  675. assert tool._selection_completed
  676. assert tool.get_visible()
  677. if selector == 'span':
  678. assert tool.extents == (10, 50)
  679. def test_span_selector_add_state(ax):
  680. tool = widgets.SpanSelector(ax, noop, 'horizontal',
  681. interactive=True)
  682. with pytest.raises(ValueError):
  683. tool.add_state('unsupported_state')
  684. with pytest.raises(ValueError):
  685. tool.add_state('center')
  686. with pytest.raises(ValueError):
  687. tool.add_state('square')
  688. tool.add_state('move')
  689. def test_tool_line_handle(ax):
  690. positions = [20, 30, 50]
  691. tool_line_handle = widgets.ToolLineHandles(ax, positions, 'horizontal',
  692. useblit=False)
  693. for artist in tool_line_handle.artists:
  694. assert not artist.get_animated()
  695. assert not artist.get_visible()
  696. tool_line_handle.set_visible(True)
  697. tool_line_handle.set_animated(True)
  698. for artist in tool_line_handle.artists:
  699. assert artist.get_animated()
  700. assert artist.get_visible()
  701. assert tool_line_handle.positions == positions
  702. @pytest.mark.parametrize('direction', ("horizontal", "vertical"))
  703. def test_span_selector_bound(direction):
  704. fig, ax = plt.subplots(1, 1)
  705. ax.plot([10, 20], [10, 30])
  706. fig.canvas.draw()
  707. x_bound = ax.get_xbound()
  708. y_bound = ax.get_ybound()
  709. tool = widgets.SpanSelector(ax, print, direction, interactive=True)
  710. assert ax.get_xbound() == x_bound
  711. assert ax.get_ybound() == y_bound
  712. bound = x_bound if direction == 'horizontal' else y_bound
  713. assert tool._edge_handles.positions == list(bound)
  714. press_data = (10.5, 11.5)
  715. move_data = (11, 13) # Updating selector is done in onmove
  716. release_data = move_data
  717. click_and_drag(tool, start=press_data, end=move_data)
  718. assert ax.get_xbound() == x_bound
  719. assert ax.get_ybound() == y_bound
  720. index = 0 if direction == 'horizontal' else 1
  721. handle_positions = [press_data[index], release_data[index]]
  722. assert tool._edge_handles.positions == handle_positions
  723. @pytest.mark.backend('QtAgg', skip_on_importerror=True)
  724. def test_span_selector_animated_artists_callback():
  725. """Check that the animated artists changed in callbacks are updated."""
  726. x = np.linspace(0, 2 * np.pi, 100)
  727. values = np.sin(x)
  728. fig, ax = plt.subplots()
  729. ln, = ax.plot(x, values, animated=True)
  730. ln2, = ax.plot([], animated=True)
  731. # spin the event loop to let the backend process any pending operations
  732. # before drawing artists
  733. # See blitting tutorial
  734. plt.pause(0.1)
  735. ax.draw_artist(ln)
  736. fig.canvas.blit(fig.bbox)
  737. def mean(vmin, vmax):
  738. # Return mean of values in x between *vmin* and *vmax*
  739. indmin, indmax = np.searchsorted(x, (vmin, vmax))
  740. v = values[indmin:indmax].mean()
  741. ln2.set_data(x, np.full_like(x, v))
  742. span = widgets.SpanSelector(ax, mean, direction='horizontal',
  743. onmove_callback=mean,
  744. interactive=True,
  745. drag_from_anywhere=True,
  746. useblit=True)
  747. # Add span selector and check that the line is draw after it was updated
  748. # by the callback
  749. press_data = [1, 2]
  750. move_data = [2, 2]
  751. do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
  752. do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
  753. assert span._get_animated_artists() == (ln, ln2)
  754. assert ln.stale is False
  755. assert ln2.stale
  756. assert_allclose(ln2.get_ydata(), 0.9547335049088455)
  757. span.update()
  758. assert ln2.stale is False
  759. # Change span selector and check that the line is drawn/updated after its
  760. # value was updated by the callback
  761. press_data = [4, 0]
  762. move_data = [5, 2]
  763. release_data = [5, 2]
  764. do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
  765. do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
  766. assert ln.stale is False
  767. assert ln2.stale
  768. assert_allclose(ln2.get_ydata(), -0.9424150707548072)
  769. do_event(span, 'release', xdata=release_data[0],
  770. ydata=release_data[1], button=1)
  771. assert ln2.stale is False
  772. def test_snapping_values_span_selector(ax):
  773. def onselect(*args):
  774. pass
  775. tool = widgets.SpanSelector(ax, onselect, direction='horizontal',)
  776. snap_function = tool._snap
  777. snap_values = np.linspace(0, 5, 11)
  778. values = np.array([-0.1, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 4.76, 5.0, 5.5])
  779. expect = np.array([00.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 5.00, 5.0, 5.0])
  780. values = snap_function(values, snap_values)
  781. assert_allclose(values, expect)
  782. def test_span_selector_snap(ax):
  783. def onselect(vmin, vmax):
  784. ax._got_onselect = True
  785. snap_values = np.arange(50) * 4
  786. tool = widgets.SpanSelector(ax, onselect, direction='horizontal',
  787. snap_values=snap_values)
  788. tool.extents = (17, 35)
  789. assert tool.extents == (16, 36)
  790. tool.snap_values = None
  791. assert tool.snap_values is None
  792. tool.extents = (17, 35)
  793. assert tool.extents == (17, 35)
  794. def test_span_selector_extents(ax):
  795. tool = widgets.SpanSelector(
  796. ax, lambda a, b: None, "horizontal", ignore_event_outside=True
  797. )
  798. tool.extents = (5, 10)
  799. assert tool.extents == (5, 10)
  800. assert tool._selection_completed
  801. # Since `ignore_event_outside=True`, this event should be ignored
  802. press_data = (12, 14)
  803. release_data = (20, 14)
  804. click_and_drag(tool, start=press_data, end=release_data)
  805. assert tool.extents == (5, 10)
  806. @pytest.mark.parametrize('kwargs', [
  807. dict(),
  808. dict(useblit=False, props=dict(color='red')),
  809. dict(useblit=True, button=1),
  810. ])
  811. def test_lasso_selector(ax, kwargs):
  812. onselect = mock.Mock(spec=noop, return_value=None)
  813. tool = widgets.LassoSelector(ax, onselect=onselect, **kwargs)
  814. do_event(tool, 'press', xdata=100, ydata=100, button=1)
  815. do_event(tool, 'onmove', xdata=125, ydata=125, button=1)
  816. do_event(tool, 'release', xdata=150, ydata=150, button=1)
  817. onselect.assert_called_once_with([(100, 100), (125, 125), (150, 150)])
  818. def test_lasso_selector_set_props(ax):
  819. onselect = mock.Mock(spec=noop, return_value=None)
  820. tool = widgets.LassoSelector(ax, onselect=onselect,
  821. props=dict(color='b', alpha=0.2))
  822. artist = tool._selection_artist
  823. assert mcolors.same_color(artist.get_color(), 'b')
  824. assert artist.get_alpha() == 0.2
  825. tool.set_props(color='r', alpha=0.3)
  826. assert mcolors.same_color(artist.get_color(), 'r')
  827. assert artist.get_alpha() == 0.3
  828. def test_lasso_set_props(ax):
  829. onselect = mock.Mock(spec=noop, return_value=None)
  830. tool = widgets.Lasso(ax, (100, 100), onselect)
  831. line = tool.line
  832. assert mcolors.same_color(line.get_color(), 'black')
  833. assert line.get_linestyle() == '-'
  834. assert line.get_lw() == 2
  835. tool = widgets.Lasso(ax, (100, 100), onselect, props=dict(
  836. linestyle='-', color='darkblue', alpha=0.2, lw=1))
  837. line = tool.line
  838. assert mcolors.same_color(line.get_color(), 'darkblue')
  839. assert line.get_alpha() == 0.2
  840. assert line.get_lw() == 1
  841. assert line.get_linestyle() == '-'
  842. line.set_color('r')
  843. line.set_alpha(0.3)
  844. assert mcolors.same_color(line.get_color(), 'r')
  845. assert line.get_alpha() == 0.3
  846. def test_CheckButtons(ax):
  847. labels = ('a', 'b', 'c')
  848. check = widgets.CheckButtons(ax, labels, (True, False, True))
  849. assert check.get_status() == [True, False, True]
  850. check.set_active(0)
  851. assert check.get_status() == [False, False, True]
  852. assert check.get_checked_labels() == ['c']
  853. check.clear()
  854. assert check.get_status() == [False, False, False]
  855. assert check.get_checked_labels() == []
  856. for invalid_index in [-1, len(labels), len(labels)+5]:
  857. with pytest.raises(ValueError):
  858. check.set_active(index=invalid_index)
  859. for invalid_value in ['invalid', -1]:
  860. with pytest.raises(TypeError):
  861. check.set_active(1, state=invalid_value)
  862. cid = check.on_clicked(lambda: None)
  863. check.disconnect(cid)
  864. @pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"])
  865. def test_TextBox(ax, toolbar):
  866. # Avoid "toolmanager is provisional" warning.
  867. plt.rcParams._set("toolbar", toolbar)
  868. submit_event = mock.Mock(spec=noop, return_value=None)
  869. text_change_event = mock.Mock(spec=noop, return_value=None)
  870. tool = widgets.TextBox(ax, '')
  871. tool.on_submit(submit_event)
  872. tool.on_text_change(text_change_event)
  873. assert tool.text == ''
  874. do_event(tool, '_click')
  875. tool.set_val('x**2')
  876. assert tool.text == 'x**2'
  877. assert text_change_event.call_count == 1
  878. tool.begin_typing()
  879. tool.stop_typing()
  880. assert submit_event.call_count == 2
  881. do_event(tool, '_click', xdata=.5, ydata=.5) # Ensure the click is in the axes.
  882. do_event(tool, '_keypress', key='+')
  883. do_event(tool, '_keypress', key='5')
  884. assert text_change_event.call_count == 3
  885. def test_RadioButtons(ax):
  886. radio = widgets.RadioButtons(ax, ('Radio 1', 'Radio 2', 'Radio 3'))
  887. radio.set_active(1)
  888. assert radio.value_selected == 'Radio 2'
  889. assert radio.index_selected == 1
  890. radio.clear()
  891. assert radio.value_selected == 'Radio 1'
  892. assert radio.index_selected == 0
  893. @image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True)
  894. def test_check_radio_buttons_image():
  895. ax = get_ax()
  896. fig = ax.get_figure(root=False)
  897. fig.subplots_adjust(left=0.3)
  898. rax1 = fig.add_axes((0.05, 0.7, 0.2, 0.15))
  899. rb1 = widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3'))
  900. rax2 = fig.add_axes((0.05, 0.5, 0.2, 0.15))
  901. cb1 = widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'),
  902. (False, True, True))
  903. rax3 = fig.add_axes((0.05, 0.3, 0.2, 0.15))
  904. rb3 = widgets.RadioButtons(
  905. rax3, ('Radio 1', 'Radio 2', 'Radio 3'),
  906. label_props={'fontsize': [8, 12, 16],
  907. 'color': ['red', 'green', 'blue']},
  908. radio_props={'edgecolor': ['red', 'green', 'blue'],
  909. 'facecolor': ['mistyrose', 'palegreen', 'lightblue']})
  910. rax4 = fig.add_axes((0.05, 0.1, 0.2, 0.15))
  911. cb4 = widgets.CheckButtons(
  912. rax4, ('Check 1', 'Check 2', 'Check 3'), (False, True, True),
  913. label_props={'fontsize': [8, 12, 16],
  914. 'color': ['red', 'green', 'blue']},
  915. frame_props={'edgecolor': ['red', 'green', 'blue'],
  916. 'facecolor': ['mistyrose', 'palegreen', 'lightblue']},
  917. check_props={'color': ['red', 'green', 'blue']})
  918. @check_figures_equal(extensions=["png"])
  919. def test_radio_buttons(fig_test, fig_ref):
  920. widgets.RadioButtons(fig_test.subplots(), ["tea", "coffee"])
  921. ax = fig_ref.add_subplot(xticks=[], yticks=[])
  922. ax.scatter([.15, .15], [2/3, 1/3], transform=ax.transAxes,
  923. s=(plt.rcParams["font.size"] / 2) ** 2, c=["C0", "none"])
  924. ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center")
  925. ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center")
  926. @check_figures_equal(extensions=['png'])
  927. def test_radio_buttons_props(fig_test, fig_ref):
  928. label_props = {'color': ['red'], 'fontsize': [24]}
  929. radio_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2}
  930. widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'],
  931. label_props=label_props, radio_props=radio_props)
  932. cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee'])
  933. cb.set_label_props(label_props)
  934. # Setting the label size automatically increases default marker size, so we
  935. # need to do that here as well.
  936. cb.set_radio_props({**radio_props, 's': (24 / 2)**2})
  937. def test_radio_button_active_conflict(ax):
  938. with pytest.warns(UserWarning,
  939. match=r'Both the \*activecolor\* parameter'):
  940. rb = widgets.RadioButtons(ax, ['tea', 'coffee'], activecolor='red',
  941. radio_props={'facecolor': 'green'})
  942. # *radio_props*' facecolor wins over *activecolor*
  943. assert mcolors.same_color(rb._buttons.get_facecolor(), ['green', 'none'])
  944. @check_figures_equal(extensions=['png'])
  945. def test_radio_buttons_activecolor_change(fig_test, fig_ref):
  946. widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'],
  947. activecolor='green')
  948. # Test property setter.
  949. cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee'],
  950. activecolor='red')
  951. cb.activecolor = 'green'
  952. @check_figures_equal(extensions=["png"])
  953. def test_check_buttons(fig_test, fig_ref):
  954. widgets.CheckButtons(fig_test.subplots(), ["tea", "coffee"], [True, True])
  955. ax = fig_ref.add_subplot(xticks=[], yticks=[])
  956. ax.scatter([.15, .15], [2/3, 1/3], marker='s', transform=ax.transAxes,
  957. s=(plt.rcParams["font.size"] / 2) ** 2, c=["none", "none"])
  958. ax.scatter([.15, .15], [2/3, 1/3], marker='x', transform=ax.transAxes,
  959. s=(plt.rcParams["font.size"] / 2) ** 2, c=["k", "k"])
  960. ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center")
  961. ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center")
  962. @check_figures_equal(extensions=['png'])
  963. def test_check_button_props(fig_test, fig_ref):
  964. label_props = {'color': ['red'], 'fontsize': [24]}
  965. frame_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2}
  966. check_props = {'facecolor': 'red', 'linewidth': 2}
  967. widgets.CheckButtons(fig_ref.subplots(), ['tea', 'coffee'], [True, True],
  968. label_props=label_props, frame_props=frame_props,
  969. check_props=check_props)
  970. cb = widgets.CheckButtons(fig_test.subplots(), ['tea', 'coffee'],
  971. [True, True])
  972. cb.set_label_props(label_props)
  973. # Setting the label size automatically increases default marker size, so we
  974. # need to do that here as well.
  975. cb.set_frame_props({**frame_props, 's': (24 / 2)**2})
  976. # FIXME: Axes.scatter promotes facecolor to edgecolor on unfilled markers,
  977. # but Collection.update doesn't do that (it forgot the marker already).
  978. # This means we cannot pass facecolor to both setters directly.
  979. check_props['edgecolor'] = check_props.pop('facecolor')
  980. cb.set_check_props({**check_props, 's': (24 / 2)**2})
  981. def test_slider_slidermin_slidermax_invalid():
  982. fig, ax = plt.subplots()
  983. # test min/max with floats
  984. with pytest.raises(ValueError):
  985. widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  986. slidermin=10.0)
  987. with pytest.raises(ValueError):
  988. widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  989. slidermax=10.0)
  990. def test_slider_slidermin_slidermax():
  991. fig, ax = plt.subplots()
  992. slider_ = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  993. valinit=5.0)
  994. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  995. valinit=1.0, slidermin=slider_)
  996. assert slider.val == slider_.val
  997. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  998. valinit=10.0, slidermax=slider_)
  999. assert slider.val == slider_.val
  1000. def test_slider_valmin_valmax():
  1001. fig, ax = plt.subplots()
  1002. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  1003. valinit=-10.0)
  1004. assert slider.val == slider.valmin
  1005. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  1006. valinit=25.0)
  1007. assert slider.val == slider.valmax
  1008. def test_slider_valstep_snapping():
  1009. fig, ax = plt.subplots()
  1010. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  1011. valinit=11.4, valstep=1)
  1012. assert slider.val == 11
  1013. slider = widgets.Slider(ax=ax, label='', valmin=0.0, valmax=24.0,
  1014. valinit=11.4, valstep=[0, 1, 5.5, 19.7])
  1015. assert slider.val == 5.5
  1016. def test_slider_horizontal_vertical():
  1017. fig, ax = plt.subplots()
  1018. slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=24,
  1019. valinit=12, orientation='horizontal')
  1020. slider.set_val(10)
  1021. assert slider.val == 10
  1022. # check the dimension of the slider patch in axes units
  1023. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  1024. assert_allclose(box.bounds, [0, .25, 10/24, .5])
  1025. fig, ax = plt.subplots()
  1026. slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=24,
  1027. valinit=12, orientation='vertical')
  1028. slider.set_val(10)
  1029. assert slider.val == 10
  1030. # check the dimension of the slider patch in axes units
  1031. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  1032. assert_allclose(box.bounds, [.25, 0, .5, 10/24])
  1033. def test_slider_reset():
  1034. fig, ax = plt.subplots()
  1035. slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=1, valinit=.5)
  1036. slider.set_val(0.75)
  1037. slider.reset()
  1038. assert slider.val == 0.5
  1039. @pytest.mark.parametrize("orientation", ["horizontal", "vertical"])
  1040. def test_range_slider(orientation):
  1041. if orientation == "vertical":
  1042. idx = [1, 0, 3, 2]
  1043. else:
  1044. idx = [0, 1, 2, 3]
  1045. fig, ax = plt.subplots()
  1046. slider = widgets.RangeSlider(
  1047. ax=ax, label="", valmin=0.0, valmax=1.0, orientation=orientation,
  1048. valinit=[0.1, 0.34]
  1049. )
  1050. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  1051. assert_allclose(box.get_points().flatten()[idx], [0.1, 0.25, 0.34, 0.75])
  1052. # Check initial value is set correctly
  1053. assert_allclose(slider.val, (0.1, 0.34))
  1054. def handle_positions(slider):
  1055. if orientation == "vertical":
  1056. return [h.get_ydata()[0] for h in slider._handles]
  1057. else:
  1058. return [h.get_xdata()[0] for h in slider._handles]
  1059. slider.set_val((0.4, 0.6))
  1060. assert_allclose(slider.val, (0.4, 0.6))
  1061. assert_allclose(handle_positions(slider), (0.4, 0.6))
  1062. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  1063. assert_allclose(box.get_points().flatten()[idx], [0.4, .25, 0.6, .75])
  1064. slider.set_val((0.2, 0.1))
  1065. assert_allclose(slider.val, (0.1, 0.2))
  1066. assert_allclose(handle_positions(slider), (0.1, 0.2))
  1067. slider.set_val((-1, 10))
  1068. assert_allclose(slider.val, (0, 1))
  1069. assert_allclose(handle_positions(slider), (0, 1))
  1070. slider.reset()
  1071. assert_allclose(slider.val, (0.1, 0.34))
  1072. assert_allclose(handle_positions(slider), (0.1, 0.34))
  1073. @pytest.mark.parametrize("orientation", ["horizontal", "vertical"])
  1074. def test_range_slider_same_init_values(orientation):
  1075. if orientation == "vertical":
  1076. idx = [1, 0, 3, 2]
  1077. else:
  1078. idx = [0, 1, 2, 3]
  1079. fig, ax = plt.subplots()
  1080. slider = widgets.RangeSlider(
  1081. ax=ax, label="", valmin=0.0, valmax=1.0, orientation=orientation,
  1082. valinit=[0, 0]
  1083. )
  1084. box = slider.poly.get_extents().transformed(ax.transAxes.inverted())
  1085. assert_allclose(box.get_points().flatten()[idx], [0, 0.25, 0, 0.75])
  1086. def check_polygon_selector(event_sequence, expected_result, selections_count,
  1087. **kwargs):
  1088. """
  1089. Helper function to test Polygon Selector.
  1090. Parameters
  1091. ----------
  1092. event_sequence : list of tuples (etype, dict())
  1093. A sequence of events to perform. The sequence is a list of tuples
  1094. where the first element of the tuple is an etype (e.g., 'onmove',
  1095. 'press', etc.), and the second element of the tuple is a dictionary of
  1096. the arguments for the event (e.g., xdata=5, key='shift', etc.).
  1097. expected_result : list of vertices (xdata, ydata)
  1098. The list of vertices that are expected to result from the event
  1099. sequence.
  1100. selections_count : int
  1101. Wait for the tool to call its `onselect` function `selections_count`
  1102. times, before comparing the result to the `expected_result`
  1103. **kwargs
  1104. Keyword arguments are passed to PolygonSelector.
  1105. """
  1106. ax = get_ax()
  1107. onselect = mock.Mock(spec=noop, return_value=None)
  1108. tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs)
  1109. for (etype, event_args) in event_sequence:
  1110. do_event(tool, etype, **event_args)
  1111. assert onselect.call_count == selections_count
  1112. assert onselect.call_args == ((expected_result, ), {})
  1113. def polygon_place_vertex(xdata, ydata):
  1114. return [('onmove', dict(xdata=xdata, ydata=ydata)),
  1115. ('press', dict(xdata=xdata, ydata=ydata)),
  1116. ('release', dict(xdata=xdata, ydata=ydata))]
  1117. def polygon_remove_vertex(xdata, ydata):
  1118. return [('onmove', dict(xdata=xdata, ydata=ydata)),
  1119. ('press', dict(xdata=xdata, ydata=ydata, button=3)),
  1120. ('release', dict(xdata=xdata, ydata=ydata, button=3))]
  1121. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1122. def test_polygon_selector(draw_bounding_box):
  1123. check_selector = functools.partial(
  1124. check_polygon_selector, draw_bounding_box=draw_bounding_box)
  1125. # Simple polygon
  1126. expected_result = [(50, 50), (150, 50), (50, 150)]
  1127. event_sequence = [
  1128. *polygon_place_vertex(50, 50),
  1129. *polygon_place_vertex(150, 50),
  1130. *polygon_place_vertex(50, 150),
  1131. *polygon_place_vertex(50, 50),
  1132. ]
  1133. check_selector(event_sequence, expected_result, 1)
  1134. # Move first vertex before completing the polygon.
  1135. expected_result = [(75, 50), (150, 50), (50, 150)]
  1136. event_sequence = [
  1137. *polygon_place_vertex(50, 50),
  1138. *polygon_place_vertex(150, 50),
  1139. ('on_key_press', dict(key='control')),
  1140. ('onmove', dict(xdata=50, ydata=50)),
  1141. ('press', dict(xdata=50, ydata=50)),
  1142. ('onmove', dict(xdata=75, ydata=50)),
  1143. ('release', dict(xdata=75, ydata=50)),
  1144. ('on_key_release', dict(key='control')),
  1145. *polygon_place_vertex(50, 150),
  1146. *polygon_place_vertex(75, 50),
  1147. ]
  1148. check_selector(event_sequence, expected_result, 1)
  1149. # Move first two vertices at once before completing the polygon.
  1150. expected_result = [(50, 75), (150, 75), (50, 150)]
  1151. event_sequence = [
  1152. *polygon_place_vertex(50, 50),
  1153. *polygon_place_vertex(150, 50),
  1154. ('on_key_press', dict(key='shift')),
  1155. ('onmove', dict(xdata=100, ydata=100)),
  1156. ('press', dict(xdata=100, ydata=100)),
  1157. ('onmove', dict(xdata=100, ydata=125)),
  1158. ('release', dict(xdata=100, ydata=125)),
  1159. ('on_key_release', dict(key='shift')),
  1160. *polygon_place_vertex(50, 150),
  1161. *polygon_place_vertex(50, 75),
  1162. ]
  1163. check_selector(event_sequence, expected_result, 1)
  1164. # Move first vertex after completing the polygon.
  1165. expected_result = [(75, 50), (150, 50), (50, 150)]
  1166. event_sequence = [
  1167. *polygon_place_vertex(50, 50),
  1168. *polygon_place_vertex(150, 50),
  1169. *polygon_place_vertex(50, 150),
  1170. *polygon_place_vertex(50, 50),
  1171. ('onmove', dict(xdata=50, ydata=50)),
  1172. ('press', dict(xdata=50, ydata=50)),
  1173. ('onmove', dict(xdata=75, ydata=50)),
  1174. ('release', dict(xdata=75, ydata=50)),
  1175. ]
  1176. check_selector(event_sequence, expected_result, 2)
  1177. # Move all vertices after completing the polygon.
  1178. expected_result = [(75, 75), (175, 75), (75, 175)]
  1179. event_sequence = [
  1180. *polygon_place_vertex(50, 50),
  1181. *polygon_place_vertex(150, 50),
  1182. *polygon_place_vertex(50, 150),
  1183. *polygon_place_vertex(50, 50),
  1184. ('on_key_press', dict(key='shift')),
  1185. ('onmove', dict(xdata=100, ydata=100)),
  1186. ('press', dict(xdata=100, ydata=100)),
  1187. ('onmove', dict(xdata=125, ydata=125)),
  1188. ('release', dict(xdata=125, ydata=125)),
  1189. ('on_key_release', dict(key='shift')),
  1190. ]
  1191. check_selector(event_sequence, expected_result, 2)
  1192. # Try to move a vertex and move all before placing any vertices.
  1193. expected_result = [(50, 50), (150, 50), (50, 150)]
  1194. event_sequence = [
  1195. ('on_key_press', dict(key='control')),
  1196. ('onmove', dict(xdata=100, ydata=100)),
  1197. ('press', dict(xdata=100, ydata=100)),
  1198. ('onmove', dict(xdata=125, ydata=125)),
  1199. ('release', dict(xdata=125, ydata=125)),
  1200. ('on_key_release', dict(key='control')),
  1201. ('on_key_press', dict(key='shift')),
  1202. ('onmove', dict(xdata=100, ydata=100)),
  1203. ('press', dict(xdata=100, ydata=100)),
  1204. ('onmove', dict(xdata=125, ydata=125)),
  1205. ('release', dict(xdata=125, ydata=125)),
  1206. ('on_key_release', dict(key='shift')),
  1207. *polygon_place_vertex(50, 50),
  1208. *polygon_place_vertex(150, 50),
  1209. *polygon_place_vertex(50, 150),
  1210. *polygon_place_vertex(50, 50),
  1211. ]
  1212. check_selector(event_sequence, expected_result, 1)
  1213. # Try to place vertex out-of-bounds, then reset, and start a new polygon.
  1214. expected_result = [(50, 50), (150, 50), (50, 150)]
  1215. event_sequence = [
  1216. *polygon_place_vertex(50, 50),
  1217. *polygon_place_vertex(250, 50),
  1218. ('on_key_press', dict(key='escape')),
  1219. ('on_key_release', dict(key='escape')),
  1220. *polygon_place_vertex(50, 50),
  1221. *polygon_place_vertex(150, 50),
  1222. *polygon_place_vertex(50, 150),
  1223. *polygon_place_vertex(50, 50),
  1224. ]
  1225. check_selector(event_sequence, expected_result, 1)
  1226. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1227. def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box):
  1228. tool = widgets.PolygonSelector(ax,
  1229. props=dict(color='b', alpha=0.2),
  1230. handle_props=dict(alpha=0.5),
  1231. draw_bounding_box=draw_bounding_box)
  1232. event_sequence = [
  1233. *polygon_place_vertex(50, 50),
  1234. *polygon_place_vertex(150, 50),
  1235. *polygon_place_vertex(50, 150),
  1236. *polygon_place_vertex(50, 50),
  1237. ]
  1238. for (etype, event_args) in event_sequence:
  1239. do_event(tool, etype, **event_args)
  1240. artist = tool._selection_artist
  1241. assert artist.get_color() == 'b'
  1242. assert artist.get_alpha() == 0.2
  1243. tool.set_props(color='r', alpha=0.3)
  1244. assert artist.get_color() == 'r'
  1245. assert artist.get_alpha() == 0.3
  1246. for artist in tool._handles_artists:
  1247. assert artist.get_color() == 'b'
  1248. assert artist.get_alpha() == 0.5
  1249. tool.set_handle_props(color='r', alpha=0.3)
  1250. for artist in tool._handles_artists:
  1251. assert artist.get_color() == 'r'
  1252. assert artist.get_alpha() == 0.3
  1253. @check_figures_equal(extensions=['png'])
  1254. def test_rect_visibility(fig_test, fig_ref):
  1255. # Check that requesting an invisible selector makes it invisible
  1256. ax_test = fig_test.subplots()
  1257. _ = fig_ref.subplots()
  1258. tool = widgets.RectangleSelector(ax_test, props={'visible': False})
  1259. tool.extents = (0.2, 0.8, 0.3, 0.7)
  1260. # Change the order that the extra point is inserted in
  1261. @pytest.mark.parametrize('idx', [1, 2, 3])
  1262. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1263. def test_polygon_selector_remove(idx, draw_bounding_box):
  1264. verts = [(50, 50), (150, 50), (50, 150)]
  1265. event_sequence = [polygon_place_vertex(*verts[0]),
  1266. polygon_place_vertex(*verts[1]),
  1267. polygon_place_vertex(*verts[2]),
  1268. # Finish the polygon
  1269. polygon_place_vertex(*verts[0])]
  1270. # Add an extra point
  1271. event_sequence.insert(idx, polygon_place_vertex(200, 200))
  1272. # Remove the extra point
  1273. event_sequence.append(polygon_remove_vertex(200, 200))
  1274. # Flatten list of lists
  1275. event_sequence = functools.reduce(operator.iadd, event_sequence, [])
  1276. check_polygon_selector(event_sequence, verts, 2,
  1277. draw_bounding_box=draw_bounding_box)
  1278. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1279. def test_polygon_selector_remove_first_point(draw_bounding_box):
  1280. verts = [(50, 50), (150, 50), (50, 150)]
  1281. event_sequence = [
  1282. *polygon_place_vertex(*verts[0]),
  1283. *polygon_place_vertex(*verts[1]),
  1284. *polygon_place_vertex(*verts[2]),
  1285. *polygon_place_vertex(*verts[0]),
  1286. *polygon_remove_vertex(*verts[0]),
  1287. ]
  1288. check_polygon_selector(event_sequence, verts[1:], 2,
  1289. draw_bounding_box=draw_bounding_box)
  1290. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1291. def test_polygon_selector_redraw(ax, draw_bounding_box):
  1292. verts = [(50, 50), (150, 50), (50, 150)]
  1293. event_sequence = [
  1294. *polygon_place_vertex(*verts[0]),
  1295. *polygon_place_vertex(*verts[1]),
  1296. *polygon_place_vertex(*verts[2]),
  1297. *polygon_place_vertex(*verts[0]),
  1298. # Polygon completed, now remove first two verts.
  1299. *polygon_remove_vertex(*verts[1]),
  1300. *polygon_remove_vertex(*verts[2]),
  1301. # At this point the tool should be reset so we can add more vertices.
  1302. *polygon_place_vertex(*verts[1]),
  1303. ]
  1304. tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box)
  1305. for (etype, event_args) in event_sequence:
  1306. do_event(tool, etype, **event_args)
  1307. # After removing two verts, only one remains, and the
  1308. # selector should be automatically resete
  1309. assert tool.verts == verts[0:2]
  1310. @pytest.mark.parametrize('draw_bounding_box', [False, True])
  1311. @check_figures_equal(extensions=['png'])
  1312. def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box):
  1313. verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)]
  1314. ax_test = fig_test.add_subplot()
  1315. tool_test = widgets.PolygonSelector(ax_test, draw_bounding_box=draw_bounding_box)
  1316. tool_test.verts = verts
  1317. assert tool_test.verts == verts
  1318. ax_ref = fig_ref.add_subplot()
  1319. tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box)
  1320. event_sequence = [
  1321. *polygon_place_vertex(*verts[0]),
  1322. *polygon_place_vertex(*verts[1]),
  1323. *polygon_place_vertex(*verts[2]),
  1324. *polygon_place_vertex(*verts[0]),
  1325. ]
  1326. for (etype, event_args) in event_sequence:
  1327. do_event(tool_ref, etype, **event_args)
  1328. def test_polygon_selector_box(ax):
  1329. # Create a diamond (adjusting axes lims s.t. the diamond lies within axes limits).
  1330. ax.set(xlim=(-10, 50), ylim=(-10, 50))
  1331. verts = [(20, 0), (0, 20), (20, 40), (40, 20)]
  1332. event_sequence = [
  1333. *polygon_place_vertex(*verts[0]),
  1334. *polygon_place_vertex(*verts[1]),
  1335. *polygon_place_vertex(*verts[2]),
  1336. *polygon_place_vertex(*verts[3]),
  1337. *polygon_place_vertex(*verts[0]),
  1338. ]
  1339. # Create selector
  1340. tool = widgets.PolygonSelector(ax, draw_bounding_box=True)
  1341. for (etype, event_args) in event_sequence:
  1342. do_event(tool, etype, **event_args)
  1343. # In order to trigger the correct callbacks, trigger events on the canvas
  1344. # instead of the individual tools
  1345. t = ax.transData
  1346. canvas = ax.get_figure(root=True).canvas
  1347. # Scale to half size using the top right corner of the bounding box
  1348. MouseEvent(
  1349. "button_press_event", canvas, *t.transform((40, 40)), 1)._process()
  1350. MouseEvent(
  1351. "motion_notify_event", canvas, *t.transform((20, 20)))._process()
  1352. MouseEvent(
  1353. "button_release_event", canvas, *t.transform((20, 20)), 1)._process()
  1354. np.testing.assert_allclose(
  1355. tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)])
  1356. # Move using the center of the bounding box
  1357. MouseEvent(
  1358. "button_press_event", canvas, *t.transform((10, 10)), 1)._process()
  1359. MouseEvent(
  1360. "motion_notify_event", canvas, *t.transform((30, 30)))._process()
  1361. MouseEvent(
  1362. "button_release_event", canvas, *t.transform((30, 30)), 1)._process()
  1363. np.testing.assert_allclose(
  1364. tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)])
  1365. # Remove a point from the polygon and check that the box extents update
  1366. np.testing.assert_allclose(
  1367. tool._box.extents, (20.0, 40.0, 20.0, 40.0))
  1368. MouseEvent(
  1369. "button_press_event", canvas, *t.transform((30, 20)), 3)._process()
  1370. MouseEvent(
  1371. "button_release_event", canvas, *t.transform((30, 20)), 3)._process()
  1372. np.testing.assert_allclose(
  1373. tool.verts, [(20, 30), (30, 40), (40, 30)])
  1374. np.testing.assert_allclose(
  1375. tool._box.extents, (20.0, 40.0, 30.0, 40.0))
  1376. def test_polygon_selector_clear_method(ax):
  1377. onselect = mock.Mock(spec=noop, return_value=None)
  1378. tool = widgets.PolygonSelector(ax, onselect)
  1379. for result in ([(50, 50), (150, 50), (50, 150), (50, 50)],
  1380. [(50, 50), (100, 50), (50, 150), (50, 50)]):
  1381. for x, y in result:
  1382. for etype, event_args in polygon_place_vertex(x, y):
  1383. do_event(tool, etype, **event_args)
  1384. artist = tool._selection_artist
  1385. assert tool._selection_completed
  1386. assert tool.get_visible()
  1387. assert artist.get_visible()
  1388. np.testing.assert_equal(artist.get_xydata(), result)
  1389. assert onselect.call_args == ((result[:-1],), {})
  1390. tool.clear()
  1391. assert not tool._selection_completed
  1392. np.testing.assert_equal(artist.get_xydata(), [(0, 0)])
  1393. @pytest.mark.parametrize("horizOn", [False, True])
  1394. @pytest.mark.parametrize("vertOn", [False, True])
  1395. def test_MultiCursor(horizOn, vertOn):
  1396. fig = plt.figure()
  1397. (ax1, ax3) = fig.subplots(2, sharex=True)
  1398. ax2 = plt.figure().subplots()
  1399. # useblit=false to avoid having to draw the figure to cache the renderer
  1400. multi = widgets.MultiCursor(
  1401. None, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn
  1402. )
  1403. # Only two of the axes should have a line drawn on them.
  1404. assert len(multi.vlines) == 2
  1405. assert len(multi.hlines) == 2
  1406. # mock a motion_notify_event
  1407. # Can't use `do_event` as that helper requires the widget
  1408. # to have a single .ax attribute.
  1409. event = mock_event(ax1, xdata=.5, ydata=.25)
  1410. multi.onmove(event)
  1411. # force a draw + draw event to exercise clear
  1412. fig.canvas.draw()
  1413. # the lines in the first two ax should both move
  1414. for l in multi.vlines:
  1415. assert l.get_xdata() == (.5, .5)
  1416. for l in multi.hlines:
  1417. assert l.get_ydata() == (.25, .25)
  1418. # The relevant lines get turned on after move.
  1419. assert len([line for line in multi.vlines if line.get_visible()]) == (
  1420. 2 if vertOn else 0)
  1421. assert len([line for line in multi.hlines if line.get_visible()]) == (
  1422. 2 if horizOn else 0)
  1423. # After toggling settings, the opposite lines should be visible after move.
  1424. multi.horizOn = not multi.horizOn
  1425. multi.vertOn = not multi.vertOn
  1426. event = mock_event(ax1, xdata=.5, ydata=.25)
  1427. multi.onmove(event)
  1428. assert len([line for line in multi.vlines if line.get_visible()]) == (
  1429. 0 if vertOn else 2)
  1430. assert len([line for line in multi.hlines if line.get_visible()]) == (
  1431. 0 if horizOn else 2)
  1432. # test a move event in an Axes not part of the MultiCursor
  1433. # the lines in ax1 and ax2 should not have moved.
  1434. event = mock_event(ax3, xdata=.75, ydata=.75)
  1435. multi.onmove(event)
  1436. for l in multi.vlines:
  1437. assert l.get_xdata() == (.5, .5)
  1438. for l in multi.hlines:
  1439. assert l.get_ydata() == (.25, .25)