test_widgets.py 66 KB

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