widgets.py 91 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712
  1. """
  2. GUI neutral widgets
  3. ===================
  4. Widgets that are designed to work for any of the GUI backends.
  5. All of these widgets require you to predefine a `matplotlib.axes.Axes`
  6. instance and pass that as the first parameter. Matplotlib doesn't try to
  7. be too smart with respect to layout -- you will have to figure out how
  8. wide and tall you want your Axes to be to accommodate your widget.
  9. """
  10. from contextlib import ExitStack
  11. import copy
  12. from numbers import Integral
  13. import numpy as np
  14. import matplotlib as mpl
  15. from . import cbook, colors, ticker
  16. from .lines import Line2D
  17. from .patches import Circle, Rectangle, Ellipse
  18. from .transforms import blended_transform_factory
  19. class LockDraw:
  20. """
  21. Some widgets, like the cursor, draw onto the canvas, and this is not
  22. desirable under all circumstances, like when the toolbar is in zoom-to-rect
  23. mode and drawing a rectangle. To avoid this, a widget can acquire a
  24. canvas' lock with ``canvas.widgetlock(widget)`` before drawing on the
  25. canvas; this will prevent other widgets from doing so at the same time (if
  26. they also try to acquire the lock first).
  27. """
  28. def __init__(self):
  29. self._owner = None
  30. def __call__(self, o):
  31. """Reserve the lock for *o*."""
  32. if not self.available(o):
  33. raise ValueError('already locked')
  34. self._owner = o
  35. def release(self, o):
  36. """Release the lock from *o*."""
  37. if not self.available(o):
  38. raise ValueError('you do not own this lock')
  39. self._owner = None
  40. def available(self, o):
  41. """Return whether drawing is available to *o*."""
  42. return not self.locked() or self.isowner(o)
  43. def isowner(self, o):
  44. """Return whether *o* owns this lock."""
  45. return self._owner is o
  46. def locked(self):
  47. """Return whether the lock is currently held by an owner."""
  48. return self._owner is not None
  49. class Widget:
  50. """
  51. Abstract base class for GUI neutral widgets
  52. """
  53. drawon = True
  54. eventson = True
  55. _active = True
  56. def set_active(self, active):
  57. """Set whether the widget is active."""
  58. self._active = active
  59. def get_active(self):
  60. """Get whether the widget is active."""
  61. return self._active
  62. # set_active is overridden by SelectorWidgets.
  63. active = property(get_active, set_active, doc="Is the widget active?")
  64. def ignore(self, event):
  65. """
  66. Return whether *event* should be ignored.
  67. This method should be called at the beginning of any event callback.
  68. """
  69. return not self.active
  70. class AxesWidget(Widget):
  71. """
  72. Widget connected to a single `~matplotlib.axes.Axes`.
  73. To guarantee that the widget remains responsive and not garbage-collected,
  74. a reference to the object should be maintained by the user.
  75. This is necessary because the callback registry
  76. maintains only weak-refs to the functions, which are member
  77. functions of the widget. If there are no references to the widget
  78. object it may be garbage collected which will disconnect the callbacks.
  79. Attributes
  80. ----------
  81. ax : `~matplotlib.axes.Axes`
  82. The parent axes for the widget.
  83. canvas : `~matplotlib.backend_bases.FigureCanvasBase`
  84. The parent figure canvas for the widget.
  85. active : bool
  86. If False, the widget does not respond to events.
  87. """
  88. def __init__(self, ax):
  89. self.ax = ax
  90. self.canvas = ax.figure.canvas
  91. self.cids = []
  92. def connect_event(self, event, callback):
  93. """
  94. Connect callback with an event.
  95. This should be used in lieu of ``figure.canvas.mpl_connect`` since this
  96. function stores callback ids for later clean up.
  97. """
  98. cid = self.canvas.mpl_connect(event, callback)
  99. self.cids.append(cid)
  100. def disconnect_events(self):
  101. """Disconnect all events created by this widget."""
  102. for c in self.cids:
  103. self.canvas.mpl_disconnect(c)
  104. class Button(AxesWidget):
  105. """
  106. A GUI neutral button.
  107. For the button to remain responsive you must keep a reference to it.
  108. Call `.on_clicked` to connect to the button.
  109. Attributes
  110. ----------
  111. ax
  112. The `matplotlib.axes.Axes` the button renders into.
  113. label
  114. A `matplotlib.text.Text` instance.
  115. color
  116. The color of the button when not hovering.
  117. hovercolor
  118. The color of the button when hovering.
  119. """
  120. def __init__(self, ax, label, image=None,
  121. color='0.85', hovercolor='0.95'):
  122. """
  123. Parameters
  124. ----------
  125. ax : `~matplotlib.axes.Axes`
  126. The `~.axes.Axes` instance the button will be placed into.
  127. label : str
  128. The button text.
  129. image : array-like or PIL Image
  130. The image to place in the button, if not *None*. The parameter is
  131. directly forwarded to `~matplotlib.axes.Axes.imshow`.
  132. color : color
  133. The color of the button when not activated.
  134. hovercolor : color
  135. The color of the button when the mouse is over it.
  136. """
  137. AxesWidget.__init__(self, ax)
  138. if image is not None:
  139. ax.imshow(image)
  140. self.label = ax.text(0.5, 0.5, label,
  141. verticalalignment='center',
  142. horizontalalignment='center',
  143. transform=ax.transAxes)
  144. self.cnt = 0
  145. self.observers = {}
  146. self.connect_event('button_press_event', self._click)
  147. self.connect_event('button_release_event', self._release)
  148. self.connect_event('motion_notify_event', self._motion)
  149. ax.set_navigate(False)
  150. ax.set_facecolor(color)
  151. ax.set_xticks([])
  152. ax.set_yticks([])
  153. self.color = color
  154. self.hovercolor = hovercolor
  155. def _click(self, event):
  156. if (self.ignore(event)
  157. or event.inaxes != self.ax
  158. or not self.eventson):
  159. return
  160. if event.canvas.mouse_grabber != self.ax:
  161. event.canvas.grab_mouse(self.ax)
  162. def _release(self, event):
  163. if (self.ignore(event)
  164. or event.canvas.mouse_grabber != self.ax):
  165. return
  166. event.canvas.release_mouse(self.ax)
  167. if (not self.eventson
  168. or event.inaxes != self.ax):
  169. return
  170. for cid, func in self.observers.items():
  171. func(event)
  172. def _motion(self, event):
  173. if self.ignore(event):
  174. return
  175. c = self.hovercolor if event.inaxes == self.ax else self.color
  176. if not colors.same_color(c, self.ax.get_facecolor()):
  177. self.ax.set_facecolor(c)
  178. if self.drawon:
  179. self.ax.figure.canvas.draw()
  180. def on_clicked(self, func):
  181. """
  182. Connect the callback function *func* to button click events.
  183. Returns a connection id, which can be used to disconnect the callback.
  184. """
  185. cid = self.cnt
  186. self.observers[cid] = func
  187. self.cnt += 1
  188. return cid
  189. def disconnect(self, cid):
  190. """Remove the callback function with connection id *cid*."""
  191. try:
  192. del self.observers[cid]
  193. except KeyError:
  194. pass
  195. class Slider(AxesWidget):
  196. """
  197. A slider representing a floating point range.
  198. Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to
  199. remain responsive you must maintain a reference to it. Call
  200. :meth:`on_changed` to connect to the slider event.
  201. Attributes
  202. ----------
  203. val : float
  204. Slider value.
  205. """
  206. def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
  207. closedmin=True, closedmax=True, slidermin=None,
  208. slidermax=None, dragging=True, valstep=None,
  209. orientation='horizontal', **kwargs):
  210. """
  211. Parameters
  212. ----------
  213. ax : Axes
  214. The Axes to put the slider in.
  215. label : str
  216. Slider label.
  217. valmin : float
  218. The minimum value of the slider.
  219. valmax : float
  220. The maximum value of the slider.
  221. valinit : float, default: 0.5
  222. The slider initial position.
  223. valfmt : str, default: None
  224. %-format string used to format the slider value. If None, a
  225. `.ScalarFormatter` is used instead.
  226. closedmin : bool, default: True
  227. Whether the slider interval is closed on the bottom.
  228. closedmax : bool, default: True
  229. Whether the slider interval is closed on the top.
  230. slidermin : Slider, default: None
  231. Do not allow the current slider to have a value less than
  232. the value of the Slider *slidermin*.
  233. slidermax : Slider, default: None
  234. Do not allow the current slider to have a value greater than
  235. the value of the Slider *slidermax*.
  236. dragging : bool, default: True
  237. If True the slider can be dragged by the mouse.
  238. valstep : float, default: None
  239. If given, the slider will snap to multiples of *valstep*.
  240. orientation : {'horizontal', 'vertical'}, default: 'horizontal'
  241. The orientation of the slider.
  242. Notes
  243. -----
  244. Additional kwargs are passed on to ``self.poly`` which is the
  245. `~matplotlib.patches.Rectangle` that draws the slider knob. See the
  246. `.Rectangle` documentation for valid property names (``facecolor``,
  247. ``edgecolor``, ``alpha``, etc.).
  248. """
  249. if ax.name == '3d':
  250. raise ValueError('Sliders cannot be added to 3D Axes')
  251. AxesWidget.__init__(self, ax)
  252. if slidermin is not None and not hasattr(slidermin, 'val'):
  253. raise ValueError("Argument slidermin ({}) has no 'val'"
  254. .format(type(slidermin)))
  255. if slidermax is not None and not hasattr(slidermax, 'val'):
  256. raise ValueError("Argument slidermax ({}) has no 'val'"
  257. .format(type(slidermax)))
  258. if orientation not in ['horizontal', 'vertical']:
  259. raise ValueError("Argument orientation ({}) must be either"
  260. "'horizontal' or 'vertical'".format(orientation))
  261. self.orientation = orientation
  262. self.closedmin = closedmin
  263. self.closedmax = closedmax
  264. self.slidermin = slidermin
  265. self.slidermax = slidermax
  266. self.drag_active = False
  267. self.valmin = valmin
  268. self.valmax = valmax
  269. self.valstep = valstep
  270. valinit = self._value_in_bounds(valinit)
  271. if valinit is None:
  272. valinit = valmin
  273. self.val = valinit
  274. self.valinit = valinit
  275. if orientation == 'vertical':
  276. self.poly = ax.axhspan(valmin, valinit, 0, 1, **kwargs)
  277. self.hline = ax.axhline(valinit, 0, 1, color='r', lw=1)
  278. else:
  279. self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs)
  280. self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1)
  281. if orientation == 'vertical':
  282. ax.set_ylim((valmin, valmax))
  283. axis = ax.yaxis
  284. else:
  285. ax.set_xlim((valmin, valmax))
  286. axis = ax.xaxis
  287. self.valfmt = valfmt
  288. self._fmt = axis.get_major_formatter()
  289. if not isinstance(self._fmt, ticker.ScalarFormatter):
  290. self._fmt = ticker.ScalarFormatter()
  291. self._fmt.set_axis(axis)
  292. self._fmt.set_useOffset(False) # No additive offset.
  293. self._fmt.set_useMathText(True) # x sign before multiplicative offset.
  294. ax.set_xticks([])
  295. ax.set_yticks([])
  296. ax.set_navigate(False)
  297. self.connect_event('button_press_event', self._update)
  298. self.connect_event('button_release_event', self._update)
  299. if dragging:
  300. self.connect_event('motion_notify_event', self._update)
  301. if orientation == 'vertical':
  302. self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes,
  303. verticalalignment='bottom',
  304. horizontalalignment='center')
  305. self.valtext = ax.text(0.5, -0.02, self._format(valinit),
  306. transform=ax.transAxes,
  307. verticalalignment='top',
  308. horizontalalignment='center')
  309. else:
  310. self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
  311. verticalalignment='center',
  312. horizontalalignment='right')
  313. self.valtext = ax.text(1.02, 0.5, self._format(valinit),
  314. transform=ax.transAxes,
  315. verticalalignment='center',
  316. horizontalalignment='left')
  317. self.cnt = 0
  318. self.observers = {}
  319. self.set_val(valinit)
  320. def _value_in_bounds(self, val):
  321. """Makes sure *val* is with given bounds."""
  322. if self.valstep:
  323. val = (self.valmin
  324. + round((val - self.valmin) / self.valstep) * self.valstep)
  325. if val <= self.valmin:
  326. if not self.closedmin:
  327. return
  328. val = self.valmin
  329. elif val >= self.valmax:
  330. if not self.closedmax:
  331. return
  332. val = self.valmax
  333. if self.slidermin is not None and val <= self.slidermin.val:
  334. if not self.closedmin:
  335. return
  336. val = self.slidermin.val
  337. if self.slidermax is not None and val >= self.slidermax.val:
  338. if not self.closedmax:
  339. return
  340. val = self.slidermax.val
  341. return val
  342. def _update(self, event):
  343. """Update the slider position."""
  344. if self.ignore(event) or event.button != 1:
  345. return
  346. if event.name == 'button_press_event' and event.inaxes == self.ax:
  347. self.drag_active = True
  348. event.canvas.grab_mouse(self.ax)
  349. if not self.drag_active:
  350. return
  351. elif ((event.name == 'button_release_event') or
  352. (event.name == 'button_press_event' and
  353. event.inaxes != self.ax)):
  354. self.drag_active = False
  355. event.canvas.release_mouse(self.ax)
  356. return
  357. if self.orientation == 'vertical':
  358. val = self._value_in_bounds(event.ydata)
  359. else:
  360. val = self._value_in_bounds(event.xdata)
  361. if val not in [None, self.val]:
  362. self.set_val(val)
  363. def _format(self, val):
  364. """Pretty-print *val*."""
  365. if self.valfmt is not None:
  366. return self.valfmt % val
  367. else:
  368. _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax])
  369. # fmt.get_offset is actually the multiplicative factor, if any.
  370. return s + self._fmt.get_offset()
  371. def set_val(self, val):
  372. """
  373. Set slider value to *val*
  374. Parameters
  375. ----------
  376. val : float
  377. """
  378. xy = self.poly.xy
  379. if self.orientation == 'vertical':
  380. xy[1] = 0, val
  381. xy[2] = 1, val
  382. else:
  383. xy[2] = val, 1
  384. xy[3] = val, 0
  385. self.poly.xy = xy
  386. self.valtext.set_text(self._format(val))
  387. if self.drawon:
  388. self.ax.figure.canvas.draw_idle()
  389. self.val = val
  390. if not self.eventson:
  391. return
  392. for cid, func in self.observers.items():
  393. func(val)
  394. def on_changed(self, func):
  395. """
  396. When the slider value is changed call *func* with the new
  397. slider value
  398. Parameters
  399. ----------
  400. func : callable
  401. Function to call when slider is changed.
  402. The function must accept a single float as its arguments.
  403. Returns
  404. -------
  405. int
  406. Connection id (which can be used to disconnect *func*)
  407. """
  408. cid = self.cnt
  409. self.observers[cid] = func
  410. self.cnt += 1
  411. return cid
  412. def disconnect(self, cid):
  413. """
  414. Remove the observer with connection id *cid*
  415. Parameters
  416. ----------
  417. cid : int
  418. Connection id of the observer to be removed
  419. """
  420. try:
  421. del self.observers[cid]
  422. except KeyError:
  423. pass
  424. def reset(self):
  425. """Reset the slider to the initial value"""
  426. if self.val != self.valinit:
  427. self.set_val(self.valinit)
  428. class CheckButtons(AxesWidget):
  429. r"""
  430. A GUI neutral set of check buttons.
  431. For the check buttons to remain responsive you must keep a
  432. reference to this object.
  433. Connect to the CheckButtons with the `.on_clicked` method.
  434. Attributes
  435. ----------
  436. ax : `~matplotlib.axes.Axes`
  437. The parent axes for the widget.
  438. labels : list of `.Text`
  439. rectangles : list of `.Rectangle`
  440. lines : list of (`.Line2D`, `.Line2D`) pairs
  441. List of lines for the x's in the check boxes. These lines exist for
  442. each box, but have ``set_visible(False)`` when its box is not checked.
  443. """
  444. def __init__(self, ax, labels, actives=None):
  445. """
  446. Add check buttons to `matplotlib.axes.Axes` instance *ax*
  447. Parameters
  448. ----------
  449. ax : `~matplotlib.axes.Axes`
  450. The parent axes for the widget.
  451. labels : list of str
  452. The labels of the check buttons.
  453. actives : list of bool, optional
  454. The initial check states of the buttons. The list must have the
  455. same length as *labels*. If not given, all buttons are unchecked.
  456. """
  457. AxesWidget.__init__(self, ax)
  458. ax.set_xticks([])
  459. ax.set_yticks([])
  460. ax.set_navigate(False)
  461. if actives is None:
  462. actives = [False] * len(labels)
  463. if len(labels) > 1:
  464. dy = 1. / (len(labels) + 1)
  465. ys = np.linspace(1 - dy, dy, len(labels))
  466. else:
  467. dy = 0.25
  468. ys = [0.5]
  469. axcolor = ax.get_facecolor()
  470. self.labels = []
  471. self.lines = []
  472. self.rectangles = []
  473. lineparams = {'color': 'k', 'linewidth': 1.25,
  474. 'transform': ax.transAxes, 'solid_capstyle': 'butt'}
  475. for y, label, active in zip(ys, labels, actives):
  476. t = ax.text(0.25, y, label, transform=ax.transAxes,
  477. horizontalalignment='left',
  478. verticalalignment='center')
  479. w, h = dy / 2, dy / 2
  480. x, y = 0.05, y - h / 2
  481. p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black',
  482. facecolor=axcolor, transform=ax.transAxes)
  483. l1 = Line2D([x, x + w], [y + h, y], **lineparams)
  484. l2 = Line2D([x, x + w], [y, y + h], **lineparams)
  485. l1.set_visible(active)
  486. l2.set_visible(active)
  487. self.labels.append(t)
  488. self.rectangles.append(p)
  489. self.lines.append((l1, l2))
  490. ax.add_patch(p)
  491. ax.add_line(l1)
  492. ax.add_line(l2)
  493. self.connect_event('button_press_event', self._clicked)
  494. self.cnt = 0
  495. self.observers = {}
  496. def _clicked(self, event):
  497. if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
  498. return
  499. for i, (p, t) in enumerate(zip(self.rectangles, self.labels)):
  500. if (t.get_window_extent().contains(event.x, event.y) or
  501. p.get_window_extent().contains(event.x, event.y)):
  502. self.set_active(i)
  503. break
  504. def set_active(self, index):
  505. """
  506. Toggle (activate or deactivate) a check button by index.
  507. Callbacks will be triggered if :attr:`eventson` is True.
  508. Parameters
  509. ----------
  510. index : int
  511. Index of the check button to toggle.
  512. Raises
  513. ------
  514. ValueError
  515. If *index* is invalid.
  516. """
  517. if not 0 <= index < len(self.labels):
  518. raise ValueError("Invalid CheckButton index: %d" % index)
  519. l1, l2 = self.lines[index]
  520. l1.set_visible(not l1.get_visible())
  521. l2.set_visible(not l2.get_visible())
  522. if self.drawon:
  523. self.ax.figure.canvas.draw()
  524. if not self.eventson:
  525. return
  526. for cid, func in self.observers.items():
  527. func(self.labels[index].get_text())
  528. def get_status(self):
  529. """
  530. Return a tuple of the status (True/False) of all of the check buttons.
  531. """
  532. return [l1.get_visible() for (l1, l2) in self.lines]
  533. def on_clicked(self, func):
  534. """
  535. Connect the callback function *func* to button click events.
  536. Returns a connection id, which can be used to disconnect the callback.
  537. """
  538. cid = self.cnt
  539. self.observers[cid] = func
  540. self.cnt += 1
  541. return cid
  542. def disconnect(self, cid):
  543. """Remove the observer with connection id *cid*."""
  544. try:
  545. del self.observers[cid]
  546. except KeyError:
  547. pass
  548. class TextBox(AxesWidget):
  549. """
  550. A GUI neutral text input box.
  551. For the text box to remain responsive you must keep a reference to it.
  552. Call `.on_text_change` to be updated whenever the text changes.
  553. Call `.on_submit` to be updated whenever the user hits enter or
  554. leaves the text entry field.
  555. Attributes
  556. ----------
  557. ax : `~matplotlib.axes.Axes`
  558. The parent axes for the widget.
  559. label : `.Text`
  560. color : color
  561. The color of the text box when not hovering.
  562. hovercolor : color
  563. The color of the text box when hovering.
  564. """
  565. @cbook.deprecated("3.3")
  566. @property
  567. def params_to_disable(self):
  568. return [key for key in mpl.rcParams if 'keymap' in key]
  569. def __init__(self, ax, label, initial='',
  570. color='.95', hovercolor='1', label_pad=.01):
  571. """
  572. Parameters
  573. ----------
  574. ax : `~matplotlib.axes.Axes`
  575. The `~.axes.Axes` instance the button will be placed into.
  576. label : str
  577. Label for this text box.
  578. initial : str
  579. Initial value in the text box.
  580. color : color
  581. The color of the box.
  582. hovercolor : color
  583. The color of the box when the mouse is over it.
  584. label_pad : float
  585. The distance between the label and the right side of the textbox.
  586. """
  587. AxesWidget.__init__(self, ax)
  588. self.DIST_FROM_LEFT = .05
  589. self.label = ax.text(
  590. -label_pad, 0.5, label, transform=ax.transAxes,
  591. verticalalignment='center', horizontalalignment='right')
  592. self.text_disp = self.ax.text(
  593. self.DIST_FROM_LEFT, 0.5, initial, transform=self.ax.transAxes,
  594. verticalalignment='center', horizontalalignment='left')
  595. self.cnt = 0
  596. self.change_observers = {}
  597. self.submit_observers = {}
  598. ax.set(
  599. xlim=(0, 1), ylim=(0, 1), # s.t. cursor appears from first click.
  600. navigate=False, facecolor=color,
  601. xticks=[], yticks=[])
  602. self.cursor_index = 0
  603. self.cursor = ax.vlines(0, 0, 0, visible=False,
  604. transform=mpl.transforms.IdentityTransform())
  605. self.connect_event('button_press_event', self._click)
  606. self.connect_event('button_release_event', self._release)
  607. self.connect_event('motion_notify_event', self._motion)
  608. self.connect_event('key_press_event', self._keypress)
  609. self.connect_event('resize_event', self._resize)
  610. self.color = color
  611. self.hovercolor = hovercolor
  612. self.capturekeystrokes = False
  613. @property
  614. def text(self):
  615. return self.text_disp.get_text()
  616. def _rendercursor(self):
  617. # this is a hack to figure out where the cursor should go.
  618. # we draw the text up to where the cursor should go, measure
  619. # and save its dimensions, draw the real text, then put the cursor
  620. # at the saved dimensions
  621. # This causes a single extra draw if the figure has never been rendered
  622. # yet, which should be fine as we're going to repeatedly re-render the
  623. # figure later anyways.
  624. if self.ax.figure._cachedRenderer is None:
  625. self.ax.figure.canvas.draw()
  626. text = self.text_disp.get_text() # Save value before overwriting it.
  627. widthtext = text[:self.cursor_index]
  628. self.text_disp.set_text(widthtext or ",")
  629. bb = self.text_disp.get_window_extent()
  630. if not widthtext: # Use the comma for the height, but keep width to 0.
  631. bb.x1 = bb.x0
  632. self.cursor.set(
  633. segments=[[(bb.x1, bb.y0), (bb.x1, bb.y1)]], visible=True)
  634. self.text_disp.set_text(text)
  635. self.ax.figure.canvas.draw()
  636. def _notify_submit_observers(self):
  637. if self.eventson:
  638. for cid, func in self.submit_observers.items():
  639. func(self.text)
  640. def _release(self, event):
  641. if self.ignore(event):
  642. return
  643. if event.canvas.mouse_grabber != self.ax:
  644. return
  645. event.canvas.release_mouse(self.ax)
  646. def _keypress(self, event):
  647. if self.ignore(event):
  648. return
  649. if self.capturekeystrokes:
  650. key = event.key
  651. text = self.text
  652. if len(key) == 1:
  653. text = (text[:self.cursor_index] + key +
  654. text[self.cursor_index:])
  655. self.cursor_index += 1
  656. elif key == "right":
  657. if self.cursor_index != len(text):
  658. self.cursor_index += 1
  659. elif key == "left":
  660. if self.cursor_index != 0:
  661. self.cursor_index -= 1
  662. elif key == "home":
  663. self.cursor_index = 0
  664. elif key == "end":
  665. self.cursor_index = len(text)
  666. elif key == "backspace":
  667. if self.cursor_index != 0:
  668. text = (text[:self.cursor_index - 1] +
  669. text[self.cursor_index:])
  670. self.cursor_index -= 1
  671. elif key == "delete":
  672. if self.cursor_index != len(self.text):
  673. text = (text[:self.cursor_index] +
  674. text[self.cursor_index + 1:])
  675. self.text_disp.set_text(text)
  676. self._rendercursor()
  677. self._notify_change_observers()
  678. if key == "enter":
  679. self._notify_submit_observers()
  680. def set_val(self, val):
  681. newval = str(val)
  682. if self.text == newval:
  683. return
  684. self.text_disp.set_text(newval)
  685. self._rendercursor()
  686. self._notify_change_observers()
  687. self._notify_submit_observers()
  688. def _notify_change_observers(self):
  689. if self.eventson:
  690. for cid, func in self.change_observers.items():
  691. func(self.text)
  692. def begin_typing(self, x):
  693. self.capturekeystrokes = True
  694. # Disable keypress shortcuts, which may otherwise cause the figure to
  695. # be saved, closed, etc., until the user stops typing. The way to
  696. # achieve this depends on whether toolmanager is in use.
  697. stack = ExitStack() # Register cleanup actions when user stops typing.
  698. self._on_stop_typing = stack.close
  699. toolmanager = getattr(
  700. self.ax.figure.canvas.manager, "toolmanager", None)
  701. if toolmanager is not None:
  702. # If using toolmanager, lock keypresses, and plan to release the
  703. # lock when typing stops.
  704. toolmanager.keypresslock(self)
  705. stack.push(toolmanager.keypresslock.release, self)
  706. else:
  707. # If not using toolmanager, disable all keypress-related rcParams.
  708. # Avoid spurious warnings if keymaps are getting deprecated.
  709. with cbook._suppress_matplotlib_deprecation_warning():
  710. stack.enter_context(mpl.rc_context(
  711. {k: [] for k in mpl.rcParams if k.startswith("keymap.")}))
  712. def stop_typing(self):
  713. if self.capturekeystrokes:
  714. self._on_stop_typing()
  715. self._on_stop_typing = None
  716. notifysubmit = True
  717. else:
  718. notifysubmit = False
  719. self.capturekeystrokes = False
  720. self.cursor.set_visible(False)
  721. self.ax.figure.canvas.draw()
  722. if notifysubmit:
  723. # Because _notify_submit_observers might throw an error in the
  724. # user's code, only call it once we've already done our cleanup.
  725. self._notify_submit_observers()
  726. def position_cursor(self, x):
  727. # now, we have to figure out where the cursor goes.
  728. # approximate it based on assuming all characters the same length
  729. if len(self.text) == 0:
  730. self.cursor_index = 0
  731. else:
  732. bb = self.text_disp.get_window_extent()
  733. ratio = np.clip((x - bb.x0) / bb.width, 0, 1)
  734. self.cursor_index = int(len(self.text) * ratio)
  735. self._rendercursor()
  736. def _click(self, event):
  737. if self.ignore(event):
  738. return
  739. if event.inaxes != self.ax:
  740. self.stop_typing()
  741. return
  742. if not self.eventson:
  743. return
  744. if event.canvas.mouse_grabber != self.ax:
  745. event.canvas.grab_mouse(self.ax)
  746. if not self.capturekeystrokes:
  747. self.begin_typing(event.x)
  748. self.position_cursor(event.x)
  749. def _resize(self, event):
  750. self.stop_typing()
  751. def _motion(self, event):
  752. if self.ignore(event):
  753. return
  754. c = self.hovercolor if event.inaxes == self.ax else self.color
  755. if not colors.same_color(c, self.ax.get_facecolor()):
  756. self.ax.set_facecolor(c)
  757. if self.drawon:
  758. self.ax.figure.canvas.draw()
  759. def on_text_change(self, func):
  760. """
  761. When the text changes, call this *func* with event.
  762. A connection id is returned which can be used to disconnect.
  763. """
  764. cid = self.cnt
  765. self.change_observers[cid] = func
  766. self.cnt += 1
  767. return cid
  768. def on_submit(self, func):
  769. """
  770. When the user hits enter or leaves the submission box, call this
  771. *func* with event.
  772. A connection id is returned which can be used to disconnect.
  773. """
  774. cid = self.cnt
  775. self.submit_observers[cid] = func
  776. self.cnt += 1
  777. return cid
  778. def disconnect(self, cid):
  779. """Remove the observer with connection id *cid*."""
  780. for reg in [self.change_observers, self.submit_observers]:
  781. try:
  782. del reg[cid]
  783. except KeyError:
  784. pass
  785. class RadioButtons(AxesWidget):
  786. """
  787. A GUI neutral radio button.
  788. For the buttons to remain responsive you must keep a reference to this
  789. object.
  790. Connect to the RadioButtons with the `.on_clicked` method.
  791. Attributes
  792. ----------
  793. ax : `~matplotlib.axes.Axes`
  794. The parent axes for the widget.
  795. activecolor : color
  796. The color of the selected button.
  797. labels : list of `.Text`
  798. The button labels.
  799. circles : list of `~.patches.Circle`
  800. The buttons.
  801. value_selected : str
  802. The label text of the currently selected button.
  803. """
  804. def __init__(self, ax, labels, active=0, activecolor='blue'):
  805. """
  806. Add radio buttons to an `~.axes.Axes`.
  807. Parameters
  808. ----------
  809. ax : `~matplotlib.axes.Axes`
  810. The axes to add the buttons to.
  811. labels : list of str
  812. The button labels.
  813. active : int
  814. The index of the initially selected button.
  815. activecolor : color
  816. The color of the selected button.
  817. """
  818. AxesWidget.__init__(self, ax)
  819. self.activecolor = activecolor
  820. self.value_selected = None
  821. ax.set_xticks([])
  822. ax.set_yticks([])
  823. ax.set_navigate(False)
  824. dy = 1. / (len(labels) + 1)
  825. ys = np.linspace(1 - dy, dy, len(labels))
  826. cnt = 0
  827. axcolor = ax.get_facecolor()
  828. # scale the radius of the circle with the spacing between each one
  829. circle_radius = dy / 2 - 0.01
  830. # default to hard-coded value if the radius becomes too large
  831. circle_radius = min(circle_radius, 0.05)
  832. self.labels = []
  833. self.circles = []
  834. for y, label in zip(ys, labels):
  835. t = ax.text(0.25, y, label, transform=ax.transAxes,
  836. horizontalalignment='left',
  837. verticalalignment='center')
  838. if cnt == active:
  839. self.value_selected = label
  840. facecolor = activecolor
  841. else:
  842. facecolor = axcolor
  843. p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
  844. facecolor=facecolor, transform=ax.transAxes)
  845. self.labels.append(t)
  846. self.circles.append(p)
  847. ax.add_patch(p)
  848. cnt += 1
  849. self.connect_event('button_press_event', self._clicked)
  850. self.cnt = 0
  851. self.observers = {}
  852. def _clicked(self, event):
  853. if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
  854. return
  855. pclicked = self.ax.transAxes.inverted().transform((event.x, event.y))
  856. distances = {}
  857. for i, (p, t) in enumerate(zip(self.circles, self.labels)):
  858. if (t.get_window_extent().contains(event.x, event.y)
  859. or np.linalg.norm(pclicked - p.center) < p.radius):
  860. distances[i] = np.linalg.norm(pclicked - p.center)
  861. if len(distances) > 0:
  862. closest = min(distances, key=distances.get)
  863. self.set_active(closest)
  864. def set_active(self, index):
  865. """
  866. Select button with number *index*.
  867. Callbacks will be triggered if :attr:`eventson` is True.
  868. """
  869. if 0 > index >= len(self.labels):
  870. raise ValueError("Invalid RadioButton index: %d" % index)
  871. self.value_selected = self.labels[index].get_text()
  872. for i, p in enumerate(self.circles):
  873. if i == index:
  874. color = self.activecolor
  875. else:
  876. color = self.ax.get_facecolor()
  877. p.set_facecolor(color)
  878. if self.drawon:
  879. self.ax.figure.canvas.draw()
  880. if not self.eventson:
  881. return
  882. for cid, func in self.observers.items():
  883. func(self.labels[index].get_text())
  884. def on_clicked(self, func):
  885. """
  886. Connect the callback function *func* to button click events.
  887. Returns a connection id, which can be used to disconnect the callback.
  888. """
  889. cid = self.cnt
  890. self.observers[cid] = func
  891. self.cnt += 1
  892. return cid
  893. def disconnect(self, cid):
  894. """Remove the observer with connection id *cid*."""
  895. try:
  896. del self.observers[cid]
  897. except KeyError:
  898. pass
  899. class SubplotTool(Widget):
  900. """
  901. A tool to adjust the subplot params of a `matplotlib.figure.Figure`.
  902. """
  903. def __init__(self, targetfig, toolfig):
  904. """
  905. Parameters
  906. ----------
  907. targetfig : `.Figure`
  908. The figure instance to adjust.
  909. toolfig : `.Figure`
  910. The figure instance to embed the subplot tool into.
  911. """
  912. self.targetfig = targetfig
  913. toolfig.subplots_adjust(left=0.2, right=0.9)
  914. toolfig.suptitle("Click on slider to adjust subplot param")
  915. self._sliders = []
  916. names = ["left", "bottom", "right", "top", "wspace", "hspace"]
  917. # The last subplot, removed below, keeps space for the "Reset" button.
  918. for name, ax in zip(names, toolfig.subplots(len(names) + 1)):
  919. ax.set_navigate(False)
  920. slider = Slider(ax, name,
  921. 0, 1, getattr(targetfig.subplotpars, name))
  922. slider.on_changed(self._on_slider_changed)
  923. self._sliders.append(slider)
  924. toolfig.axes[-1].remove()
  925. (self.sliderleft, self.sliderbottom, self.sliderright, self.slidertop,
  926. self.sliderwspace, self.sliderhspace) = self._sliders
  927. for slider in [self.sliderleft, self.sliderbottom,
  928. self.sliderwspace, self.sliderhspace]:
  929. slider.closedmax = False
  930. for slider in [self.sliderright, self.slidertop]:
  931. slider.closedmin = False
  932. # constraints
  933. self.sliderleft.slidermax = self.sliderright
  934. self.sliderright.slidermin = self.sliderleft
  935. self.sliderbottom.slidermax = self.slidertop
  936. self.slidertop.slidermin = self.sliderbottom
  937. bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075])
  938. self.buttonreset = Button(bax, 'Reset')
  939. # During reset there can be a temporary invalid state depending on the
  940. # order of the reset so we turn off validation for the resetting
  941. with cbook._setattr_cm(toolfig.subplotpars, validate=False):
  942. self.buttonreset.on_clicked(self._on_reset)
  943. def _on_slider_changed(self, _):
  944. self.targetfig.subplots_adjust(
  945. **{slider.label.get_text(): slider.val
  946. for slider in self._sliders})
  947. if self.drawon:
  948. self.targetfig.canvas.draw()
  949. def _on_reset(self, event):
  950. with ExitStack() as stack:
  951. # Temporarily disable drawing on self and self's sliders.
  952. stack.enter_context(cbook._setattr_cm(self, drawon=False))
  953. for slider in self._sliders:
  954. stack.enter_context(cbook._setattr_cm(slider, drawon=False))
  955. # Reset the slider to the initial position.
  956. for slider in self._sliders:
  957. slider.reset()
  958. # Draw the canvas.
  959. if self.drawon:
  960. event.canvas.draw()
  961. self.targetfig.canvas.draw()
  962. axleft = cbook.deprecated("3.3", name="axleft")(
  963. property(lambda self: self.sliderleft.ax))
  964. axright = cbook.deprecated("3.3", name="axright")(
  965. property(lambda self: self.sliderright.ax))
  966. axbottom = cbook.deprecated("3.3", name="axbottom")(
  967. property(lambda self: self.sliderbottom.ax))
  968. axtop = cbook.deprecated("3.3", name="axtop")(
  969. property(lambda self: self.slidertop.ax))
  970. axwspace = cbook.deprecated("3.3", name="axwspace")(
  971. property(lambda self: self.sliderwspace.ax))
  972. axhspace = cbook.deprecated("3.3", name="axhspace")(
  973. property(lambda self: self.sliderhspace.ax))
  974. @cbook.deprecated("3.3")
  975. def funcleft(self, val):
  976. self.targetfig.subplots_adjust(left=val)
  977. if self.drawon:
  978. self.targetfig.canvas.draw()
  979. @cbook.deprecated("3.3")
  980. def funcright(self, val):
  981. self.targetfig.subplots_adjust(right=val)
  982. if self.drawon:
  983. self.targetfig.canvas.draw()
  984. @cbook.deprecated("3.3")
  985. def funcbottom(self, val):
  986. self.targetfig.subplots_adjust(bottom=val)
  987. if self.drawon:
  988. self.targetfig.canvas.draw()
  989. @cbook.deprecated("3.3")
  990. def functop(self, val):
  991. self.targetfig.subplots_adjust(top=val)
  992. if self.drawon:
  993. self.targetfig.canvas.draw()
  994. @cbook.deprecated("3.3")
  995. def funcwspace(self, val):
  996. self.targetfig.subplots_adjust(wspace=val)
  997. if self.drawon:
  998. self.targetfig.canvas.draw()
  999. @cbook.deprecated("3.3")
  1000. def funchspace(self, val):
  1001. self.targetfig.subplots_adjust(hspace=val)
  1002. if self.drawon:
  1003. self.targetfig.canvas.draw()
  1004. class Cursor(AxesWidget):
  1005. """
  1006. A crosshair cursor that spans the axes and moves with mouse cursor.
  1007. For the cursor to remain responsive you must keep a reference to it.
  1008. Parameters
  1009. ----------
  1010. ax : `matplotlib.axes.Axes`
  1011. The `~.axes.Axes` to attach the cursor to.
  1012. horizOn : bool, default: True
  1013. Whether to draw the horizontal line.
  1014. vertOn : bool, default: True
  1015. Whether to draw the vertical line.
  1016. useblit : bool, default: False
  1017. Use blitting for faster drawing if supported by the backend.
  1018. Other Parameters
  1019. ----------------
  1020. **lineprops
  1021. `.Line2D` properties that control the appearance of the lines.
  1022. See also `~.Axes.axhline`.
  1023. Examples
  1024. --------
  1025. See :doc:`/gallery/widgets/cursor`.
  1026. """
  1027. def __init__(self, ax, horizOn=True, vertOn=True, useblit=False,
  1028. **lineprops):
  1029. AxesWidget.__init__(self, ax)
  1030. self.connect_event('motion_notify_event', self.onmove)
  1031. self.connect_event('draw_event', self.clear)
  1032. self.visible = True
  1033. self.horizOn = horizOn
  1034. self.vertOn = vertOn
  1035. self.useblit = useblit and self.canvas.supports_blit
  1036. if self.useblit:
  1037. lineprops['animated'] = True
  1038. self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
  1039. self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
  1040. self.background = None
  1041. self.needclear = False
  1042. def clear(self, event):
  1043. """Internal event handler to clear the cursor."""
  1044. if self.ignore(event):
  1045. return
  1046. if self.useblit:
  1047. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  1048. self.linev.set_visible(False)
  1049. self.lineh.set_visible(False)
  1050. def onmove(self, event):
  1051. """Internal event handler to draw the cursor when the mouse moves."""
  1052. if self.ignore(event):
  1053. return
  1054. if not self.canvas.widgetlock.available(self):
  1055. return
  1056. if event.inaxes != self.ax:
  1057. self.linev.set_visible(False)
  1058. self.lineh.set_visible(False)
  1059. if self.needclear:
  1060. self.canvas.draw()
  1061. self.needclear = False
  1062. return
  1063. self.needclear = True
  1064. if not self.visible:
  1065. return
  1066. self.linev.set_xdata((event.xdata, event.xdata))
  1067. self.lineh.set_ydata((event.ydata, event.ydata))
  1068. self.linev.set_visible(self.visible and self.vertOn)
  1069. self.lineh.set_visible(self.visible and self.horizOn)
  1070. self._update()
  1071. def _update(self):
  1072. if self.useblit:
  1073. if self.background is not None:
  1074. self.canvas.restore_region(self.background)
  1075. self.ax.draw_artist(self.linev)
  1076. self.ax.draw_artist(self.lineh)
  1077. self.canvas.blit(self.ax.bbox)
  1078. else:
  1079. self.canvas.draw_idle()
  1080. return False
  1081. class MultiCursor(Widget):
  1082. """
  1083. Provide a vertical (default) and/or horizontal line cursor shared between
  1084. multiple axes.
  1085. For the cursor to remain responsive you must keep a reference to it.
  1086. Example usage::
  1087. from matplotlib.widgets import MultiCursor
  1088. import matplotlib.pyplot as plt
  1089. import numpy as np
  1090. fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
  1091. t = np.arange(0.0, 2.0, 0.01)
  1092. ax1.plot(t, np.sin(2*np.pi*t))
  1093. ax2.plot(t, np.sin(4*np.pi*t))
  1094. multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1,
  1095. horizOn=False, vertOn=True)
  1096. plt.show()
  1097. """
  1098. def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True,
  1099. **lineprops):
  1100. self.canvas = canvas
  1101. self.axes = axes
  1102. self.horizOn = horizOn
  1103. self.vertOn = vertOn
  1104. xmin, xmax = axes[-1].get_xlim()
  1105. ymin, ymax = axes[-1].get_ylim()
  1106. xmid = 0.5 * (xmin + xmax)
  1107. ymid = 0.5 * (ymin + ymax)
  1108. self.visible = True
  1109. self.useblit = useblit and self.canvas.supports_blit
  1110. self.background = None
  1111. self.needclear = False
  1112. if self.useblit:
  1113. lineprops['animated'] = True
  1114. if vertOn:
  1115. self.vlines = [ax.axvline(xmid, visible=False, **lineprops)
  1116. for ax in axes]
  1117. else:
  1118. self.vlines = []
  1119. if horizOn:
  1120. self.hlines = [ax.axhline(ymid, visible=False, **lineprops)
  1121. for ax in axes]
  1122. else:
  1123. self.hlines = []
  1124. self.connect()
  1125. def connect(self):
  1126. """Connect events."""
  1127. self._cidmotion = self.canvas.mpl_connect('motion_notify_event',
  1128. self.onmove)
  1129. self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear)
  1130. def disconnect(self):
  1131. """Disconnect events."""
  1132. self.canvas.mpl_disconnect(self._cidmotion)
  1133. self.canvas.mpl_disconnect(self._ciddraw)
  1134. def clear(self, event):
  1135. """Clear the cursor."""
  1136. if self.ignore(event):
  1137. return
  1138. if self.useblit:
  1139. self.background = (
  1140. self.canvas.copy_from_bbox(self.canvas.figure.bbox))
  1141. for line in self.vlines + self.hlines:
  1142. line.set_visible(False)
  1143. def onmove(self, event):
  1144. if self.ignore(event):
  1145. return
  1146. if event.inaxes is None:
  1147. return
  1148. if not self.canvas.widgetlock.available(self):
  1149. return
  1150. self.needclear = True
  1151. if not self.visible:
  1152. return
  1153. if self.vertOn:
  1154. for line in self.vlines:
  1155. line.set_xdata((event.xdata, event.xdata))
  1156. line.set_visible(self.visible)
  1157. if self.horizOn:
  1158. for line in self.hlines:
  1159. line.set_ydata((event.ydata, event.ydata))
  1160. line.set_visible(self.visible)
  1161. self._update()
  1162. def _update(self):
  1163. if self.useblit:
  1164. if self.background is not None:
  1165. self.canvas.restore_region(self.background)
  1166. if self.vertOn:
  1167. for ax, line in zip(self.axes, self.vlines):
  1168. ax.draw_artist(line)
  1169. if self.horizOn:
  1170. for ax, line in zip(self.axes, self.hlines):
  1171. ax.draw_artist(line)
  1172. self.canvas.blit()
  1173. else:
  1174. self.canvas.draw_idle()
  1175. class _SelectorWidget(AxesWidget):
  1176. def __init__(self, ax, onselect, useblit=False, button=None,
  1177. state_modifier_keys=None):
  1178. AxesWidget.__init__(self, ax)
  1179. self.visible = True
  1180. self.onselect = onselect
  1181. self.useblit = useblit and self.canvas.supports_blit
  1182. self.connect_default_events()
  1183. self.state_modifier_keys = dict(move=' ', clear='escape',
  1184. square='shift', center='control')
  1185. self.state_modifier_keys.update(state_modifier_keys or {})
  1186. self.background = None
  1187. self.artists = []
  1188. if isinstance(button, Integral):
  1189. self.validButtons = [button]
  1190. else:
  1191. self.validButtons = button
  1192. # will save the data (position at mouseclick)
  1193. self.eventpress = None
  1194. # will save the data (pos. at mouserelease)
  1195. self.eventrelease = None
  1196. self._prev_event = None
  1197. self.state = set()
  1198. def set_active(self, active):
  1199. AxesWidget.set_active(self, active)
  1200. if active:
  1201. self.update_background(None)
  1202. def update_background(self, event):
  1203. """Force an update of the background."""
  1204. # If you add a call to `ignore` here, you'll want to check edge case:
  1205. # `release` can call a draw event even when `ignore` is True.
  1206. if self.useblit:
  1207. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  1208. def connect_default_events(self):
  1209. """Connect the major canvas events to methods."""
  1210. self.connect_event('motion_notify_event', self.onmove)
  1211. self.connect_event('button_press_event', self.press)
  1212. self.connect_event('button_release_event', self.release)
  1213. self.connect_event('draw_event', self.update_background)
  1214. self.connect_event('key_press_event', self.on_key_press)
  1215. self.connect_event('key_release_event', self.on_key_release)
  1216. self.connect_event('scroll_event', self.on_scroll)
  1217. def ignore(self, event):
  1218. # docstring inherited
  1219. if not self.active or not self.ax.get_visible():
  1220. return True
  1221. # If canvas was locked
  1222. if not self.canvas.widgetlock.available(self):
  1223. return True
  1224. if not hasattr(event, 'button'):
  1225. event.button = None
  1226. # Only do rectangle selection if event was triggered
  1227. # with a desired button
  1228. if (self.validButtons is not None
  1229. and event.button not in self.validButtons):
  1230. return True
  1231. # If no button was pressed yet ignore the event if it was out
  1232. # of the axes
  1233. if self.eventpress is None:
  1234. return event.inaxes != self.ax
  1235. # If a button was pressed, check if the release-button is the same.
  1236. if event.button == self.eventpress.button:
  1237. return False
  1238. # If a button was pressed, check if the release-button is the same.
  1239. return (event.inaxes != self.ax or
  1240. event.button != self.eventpress.button)
  1241. def update(self):
  1242. """Draw using blit() or draw_idle(), depending on ``self.useblit``."""
  1243. if not self.ax.get_visible():
  1244. return False
  1245. if self.useblit:
  1246. if self.background is not None:
  1247. self.canvas.restore_region(self.background)
  1248. for artist in self.artists:
  1249. self.ax.draw_artist(artist)
  1250. self.canvas.blit(self.ax.bbox)
  1251. else:
  1252. self.canvas.draw_idle()
  1253. return False
  1254. def _get_data(self, event):
  1255. """Get the xdata and ydata for event, with limits."""
  1256. if event.xdata is None:
  1257. return None, None
  1258. xdata = np.clip(event.xdata, *self.ax.get_xbound())
  1259. ydata = np.clip(event.ydata, *self.ax.get_ybound())
  1260. return xdata, ydata
  1261. def _clean_event(self, event):
  1262. """
  1263. Preprocess an event:
  1264. - Replace *event* by the previous event if *event* has no ``xdata``.
  1265. - Clip ``xdata`` and ``ydata`` to the axes limits.
  1266. - Update the previous event.
  1267. """
  1268. if event.xdata is None:
  1269. event = self._prev_event
  1270. else:
  1271. event = copy.copy(event)
  1272. event.xdata, event.ydata = self._get_data(event)
  1273. self._prev_event = event
  1274. return event
  1275. def press(self, event):
  1276. """Button press handler and validator."""
  1277. if not self.ignore(event):
  1278. event = self._clean_event(event)
  1279. self.eventpress = event
  1280. self._prev_event = event
  1281. key = event.key or ''
  1282. key = key.replace('ctrl', 'control')
  1283. # move state is locked in on a button press
  1284. if key == self.state_modifier_keys['move']:
  1285. self.state.add('move')
  1286. self._press(event)
  1287. return True
  1288. return False
  1289. def _press(self, event):
  1290. """Button press handler."""
  1291. def release(self, event):
  1292. """Button release event handler and validator."""
  1293. if not self.ignore(event) and self.eventpress:
  1294. event = self._clean_event(event)
  1295. self.eventrelease = event
  1296. self._release(event)
  1297. self.eventpress = None
  1298. self.eventrelease = None
  1299. self.state.discard('move')
  1300. return True
  1301. return False
  1302. def _release(self, event):
  1303. """Button release event handler."""
  1304. def onmove(self, event):
  1305. """Cursor move event handler and validator."""
  1306. if not self.ignore(event) and self.eventpress:
  1307. event = self._clean_event(event)
  1308. self._onmove(event)
  1309. return True
  1310. return False
  1311. def _onmove(self, event):
  1312. """Cursor move event handler."""
  1313. def on_scroll(self, event):
  1314. """Mouse scroll event handler and validator."""
  1315. if not self.ignore(event):
  1316. self._on_scroll(event)
  1317. def _on_scroll(self, event):
  1318. """Mouse scroll event handler."""
  1319. def on_key_press(self, event):
  1320. """Key press event handler and validator for all selection widgets."""
  1321. if self.active:
  1322. key = event.key or ''
  1323. key = key.replace('ctrl', 'control')
  1324. if key == self.state_modifier_keys['clear']:
  1325. for artist in self.artists:
  1326. artist.set_visible(False)
  1327. self.update()
  1328. return
  1329. for (state, modifier) in self.state_modifier_keys.items():
  1330. if modifier in key:
  1331. self.state.add(state)
  1332. self._on_key_press(event)
  1333. def _on_key_press(self, event):
  1334. """Key press event handler - for widget-specific key press actions."""
  1335. def on_key_release(self, event):
  1336. """Key release event handler and validator."""
  1337. if self.active:
  1338. key = event.key or ''
  1339. for (state, modifier) in self.state_modifier_keys.items():
  1340. if modifier in key:
  1341. self.state.discard(state)
  1342. self._on_key_release(event)
  1343. def _on_key_release(self, event):
  1344. """Key release event handler."""
  1345. def set_visible(self, visible):
  1346. """Set the visibility of our artists."""
  1347. self.visible = visible
  1348. for artist in self.artists:
  1349. artist.set_visible(visible)
  1350. class SpanSelector(_SelectorWidget):
  1351. """
  1352. Visually select a min/max range on a single axis and call a function with
  1353. those values.
  1354. To guarantee that the selector remains responsive, keep a reference to it.
  1355. In order to turn off the SpanSelector, set ``span_selector.active`` to
  1356. False. To turn it back on, set it to True.
  1357. Parameters
  1358. ----------
  1359. ax : `matplotlib.axes.Axes`
  1360. onselect : func(min, max), min/max are floats
  1361. direction : {"horizontal", "vertical"}
  1362. The direction along which to draw the span selector.
  1363. minspan : float, default: None
  1364. If selection is less than *minspan*, do not call *onselect*.
  1365. useblit : bool, default: False
  1366. If True, use the backend-dependent blitting features for faster
  1367. canvas updates.
  1368. rectprops : dict, default: None
  1369. Dictionary of `matplotlib.patches.Patch` properties.
  1370. onmove_callback : func(min, max), min/max are floats, default: None
  1371. Called on mouse move while the span is being selected.
  1372. span_stays : bool, default: False
  1373. If True, the span stays visible after the mouse is released.
  1374. button : `.MouseButton` or list of `.MouseButton`
  1375. The mouse buttons which activate the span selector.
  1376. Examples
  1377. --------
  1378. >>> import matplotlib.pyplot as plt
  1379. >>> import matplotlib.widgets as mwidgets
  1380. >>> fig, ax = plt.subplots()
  1381. >>> ax.plot([1, 2, 3], [10, 50, 100])
  1382. >>> def onselect(vmin, vmax):
  1383. ... print(vmin, vmax)
  1384. >>> rectprops = dict(facecolor='blue', alpha=0.5)
  1385. >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal',
  1386. ... rectprops=rectprops)
  1387. >>> fig.show()
  1388. See also: :doc:`/gallery/widgets/span_selector`
  1389. """
  1390. def __init__(self, ax, onselect, direction, minspan=None, useblit=False,
  1391. rectprops=None, onmove_callback=None, span_stays=False,
  1392. button=None):
  1393. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  1394. button=button)
  1395. if rectprops is None:
  1396. rectprops = dict(facecolor='red', alpha=0.5)
  1397. rectprops['animated'] = self.useblit
  1398. cbook._check_in_list(['horizontal', 'vertical'], direction=direction)
  1399. self.direction = direction
  1400. self.rect = None
  1401. self.pressv = None
  1402. self.rectprops = rectprops
  1403. self.onmove_callback = onmove_callback
  1404. self.minspan = minspan
  1405. self.span_stays = span_stays
  1406. # Needed when dragging out of axes
  1407. self.prev = (0, 0)
  1408. # Reset canvas so that `new_axes` connects events.
  1409. self.canvas = None
  1410. self.new_axes(ax)
  1411. def new_axes(self, ax):
  1412. """Set SpanSelector to operate on a new Axes."""
  1413. self.ax = ax
  1414. if self.canvas is not ax.figure.canvas:
  1415. if self.canvas is not None:
  1416. self.disconnect_events()
  1417. self.canvas = ax.figure.canvas
  1418. self.connect_default_events()
  1419. if self.direction == 'horizontal':
  1420. trans = blended_transform_factory(self.ax.transData,
  1421. self.ax.transAxes)
  1422. w, h = 0, 1
  1423. else:
  1424. trans = blended_transform_factory(self.ax.transAxes,
  1425. self.ax.transData)
  1426. w, h = 1, 0
  1427. self.rect = Rectangle((0, 0), w, h,
  1428. transform=trans,
  1429. visible=False,
  1430. **self.rectprops)
  1431. if self.span_stays:
  1432. self.stay_rect = Rectangle((0, 0), w, h,
  1433. transform=trans,
  1434. visible=False,
  1435. **self.rectprops)
  1436. self.stay_rect.set_animated(False)
  1437. self.ax.add_patch(self.stay_rect)
  1438. self.ax.add_patch(self.rect)
  1439. self.artists = [self.rect]
  1440. def ignore(self, event):
  1441. # docstring inherited
  1442. return _SelectorWidget.ignore(self, event) or not self.visible
  1443. def _press(self, event):
  1444. """on button press event"""
  1445. self.rect.set_visible(self.visible)
  1446. if self.span_stays:
  1447. self.stay_rect.set_visible(False)
  1448. # really force a draw so that the stay rect is not in
  1449. # the blit background
  1450. if self.useblit:
  1451. self.canvas.draw()
  1452. xdata, ydata = self._get_data(event)
  1453. if self.direction == 'horizontal':
  1454. self.pressv = xdata
  1455. else:
  1456. self.pressv = ydata
  1457. self._set_span_xy(event)
  1458. return False
  1459. def _release(self, event):
  1460. """on button release event"""
  1461. if self.pressv is None:
  1462. return
  1463. self.rect.set_visible(False)
  1464. if self.span_stays:
  1465. self.stay_rect.set_x(self.rect.get_x())
  1466. self.stay_rect.set_y(self.rect.get_y())
  1467. self.stay_rect.set_width(self.rect.get_width())
  1468. self.stay_rect.set_height(self.rect.get_height())
  1469. self.stay_rect.set_visible(True)
  1470. self.canvas.draw_idle()
  1471. vmin = self.pressv
  1472. xdata, ydata = self._get_data(event)
  1473. if self.direction == 'horizontal':
  1474. vmax = xdata or self.prev[0]
  1475. else:
  1476. vmax = ydata or self.prev[1]
  1477. if vmin > vmax:
  1478. vmin, vmax = vmax, vmin
  1479. span = vmax - vmin
  1480. if self.minspan is not None and span < self.minspan:
  1481. return
  1482. self.onselect(vmin, vmax)
  1483. self.pressv = None
  1484. return False
  1485. def _onmove(self, event):
  1486. """on motion notify event"""
  1487. if self.pressv is None:
  1488. return
  1489. self._set_span_xy(event)
  1490. if self.onmove_callback is not None:
  1491. vmin = self.pressv
  1492. xdata, ydata = self._get_data(event)
  1493. if self.direction == 'horizontal':
  1494. vmax = xdata or self.prev[0]
  1495. else:
  1496. vmax = ydata or self.prev[1]
  1497. if vmin > vmax:
  1498. vmin, vmax = vmax, vmin
  1499. self.onmove_callback(vmin, vmax)
  1500. self.update()
  1501. return False
  1502. def _set_span_xy(self, event):
  1503. """Set the span coordinates."""
  1504. x, y = self._get_data(event)
  1505. if x is None:
  1506. return
  1507. self.prev = x, y
  1508. if self.direction == 'horizontal':
  1509. v = x
  1510. else:
  1511. v = y
  1512. minv, maxv = v, self.pressv
  1513. if minv > maxv:
  1514. minv, maxv = maxv, minv
  1515. if self.direction == 'horizontal':
  1516. self.rect.set_x(minv)
  1517. self.rect.set_width(maxv - minv)
  1518. else:
  1519. self.rect.set_y(minv)
  1520. self.rect.set_height(maxv - minv)
  1521. class ToolHandles:
  1522. """
  1523. Control handles for canvas tools.
  1524. Parameters
  1525. ----------
  1526. ax : `matplotlib.axes.Axes`
  1527. Matplotlib axes where tool handles are displayed.
  1528. x, y : 1D arrays
  1529. Coordinates of control handles.
  1530. marker : str
  1531. Shape of marker used to display handle. See `matplotlib.pyplot.plot`.
  1532. marker_props : dict
  1533. Additional marker properties. See `matplotlib.lines.Line2D`.
  1534. """
  1535. def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True):
  1536. self.ax = ax
  1537. props = dict(marker=marker, markersize=7, mfc='w', ls='none',
  1538. alpha=0.5, visible=False, label='_nolegend_')
  1539. props.update(marker_props if marker_props is not None else {})
  1540. self._markers = Line2D(x, y, animated=useblit, **props)
  1541. self.ax.add_line(self._markers)
  1542. self.artist = self._markers
  1543. @property
  1544. def x(self):
  1545. return self._markers.get_xdata()
  1546. @property
  1547. def y(self):
  1548. return self._markers.get_ydata()
  1549. def set_data(self, pts, y=None):
  1550. """Set x and y positions of handles"""
  1551. if y is not None:
  1552. x = pts
  1553. pts = np.array([x, y])
  1554. self._markers.set_data(pts)
  1555. def set_visible(self, val):
  1556. self._markers.set_visible(val)
  1557. def set_animated(self, val):
  1558. self._markers.set_animated(val)
  1559. def closest(self, x, y):
  1560. """Return index and pixel distance to closest index."""
  1561. pts = np.column_stack([self.x, self.y])
  1562. # Transform data coordinates to pixel coordinates.
  1563. pts = self.ax.transData.transform(pts)
  1564. diff = pts - [x, y]
  1565. dist = np.hypot(*diff.T)
  1566. min_index = np.argmin(dist)
  1567. return min_index, dist[min_index]
  1568. class RectangleSelector(_SelectorWidget):
  1569. """
  1570. Select a rectangular region of an axes.
  1571. For the cursor to remain responsive you must keep a reference to it.
  1572. Examples
  1573. --------
  1574. :doc:`/gallery/widgets/rectangle_selector`
  1575. """
  1576. _shape_klass = Rectangle
  1577. def __init__(self, ax, onselect, drawtype='box',
  1578. minspanx=0, minspany=0, useblit=False,
  1579. lineprops=None, rectprops=None, spancoords='data',
  1580. button=None, maxdist=10, marker_props=None,
  1581. interactive=False, state_modifier_keys=None):
  1582. r"""
  1583. Parameters
  1584. ----------
  1585. ax : `~matplotlib.axes.Axes`
  1586. The parent axes for the widget.
  1587. onselect : function
  1588. A callback function that is called after a selection is completed.
  1589. It must have the signature::
  1590. def onselect(eclick: MouseEvent, erelease: MouseEvent)
  1591. where *eclick* and *erelease* are the mouse click and release
  1592. `.MouseEvent`\s that start and complete the selection.
  1593. drawtype : {"box", "line", "none"}, default: "box"
  1594. Whether to draw the full rectangle box, the diagonal line of the
  1595. rectangle, or nothing at all.
  1596. minspanx : float, default: 0
  1597. Selections with an x-span less than *minspanx* are ignored.
  1598. minspany : float, default: 0
  1599. Selections with an y-span less than *minspany* are ignored.
  1600. useblit : bool, default: False
  1601. Whether to use blitting for faster drawing (if supported by the
  1602. backend).
  1603. lineprops : dict, optional
  1604. Properties with which the line is drawn, if ``drawtype == "line"``.
  1605. Default::
  1606. dict(color="black", linestyle="-", linewidth=2, alpha=0.5)
  1607. rectprops : dict, optional
  1608. Properties with which the rectangle is drawn, if ``drawtype ==
  1609. "box"``. Default::
  1610. dict(facecolor="red", edgecolor="black", alpha=0.2, fill=True)
  1611. spancoords : {"data", "pixels"}, default: "data"
  1612. Whether to interpret *minspanx* and *minspany* in data or in pixel
  1613. coordinates.
  1614. button : `.MouseButton`, list of `.MouseButton`, default: all buttons
  1615. Button(s) that trigger rectangle selection.
  1616. maxdist : float, default: 10
  1617. Distance in pixels within which the interactive tool handles can be
  1618. activated.
  1619. marker_props : dict
  1620. Properties with which the interactive handles are drawn. Currently
  1621. not implemented and ignored.
  1622. interactive : bool, default: False
  1623. Whether to draw a set of handles that allow interaction with the
  1624. widget after it is drawn.
  1625. state_modifier_keys : dict, optional
  1626. Keyboard modifiers which affect the widget's behavior. Values
  1627. amend the defaults.
  1628. - "move": Move the existing shape, default: no modifier.
  1629. - "clear": Clear the current shape, default: "escape".
  1630. - "square": Makes the shape square, default: "shift".
  1631. - "center": Make the initial point the center of the shape,
  1632. default: "ctrl".
  1633. "square" and "center" can be combined.
  1634. """
  1635. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  1636. button=button,
  1637. state_modifier_keys=state_modifier_keys)
  1638. self.to_draw = None
  1639. self.visible = True
  1640. self.interactive = interactive
  1641. if drawtype == 'none': # draw a line but make it invisible
  1642. drawtype = 'line'
  1643. self.visible = False
  1644. if drawtype == 'box':
  1645. if rectprops is None:
  1646. rectprops = dict(facecolor='red', edgecolor='black',
  1647. alpha=0.2, fill=True)
  1648. rectprops['animated'] = self.useblit
  1649. self.rectprops = rectprops
  1650. self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
  1651. **self.rectprops)
  1652. self.ax.add_patch(self.to_draw)
  1653. if drawtype == 'line':
  1654. if lineprops is None:
  1655. lineprops = dict(color='black', linestyle='-',
  1656. linewidth=2, alpha=0.5)
  1657. lineprops['animated'] = self.useblit
  1658. self.lineprops = lineprops
  1659. self.to_draw = Line2D([0, 0], [0, 0], visible=False,
  1660. **self.lineprops)
  1661. self.ax.add_line(self.to_draw)
  1662. self.minspanx = minspanx
  1663. self.minspany = minspany
  1664. cbook._check_in_list(['data', 'pixels'], spancoords=spancoords)
  1665. self.spancoords = spancoords
  1666. self.drawtype = drawtype
  1667. self.maxdist = maxdist
  1668. if rectprops is None:
  1669. props = dict(mec='r')
  1670. else:
  1671. props = dict(mec=rectprops.get('edgecolor', 'r'))
  1672. self._corner_order = ['NW', 'NE', 'SE', 'SW']
  1673. xc, yc = self.corners
  1674. self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props,
  1675. useblit=self.useblit)
  1676. self._edge_order = ['W', 'N', 'E', 'S']
  1677. xe, ye = self.edge_centers
  1678. self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
  1679. marker_props=props,
  1680. useblit=self.useblit)
  1681. xc, yc = self.center
  1682. self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
  1683. marker_props=props,
  1684. useblit=self.useblit)
  1685. self.active_handle = None
  1686. self.artists = [self.to_draw, self._center_handle.artist,
  1687. self._corner_handles.artist,
  1688. self._edge_handles.artist]
  1689. if not self.interactive:
  1690. self.artists = [self.to_draw]
  1691. self._extents_on_press = None
  1692. def _press(self, event):
  1693. """on button press event"""
  1694. # make the drawn box/line visible get the click-coordinates,
  1695. # button, ...
  1696. if self.interactive and self.to_draw.get_visible():
  1697. self._set_active_handle(event)
  1698. else:
  1699. self.active_handle = None
  1700. if self.active_handle is None or not self.interactive:
  1701. # Clear previous rectangle before drawing new rectangle.
  1702. self.update()
  1703. if not self.interactive:
  1704. x = event.xdata
  1705. y = event.ydata
  1706. self.extents = x, x, y, y
  1707. self.set_visible(self.visible)
  1708. def _release(self, event):
  1709. """on button release event"""
  1710. if not self.interactive:
  1711. self.to_draw.set_visible(False)
  1712. # update the eventpress and eventrelease with the resulting extents
  1713. x1, x2, y1, y2 = self.extents
  1714. self.eventpress.xdata = x1
  1715. self.eventpress.ydata = y1
  1716. xy1 = self.ax.transData.transform([x1, y1])
  1717. self.eventpress.x, self.eventpress.y = xy1
  1718. self.eventrelease.xdata = x2
  1719. self.eventrelease.ydata = y2
  1720. xy2 = self.ax.transData.transform([x2, y2])
  1721. self.eventrelease.x, self.eventrelease.y = xy2
  1722. # calculate dimensions of box or line
  1723. if self.spancoords == 'data':
  1724. spanx = abs(self.eventpress.xdata - self.eventrelease.xdata)
  1725. spany = abs(self.eventpress.ydata - self.eventrelease.ydata)
  1726. elif self.spancoords == 'pixels':
  1727. spanx = abs(self.eventpress.x - self.eventrelease.x)
  1728. spany = abs(self.eventpress.y - self.eventrelease.y)
  1729. else:
  1730. cbook._check_in_list(['data', 'pixels'],
  1731. spancoords=self.spancoords)
  1732. # check if drawn distance (if it exists) is not too small in
  1733. # either x or y-direction
  1734. if (self.drawtype != 'none'
  1735. and (self.minspanx is not None and spanx < self.minspanx
  1736. or self.minspany is not None and spany < self.minspany)):
  1737. for artist in self.artists:
  1738. artist.set_visible(False)
  1739. self.update()
  1740. return
  1741. # call desired function
  1742. self.onselect(self.eventpress, self.eventrelease)
  1743. self.update()
  1744. return False
  1745. def _onmove(self, event):
  1746. """on motion notify event if box/line is wanted"""
  1747. # resize an existing shape
  1748. if self.active_handle and self.active_handle != 'C':
  1749. x1, x2, y1, y2 = self._extents_on_press
  1750. if self.active_handle in ['E', 'W'] + self._corner_order:
  1751. x2 = event.xdata
  1752. if self.active_handle in ['N', 'S'] + self._corner_order:
  1753. y2 = event.ydata
  1754. # move existing shape
  1755. elif (('move' in self.state or self.active_handle == 'C')
  1756. and self._extents_on_press is not None):
  1757. x1, x2, y1, y2 = self._extents_on_press
  1758. dx = event.xdata - self.eventpress.xdata
  1759. dy = event.ydata - self.eventpress.ydata
  1760. x1 += dx
  1761. x2 += dx
  1762. y1 += dy
  1763. y2 += dy
  1764. # new shape
  1765. else:
  1766. center = [self.eventpress.xdata, self.eventpress.ydata]
  1767. center_pix = [self.eventpress.x, self.eventpress.y]
  1768. dx = (event.xdata - center[0]) / 2.
  1769. dy = (event.ydata - center[1]) / 2.
  1770. # square shape
  1771. if 'square' in self.state:
  1772. dx_pix = abs(event.x - center_pix[0])
  1773. dy_pix = abs(event.y - center_pix[1])
  1774. if not dx_pix:
  1775. return
  1776. maxd = max(abs(dx_pix), abs(dy_pix))
  1777. if abs(dx_pix) < maxd:
  1778. dx *= maxd / (abs(dx_pix) + 1e-6)
  1779. if abs(dy_pix) < maxd:
  1780. dy *= maxd / (abs(dy_pix) + 1e-6)
  1781. # from center
  1782. if 'center' in self.state:
  1783. dx *= 2
  1784. dy *= 2
  1785. # from corner
  1786. else:
  1787. center[0] += dx
  1788. center[1] += dy
  1789. x1, x2, y1, y2 = (center[0] - dx, center[0] + dx,
  1790. center[1] - dy, center[1] + dy)
  1791. self.extents = x1, x2, y1, y2
  1792. @property
  1793. def _rect_bbox(self):
  1794. if self.drawtype == 'box':
  1795. x0 = self.to_draw.get_x()
  1796. y0 = self.to_draw.get_y()
  1797. width = self.to_draw.get_width()
  1798. height = self.to_draw.get_height()
  1799. return x0, y0, width, height
  1800. else:
  1801. x, y = self.to_draw.get_data()
  1802. x0, x1 = min(x), max(x)
  1803. y0, y1 = min(y), max(y)
  1804. return x0, y0, x1 - x0, y1 - y0
  1805. @property
  1806. def corners(self):
  1807. """Corners of rectangle from lower left, moving clockwise."""
  1808. x0, y0, width, height = self._rect_bbox
  1809. xc = x0, x0 + width, x0 + width, x0
  1810. yc = y0, y0, y0 + height, y0 + height
  1811. return xc, yc
  1812. @property
  1813. def edge_centers(self):
  1814. """Midpoint of rectangle edges from left, moving clockwise."""
  1815. x0, y0, width, height = self._rect_bbox
  1816. w = width / 2.
  1817. h = height / 2.
  1818. xe = x0, x0 + w, x0 + width, x0 + w
  1819. ye = y0 + h, y0, y0 + h, y0 + height
  1820. return xe, ye
  1821. @property
  1822. def center(self):
  1823. """Center of rectangle"""
  1824. x0, y0, width, height = self._rect_bbox
  1825. return x0 + width / 2., y0 + height / 2.
  1826. @property
  1827. def extents(self):
  1828. """Return (xmin, xmax, ymin, ymax)."""
  1829. x0, y0, width, height = self._rect_bbox
  1830. xmin, xmax = sorted([x0, x0 + width])
  1831. ymin, ymax = sorted([y0, y0 + height])
  1832. return xmin, xmax, ymin, ymax
  1833. @extents.setter
  1834. def extents(self, extents):
  1835. # Update displayed shape
  1836. self.draw_shape(extents)
  1837. # Update displayed handles
  1838. self._corner_handles.set_data(*self.corners)
  1839. self._edge_handles.set_data(*self.edge_centers)
  1840. self._center_handle.set_data(*self.center)
  1841. self.set_visible(self.visible)
  1842. self.update()
  1843. def draw_shape(self, extents):
  1844. x0, x1, y0, y1 = extents
  1845. xmin, xmax = sorted([x0, x1])
  1846. ymin, ymax = sorted([y0, y1])
  1847. xlim = sorted(self.ax.get_xlim())
  1848. ylim = sorted(self.ax.get_ylim())
  1849. xmin = max(xlim[0], xmin)
  1850. ymin = max(ylim[0], ymin)
  1851. xmax = min(xmax, xlim[1])
  1852. ymax = min(ymax, ylim[1])
  1853. if self.drawtype == 'box':
  1854. self.to_draw.set_x(xmin)
  1855. self.to_draw.set_y(ymin)
  1856. self.to_draw.set_width(xmax - xmin)
  1857. self.to_draw.set_height(ymax - ymin)
  1858. elif self.drawtype == 'line':
  1859. self.to_draw.set_data([xmin, xmax], [ymin, ymax])
  1860. def _set_active_handle(self, event):
  1861. """Set active handle based on the location of the mouse event"""
  1862. # Note: event.xdata/ydata in data coordinates, event.x/y in pixels
  1863. c_idx, c_dist = self._corner_handles.closest(event.x, event.y)
  1864. e_idx, e_dist = self._edge_handles.closest(event.x, event.y)
  1865. m_idx, m_dist = self._center_handle.closest(event.x, event.y)
  1866. if 'move' in self.state:
  1867. self.active_handle = 'C'
  1868. self._extents_on_press = self.extents
  1869. # Set active handle as closest handle, if mouse click is close enough.
  1870. elif m_dist < self.maxdist * 2:
  1871. self.active_handle = 'C'
  1872. elif c_dist > self.maxdist and e_dist > self.maxdist:
  1873. self.active_handle = None
  1874. return
  1875. elif c_dist < e_dist:
  1876. self.active_handle = self._corner_order[c_idx]
  1877. else:
  1878. self.active_handle = self._edge_order[e_idx]
  1879. # Save coordinates of rectangle at the start of handle movement.
  1880. x1, x2, y1, y2 = self.extents
  1881. # Switch variables so that only x2 and/or y2 are updated on move.
  1882. if self.active_handle in ['W', 'SW', 'NW']:
  1883. x1, x2 = x2, event.xdata
  1884. if self.active_handle in ['N', 'NW', 'NE']:
  1885. y1, y2 = y2, event.ydata
  1886. self._extents_on_press = x1, x2, y1, y2
  1887. @property
  1888. def geometry(self):
  1889. """
  1890. Return an array of shape (2, 5) containing the
  1891. x (``RectangleSelector.geometry[1, :]``) and
  1892. y (``RectangleSelector.geometry[0, :]``) coordinates
  1893. of the four corners of the rectangle starting and ending
  1894. in the top left corner.
  1895. """
  1896. if hasattr(self.to_draw, 'get_verts'):
  1897. xfm = self.ax.transData.inverted()
  1898. y, x = xfm.transform(self.to_draw.get_verts()).T
  1899. return np.array([x, y])
  1900. else:
  1901. return np.array(self.to_draw.get_data())
  1902. class EllipseSelector(RectangleSelector):
  1903. """
  1904. Select an elliptical region of an axes.
  1905. For the cursor to remain responsive you must keep a reference to it.
  1906. Example usage::
  1907. import numpy as np
  1908. import matplotlib.pyplot as plt
  1909. from matplotlib.widgets import EllipseSelector
  1910. def onselect(eclick, erelease):
  1911. "eclick and erelease are matplotlib events at press and release."
  1912. print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
  1913. print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
  1914. print('used button : ', eclick.button)
  1915. def toggle_selector(event):
  1916. print(' Key pressed.')
  1917. if event.key in ['Q', 'q'] and toggle_selector.ES.active:
  1918. print('EllipseSelector deactivated.')
  1919. toggle_selector.RS.set_active(False)
  1920. if event.key in ['A', 'a'] and not toggle_selector.ES.active:
  1921. print('EllipseSelector activated.')
  1922. toggle_selector.ES.set_active(True)
  1923. x = np.arange(100.) / 99
  1924. y = np.sin(x)
  1925. fig, ax = plt.subplots()
  1926. ax.plot(x, y)
  1927. toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line')
  1928. fig.canvas.mpl_connect('key_press_event', toggle_selector)
  1929. plt.show()
  1930. """
  1931. _shape_klass = Ellipse
  1932. def draw_shape(self, extents):
  1933. x1, x2, y1, y2 = extents
  1934. xmin, xmax = sorted([x1, x2])
  1935. ymin, ymax = sorted([y1, y2])
  1936. center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.]
  1937. a = (xmax - xmin) / 2.
  1938. b = (ymax - ymin) / 2.
  1939. if self.drawtype == 'box':
  1940. self.to_draw.center = center
  1941. self.to_draw.width = 2 * a
  1942. self.to_draw.height = 2 * b
  1943. else:
  1944. rad = np.deg2rad(np.arange(31) * 12)
  1945. x = a * np.cos(rad) + center[0]
  1946. y = b * np.sin(rad) + center[1]
  1947. self.to_draw.set_data(x, y)
  1948. @property
  1949. def _rect_bbox(self):
  1950. if self.drawtype == 'box':
  1951. x, y = self.to_draw.center
  1952. width = self.to_draw.width
  1953. height = self.to_draw.height
  1954. return x - width / 2., y - height / 2., width, height
  1955. else:
  1956. x, y = self.to_draw.get_data()
  1957. x0, x1 = min(x), max(x)
  1958. y0, y1 = min(y), max(y)
  1959. return x0, y0, x1 - x0, y1 - y0
  1960. class LassoSelector(_SelectorWidget):
  1961. """
  1962. Selection curve of an arbitrary shape.
  1963. For the selector to remain responsive you must keep a reference to it.
  1964. The selected path can be used in conjunction with `~.Path.contains_point`
  1965. to select data points from an image.
  1966. In contrast to `Lasso`, `LassoSelector` is written with an interface
  1967. similar to `RectangleSelector` and `SpanSelector`, and will continue to
  1968. interact with the axes until disconnected.
  1969. Example usage::
  1970. ax = subplot(111)
  1971. ax.plot(x, y)
  1972. def onselect(verts):
  1973. print(verts)
  1974. lasso = LassoSelector(ax, onselect)
  1975. Parameters
  1976. ----------
  1977. ax : `~matplotlib.axes.Axes`
  1978. The parent axes for the widget.
  1979. onselect : function
  1980. Whenever the lasso is released, the *onselect* function is called and
  1981. passed the vertices of the selected path.
  1982. button : `.MouseButton` or list of `.MouseButton`, optional
  1983. The mouse buttons used for rectangle selection. Default is ``None``,
  1984. which corresponds to all buttons.
  1985. """
  1986. def __init__(self, ax, onselect=None, useblit=True, lineprops=None,
  1987. button=None):
  1988. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  1989. button=button)
  1990. self.verts = None
  1991. if lineprops is None:
  1992. lineprops = dict()
  1993. # self.useblit may be != useblit, if the canvas doesn't support blit.
  1994. lineprops.update(animated=self.useblit, visible=False)
  1995. self.line = Line2D([], [], **lineprops)
  1996. self.ax.add_line(self.line)
  1997. self.artists = [self.line]
  1998. def onpress(self, event):
  1999. self.press(event)
  2000. def _press(self, event):
  2001. self.verts = [self._get_data(event)]
  2002. self.line.set_visible(True)
  2003. def onrelease(self, event):
  2004. self.release(event)
  2005. def _release(self, event):
  2006. if self.verts is not None:
  2007. self.verts.append(self._get_data(event))
  2008. self.onselect(self.verts)
  2009. self.line.set_data([[], []])
  2010. self.line.set_visible(False)
  2011. self.verts = None
  2012. def _onmove(self, event):
  2013. if self.verts is None:
  2014. return
  2015. self.verts.append(self._get_data(event))
  2016. self.line.set_data(list(zip(*self.verts)))
  2017. self.update()
  2018. class PolygonSelector(_SelectorWidget):
  2019. """
  2020. Select a polygon region of an axes.
  2021. Place vertices with each mouse click, and make the selection by completing
  2022. the polygon (clicking on the first vertex). Hold the *ctrl* key and click
  2023. and drag a vertex to reposition it (the *ctrl* key is not necessary if the
  2024. polygon has already been completed). Hold the *shift* key and click and
  2025. drag anywhere in the axes to move all vertices. Press the *esc* key to
  2026. start a new polygon.
  2027. For the selector to remain responsive you must keep a reference to it.
  2028. Parameters
  2029. ----------
  2030. ax : `~matplotlib.axes.Axes`
  2031. The parent axes for the widget.
  2032. onselect : function
  2033. When a polygon is completed or modified after completion,
  2034. the *onselect* function is called and passed a list of the vertices as
  2035. ``(xdata, ydata)`` tuples.
  2036. useblit : bool, default: False
  2037. lineprops : dict, default: \
  2038. ``dict(color='k', linestyle='-', linewidth=2, alpha=0.5)``.
  2039. Artist properties for the line representing the edges of the polygon.
  2040. markerprops : dict, default: \
  2041. ``dict(marker='o', markersize=7, mec='k', mfc='k', alpha=0.5)``.
  2042. Artist properties for the markers drawn at the vertices of the polygon.
  2043. vertex_select_radius : float, default: 15px
  2044. A vertex is selected (to complete the polygon or to move a vertex) if
  2045. the mouse click is within *vertex_select_radius* pixels of the vertex.
  2046. Examples
  2047. --------
  2048. :doc:`/gallery/widgets/polygon_selector_demo`
  2049. """
  2050. def __init__(self, ax, onselect, useblit=False,
  2051. lineprops=None, markerprops=None, vertex_select_radius=15):
  2052. # The state modifiers 'move', 'square', and 'center' are expected by
  2053. # _SelectorWidget but are not supported by PolygonSelector
  2054. # Note: could not use the existing 'move' state modifier in-place of
  2055. # 'move_all' because _SelectorWidget automatically discards 'move'
  2056. # from the state on button release.
  2057. state_modifier_keys = dict(clear='escape', move_vertex='control',
  2058. move_all='shift', move='not-applicable',
  2059. square='not-applicable',
  2060. center='not-applicable')
  2061. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  2062. state_modifier_keys=state_modifier_keys)
  2063. self._xs, self._ys = [0], [0]
  2064. self._polygon_completed = False
  2065. if lineprops is None:
  2066. lineprops = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
  2067. lineprops['animated'] = self.useblit
  2068. self.line = Line2D(self._xs, self._ys, **lineprops)
  2069. self.ax.add_line(self.line)
  2070. if markerprops is None:
  2071. markerprops = dict(mec='k', mfc=lineprops.get('color', 'k'))
  2072. self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys,
  2073. useblit=self.useblit,
  2074. marker_props=markerprops)
  2075. self._active_handle_idx = -1
  2076. self.vertex_select_radius = vertex_select_radius
  2077. self.artists = [self.line, self._polygon_handles.artist]
  2078. self.set_visible(True)
  2079. def _press(self, event):
  2080. """Button press event handler"""
  2081. # Check for selection of a tool handle.
  2082. if ((self._polygon_completed or 'move_vertex' in self.state)
  2083. and len(self._xs) > 0):
  2084. h_idx, h_dist = self._polygon_handles.closest(event.x, event.y)
  2085. if h_dist < self.vertex_select_radius:
  2086. self._active_handle_idx = h_idx
  2087. # Save the vertex positions at the time of the press event (needed to
  2088. # support the 'move_all' state modifier).
  2089. self._xs_at_press, self._ys_at_press = self._xs.copy(), self._ys.copy()
  2090. def _release(self, event):
  2091. """Button release event handler"""
  2092. # Release active tool handle.
  2093. if self._active_handle_idx >= 0:
  2094. self._active_handle_idx = -1
  2095. # Complete the polygon.
  2096. elif (len(self._xs) > 3
  2097. and self._xs[-1] == self._xs[0]
  2098. and self._ys[-1] == self._ys[0]):
  2099. self._polygon_completed = True
  2100. # Place new vertex.
  2101. elif (not self._polygon_completed
  2102. and 'move_all' not in self.state
  2103. and 'move_vertex' not in self.state):
  2104. self._xs.insert(-1, event.xdata)
  2105. self._ys.insert(-1, event.ydata)
  2106. if self._polygon_completed:
  2107. self.onselect(self.verts)
  2108. def onmove(self, event):
  2109. """Cursor move event handler and validator"""
  2110. # Method overrides _SelectorWidget.onmove because the polygon selector
  2111. # needs to process the move callback even if there is no button press.
  2112. # _SelectorWidget.onmove include logic to ignore move event if
  2113. # eventpress is None.
  2114. if not self.ignore(event):
  2115. event = self._clean_event(event)
  2116. self._onmove(event)
  2117. return True
  2118. return False
  2119. def _onmove(self, event):
  2120. """Cursor move event handler"""
  2121. # Move the active vertex (ToolHandle).
  2122. if self._active_handle_idx >= 0:
  2123. idx = self._active_handle_idx
  2124. self._xs[idx], self._ys[idx] = event.xdata, event.ydata
  2125. # Also update the end of the polygon line if the first vertex is
  2126. # the active handle and the polygon is completed.
  2127. if idx == 0 and self._polygon_completed:
  2128. self._xs[-1], self._ys[-1] = event.xdata, event.ydata
  2129. # Move all vertices.
  2130. elif 'move_all' in self.state and self.eventpress:
  2131. dx = event.xdata - self.eventpress.xdata
  2132. dy = event.ydata - self.eventpress.ydata
  2133. for k in range(len(self._xs)):
  2134. self._xs[k] = self._xs_at_press[k] + dx
  2135. self._ys[k] = self._ys_at_press[k] + dy
  2136. # Do nothing if completed or waiting for a move.
  2137. elif (self._polygon_completed
  2138. or 'move_vertex' in self.state or 'move_all' in self.state):
  2139. return
  2140. # Position pending vertex.
  2141. else:
  2142. # Calculate distance to the start vertex.
  2143. x0, y0 = self.line.get_transform().transform((self._xs[0],
  2144. self._ys[0]))
  2145. v0_dist = np.hypot(x0 - event.x, y0 - event.y)
  2146. # Lock on to the start vertex if near it and ready to complete.
  2147. if len(self._xs) > 3 and v0_dist < self.vertex_select_radius:
  2148. self._xs[-1], self._ys[-1] = self._xs[0], self._ys[0]
  2149. else:
  2150. self._xs[-1], self._ys[-1] = event.xdata, event.ydata
  2151. self._draw_polygon()
  2152. def _on_key_press(self, event):
  2153. """Key press event handler"""
  2154. # Remove the pending vertex if entering the 'move_vertex' or
  2155. # 'move_all' mode
  2156. if (not self._polygon_completed
  2157. and ('move_vertex' in self.state or 'move_all' in self.state)):
  2158. self._xs, self._ys = self._xs[:-1], self._ys[:-1]
  2159. self._draw_polygon()
  2160. def _on_key_release(self, event):
  2161. """Key release event handler"""
  2162. # Add back the pending vertex if leaving the 'move_vertex' or
  2163. # 'move_all' mode (by checking the released key)
  2164. if (not self._polygon_completed
  2165. and
  2166. (event.key == self.state_modifier_keys.get('move_vertex')
  2167. or event.key == self.state_modifier_keys.get('move_all'))):
  2168. self._xs.append(event.xdata)
  2169. self._ys.append(event.ydata)
  2170. self._draw_polygon()
  2171. # Reset the polygon if the released key is the 'clear' key.
  2172. elif event.key == self.state_modifier_keys.get('clear'):
  2173. event = self._clean_event(event)
  2174. self._xs, self._ys = [event.xdata], [event.ydata]
  2175. self._polygon_completed = False
  2176. self.set_visible(True)
  2177. def _draw_polygon(self):
  2178. """Redraw the polygon based on the new vertex positions."""
  2179. self.line.set_data(self._xs, self._ys)
  2180. # Only show one tool handle at the start and end vertex of the polygon
  2181. # if the polygon is completed or the user is locked on to the start
  2182. # vertex.
  2183. if (self._polygon_completed
  2184. or (len(self._xs) > 3
  2185. and self._xs[-1] == self._xs[0]
  2186. and self._ys[-1] == self._ys[0])):
  2187. self._polygon_handles.set_data(self._xs[:-1], self._ys[:-1])
  2188. else:
  2189. self._polygon_handles.set_data(self._xs, self._ys)
  2190. self.update()
  2191. @property
  2192. def verts(self):
  2193. """The polygon vertices, as a list of ``(x, y)`` pairs."""
  2194. return list(zip(self._xs[:-1], self._ys[:-1]))
  2195. class Lasso(AxesWidget):
  2196. """
  2197. Selection curve of an arbitrary shape.
  2198. The selected path can be used in conjunction with
  2199. `~matplotlib.path.Path.contains_point` to select data points from an image.
  2200. Unlike `LassoSelector`, this must be initialized with a starting
  2201. point *xy*, and the `Lasso` events are destroyed upon release.
  2202. Parameters
  2203. ----------
  2204. ax : `~matplotlib.axes.Axes`
  2205. The parent axes for the widget.
  2206. xy : (float, float)
  2207. Coordinates of the start of the lasso.
  2208. callback : callable
  2209. Whenever the lasso is released, the *callback* function is called and
  2210. passed the vertices of the selected path.
  2211. """
  2212. def __init__(self, ax, xy, callback=None, useblit=True):
  2213. AxesWidget.__init__(self, ax)
  2214. self.useblit = useblit and self.canvas.supports_blit
  2215. if self.useblit:
  2216. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  2217. x, y = xy
  2218. self.verts = [(x, y)]
  2219. self.line = Line2D([x], [y], linestyle='-', color='black', lw=2)
  2220. self.ax.add_line(self.line)
  2221. self.callback = callback
  2222. self.connect_event('button_release_event', self.onrelease)
  2223. self.connect_event('motion_notify_event', self.onmove)
  2224. def onrelease(self, event):
  2225. if self.ignore(event):
  2226. return
  2227. if self.verts is not None:
  2228. self.verts.append((event.xdata, event.ydata))
  2229. if len(self.verts) > 2:
  2230. self.callback(self.verts)
  2231. self.ax.lines.remove(self.line)
  2232. self.verts = None
  2233. self.disconnect_events()
  2234. def onmove(self, event):
  2235. if self.ignore(event):
  2236. return
  2237. if self.verts is None:
  2238. return
  2239. if event.inaxes != self.ax:
  2240. return
  2241. if event.button != 1:
  2242. return
  2243. self.verts.append((event.xdata, event.ydata))
  2244. self.line.set_data(list(zip(*self.verts)))
  2245. if self.useblit:
  2246. self.canvas.restore_region(self.background)
  2247. self.ax.draw_artist(self.line)
  2248. self.canvas.blit(self.ax.bbox)
  2249. else:
  2250. self.canvas.draw_idle()