123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712 |
- """
- GUI neutral widgets
- ===================
- Widgets that are designed to work for any of the GUI backends.
- All of these widgets require you to predefine a `matplotlib.axes.Axes`
- instance and pass that as the first parameter. Matplotlib doesn't try to
- be too smart with respect to layout -- you will have to figure out how
- wide and tall you want your Axes to be to accommodate your widget.
- """
- from contextlib import ExitStack
- import copy
- from numbers import Integral
- import numpy as np
- import matplotlib as mpl
- from . import cbook, colors, ticker
- from .lines import Line2D
- from .patches import Circle, Rectangle, Ellipse
- from .transforms import blended_transform_factory
- class LockDraw:
- """
- Some widgets, like the cursor, draw onto the canvas, and this is not
- desirable under all circumstances, like when the toolbar is in zoom-to-rect
- mode and drawing a rectangle. To avoid this, a widget can acquire a
- canvas' lock with ``canvas.widgetlock(widget)`` before drawing on the
- canvas; this will prevent other widgets from doing so at the same time (if
- they also try to acquire the lock first).
- """
- def __init__(self):
- self._owner = None
- def __call__(self, o):
- """Reserve the lock for *o*."""
- if not self.available(o):
- raise ValueError('already locked')
- self._owner = o
- def release(self, o):
- """Release the lock from *o*."""
- if not self.available(o):
- raise ValueError('you do not own this lock')
- self._owner = None
- def available(self, o):
- """Return whether drawing is available to *o*."""
- return not self.locked() or self.isowner(o)
- def isowner(self, o):
- """Return whether *o* owns this lock."""
- return self._owner is o
- def locked(self):
- """Return whether the lock is currently held by an owner."""
- return self._owner is not None
- class Widget:
- """
- Abstract base class for GUI neutral widgets
- """
- drawon = True
- eventson = True
- _active = True
- def set_active(self, active):
- """Set whether the widget is active."""
- self._active = active
- def get_active(self):
- """Get whether the widget is active."""
- return self._active
- # set_active is overridden by SelectorWidgets.
- active = property(get_active, set_active, doc="Is the widget active?")
- def ignore(self, event):
- """
- Return whether *event* should be ignored.
- This method should be called at the beginning of any event callback.
- """
- return not self.active
- class AxesWidget(Widget):
- """
- Widget connected to a single `~matplotlib.axes.Axes`.
- To guarantee that the widget remains responsive and not garbage-collected,
- a reference to the object should be maintained by the user.
- This is necessary because the callback registry
- maintains only weak-refs to the functions, which are member
- functions of the widget. If there are no references to the widget
- object it may be garbage collected which will disconnect the callbacks.
- Attributes
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- canvas : `~matplotlib.backend_bases.FigureCanvasBase`
- The parent figure canvas for the widget.
- active : bool
- If False, the widget does not respond to events.
- """
- def __init__(self, ax):
- self.ax = ax
- self.canvas = ax.figure.canvas
- self.cids = []
- def connect_event(self, event, callback):
- """
- Connect callback with an event.
- This should be used in lieu of ``figure.canvas.mpl_connect`` since this
- function stores callback ids for later clean up.
- """
- cid = self.canvas.mpl_connect(event, callback)
- self.cids.append(cid)
- def disconnect_events(self):
- """Disconnect all events created by this widget."""
- for c in self.cids:
- self.canvas.mpl_disconnect(c)
- class Button(AxesWidget):
- """
- A GUI neutral button.
- For the button to remain responsive you must keep a reference to it.
- Call `.on_clicked` to connect to the button.
- Attributes
- ----------
- ax
- The `matplotlib.axes.Axes` the button renders into.
- label
- A `matplotlib.text.Text` instance.
- color
- The color of the button when not hovering.
- hovercolor
- The color of the button when hovering.
- """
- def __init__(self, ax, label, image=None,
- color='0.85', hovercolor='0.95'):
- """
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The `~.axes.Axes` instance the button will be placed into.
- label : str
- The button text.
- image : array-like or PIL Image
- The image to place in the button, if not *None*. The parameter is
- directly forwarded to `~matplotlib.axes.Axes.imshow`.
- color : color
- The color of the button when not activated.
- hovercolor : color
- The color of the button when the mouse is over it.
- """
- AxesWidget.__init__(self, ax)
- if image is not None:
- ax.imshow(image)
- self.label = ax.text(0.5, 0.5, label,
- verticalalignment='center',
- horizontalalignment='center',
- transform=ax.transAxes)
- self.cnt = 0
- self.observers = {}
- self.connect_event('button_press_event', self._click)
- self.connect_event('button_release_event', self._release)
- self.connect_event('motion_notify_event', self._motion)
- ax.set_navigate(False)
- ax.set_facecolor(color)
- ax.set_xticks([])
- ax.set_yticks([])
- self.color = color
- self.hovercolor = hovercolor
- def _click(self, event):
- if (self.ignore(event)
- or event.inaxes != self.ax
- or not self.eventson):
- return
- if event.canvas.mouse_grabber != self.ax:
- event.canvas.grab_mouse(self.ax)
- def _release(self, event):
- if (self.ignore(event)
- or event.canvas.mouse_grabber != self.ax):
- return
- event.canvas.release_mouse(self.ax)
- if (not self.eventson
- or event.inaxes != self.ax):
- return
- for cid, func in self.observers.items():
- func(event)
- def _motion(self, event):
- if self.ignore(event):
- return
- c = self.hovercolor if event.inaxes == self.ax else self.color
- if not colors.same_color(c, self.ax.get_facecolor()):
- self.ax.set_facecolor(c)
- if self.drawon:
- self.ax.figure.canvas.draw()
- def on_clicked(self, func):
- """
- Connect the callback function *func* to button click events.
- Returns a connection id, which can be used to disconnect the callback.
- """
- cid = self.cnt
- self.observers[cid] = func
- self.cnt += 1
- return cid
- def disconnect(self, cid):
- """Remove the callback function with connection id *cid*."""
- try:
- del self.observers[cid]
- except KeyError:
- pass
- class Slider(AxesWidget):
- """
- A slider representing a floating point range.
- Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to
- remain responsive you must maintain a reference to it. Call
- :meth:`on_changed` to connect to the slider event.
- Attributes
- ----------
- val : float
- Slider value.
- """
- def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
- closedmin=True, closedmax=True, slidermin=None,
- slidermax=None, dragging=True, valstep=None,
- orientation='horizontal', **kwargs):
- """
- Parameters
- ----------
- ax : Axes
- The Axes to put the slider in.
- label : str
- Slider label.
- valmin : float
- The minimum value of the slider.
- valmax : float
- The maximum value of the slider.
- valinit : float, default: 0.5
- The slider initial position.
- valfmt : str, default: None
- %-format string used to format the slider value. If None, a
- `.ScalarFormatter` is used instead.
- closedmin : bool, default: True
- Whether the slider interval is closed on the bottom.
- closedmax : bool, default: True
- Whether the slider interval is closed on the top.
- slidermin : Slider, default: None
- Do not allow the current slider to have a value less than
- the value of the Slider *slidermin*.
- slidermax : Slider, default: None
- Do not allow the current slider to have a value greater than
- the value of the Slider *slidermax*.
- dragging : bool, default: True
- If True the slider can be dragged by the mouse.
- valstep : float, default: None
- If given, the slider will snap to multiples of *valstep*.
- orientation : {'horizontal', 'vertical'}, default: 'horizontal'
- The orientation of the slider.
- Notes
- -----
- Additional kwargs are passed on to ``self.poly`` which is the
- `~matplotlib.patches.Rectangle` that draws the slider knob. See the
- `.Rectangle` documentation for valid property names (``facecolor``,
- ``edgecolor``, ``alpha``, etc.).
- """
- if ax.name == '3d':
- raise ValueError('Sliders cannot be added to 3D Axes')
- AxesWidget.__init__(self, ax)
- if slidermin is not None and not hasattr(slidermin, 'val'):
- raise ValueError("Argument slidermin ({}) has no 'val'"
- .format(type(slidermin)))
- if slidermax is not None and not hasattr(slidermax, 'val'):
- raise ValueError("Argument slidermax ({}) has no 'val'"
- .format(type(slidermax)))
- if orientation not in ['horizontal', 'vertical']:
- raise ValueError("Argument orientation ({}) must be either"
- "'horizontal' or 'vertical'".format(orientation))
- self.orientation = orientation
- self.closedmin = closedmin
- self.closedmax = closedmax
- self.slidermin = slidermin
- self.slidermax = slidermax
- self.drag_active = False
- self.valmin = valmin
- self.valmax = valmax
- self.valstep = valstep
- valinit = self._value_in_bounds(valinit)
- if valinit is None:
- valinit = valmin
- self.val = valinit
- self.valinit = valinit
- if orientation == 'vertical':
- self.poly = ax.axhspan(valmin, valinit, 0, 1, **kwargs)
- self.hline = ax.axhline(valinit, 0, 1, color='r', lw=1)
- else:
- self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs)
- self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1)
- if orientation == 'vertical':
- ax.set_ylim((valmin, valmax))
- axis = ax.yaxis
- else:
- ax.set_xlim((valmin, valmax))
- axis = ax.xaxis
- self.valfmt = valfmt
- self._fmt = axis.get_major_formatter()
- if not isinstance(self._fmt, ticker.ScalarFormatter):
- self._fmt = ticker.ScalarFormatter()
- self._fmt.set_axis(axis)
- self._fmt.set_useOffset(False) # No additive offset.
- self._fmt.set_useMathText(True) # x sign before multiplicative offset.
- ax.set_xticks([])
- ax.set_yticks([])
- ax.set_navigate(False)
- self.connect_event('button_press_event', self._update)
- self.connect_event('button_release_event', self._update)
- if dragging:
- self.connect_event('motion_notify_event', self._update)
- if orientation == 'vertical':
- self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes,
- verticalalignment='bottom',
- horizontalalignment='center')
- self.valtext = ax.text(0.5, -0.02, self._format(valinit),
- transform=ax.transAxes,
- verticalalignment='top',
- horizontalalignment='center')
- else:
- self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
- verticalalignment='center',
- horizontalalignment='right')
- self.valtext = ax.text(1.02, 0.5, self._format(valinit),
- transform=ax.transAxes,
- verticalalignment='center',
- horizontalalignment='left')
- self.cnt = 0
- self.observers = {}
- self.set_val(valinit)
- def _value_in_bounds(self, val):
- """Makes sure *val* is with given bounds."""
- if self.valstep:
- val = (self.valmin
- + round((val - self.valmin) / self.valstep) * self.valstep)
- if val <= self.valmin:
- if not self.closedmin:
- return
- val = self.valmin
- elif val >= self.valmax:
- if not self.closedmax:
- return
- val = self.valmax
- if self.slidermin is not None and val <= self.slidermin.val:
- if not self.closedmin:
- return
- val = self.slidermin.val
- if self.slidermax is not None and val >= self.slidermax.val:
- if not self.closedmax:
- return
- val = self.slidermax.val
- return val
- def _update(self, event):
- """Update the slider position."""
- if self.ignore(event) or event.button != 1:
- return
- if event.name == 'button_press_event' and event.inaxes == self.ax:
- self.drag_active = True
- event.canvas.grab_mouse(self.ax)
- if not self.drag_active:
- return
- elif ((event.name == 'button_release_event') or
- (event.name == 'button_press_event' and
- event.inaxes != self.ax)):
- self.drag_active = False
- event.canvas.release_mouse(self.ax)
- return
- if self.orientation == 'vertical':
- val = self._value_in_bounds(event.ydata)
- else:
- val = self._value_in_bounds(event.xdata)
- if val not in [None, self.val]:
- self.set_val(val)
- def _format(self, val):
- """Pretty-print *val*."""
- if self.valfmt is not None:
- return self.valfmt % val
- else:
- _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax])
- # fmt.get_offset is actually the multiplicative factor, if any.
- return s + self._fmt.get_offset()
- def set_val(self, val):
- """
- Set slider value to *val*
- Parameters
- ----------
- val : float
- """
- xy = self.poly.xy
- if self.orientation == 'vertical':
- xy[1] = 0, val
- xy[2] = 1, val
- else:
- xy[2] = val, 1
- xy[3] = val, 0
- self.poly.xy = xy
- self.valtext.set_text(self._format(val))
- if self.drawon:
- self.ax.figure.canvas.draw_idle()
- self.val = val
- if not self.eventson:
- return
- for cid, func in self.observers.items():
- func(val)
- def on_changed(self, func):
- """
- When the slider value is changed call *func* with the new
- slider value
- Parameters
- ----------
- func : callable
- Function to call when slider is changed.
- The function must accept a single float as its arguments.
- Returns
- -------
- int
- Connection id (which can be used to disconnect *func*)
- """
- cid = self.cnt
- self.observers[cid] = func
- self.cnt += 1
- return cid
- def disconnect(self, cid):
- """
- Remove the observer with connection id *cid*
- Parameters
- ----------
- cid : int
- Connection id of the observer to be removed
- """
- try:
- del self.observers[cid]
- except KeyError:
- pass
- def reset(self):
- """Reset the slider to the initial value"""
- if self.val != self.valinit:
- self.set_val(self.valinit)
- class CheckButtons(AxesWidget):
- r"""
- A GUI neutral set of check buttons.
- For the check buttons to remain responsive you must keep a
- reference to this object.
- Connect to the CheckButtons with the `.on_clicked` method.
- Attributes
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- labels : list of `.Text`
- rectangles : list of `.Rectangle`
- lines : list of (`.Line2D`, `.Line2D`) pairs
- List of lines for the x's in the check boxes. These lines exist for
- each box, but have ``set_visible(False)`` when its box is not checked.
- """
- def __init__(self, ax, labels, actives=None):
- """
- Add check buttons to `matplotlib.axes.Axes` instance *ax*
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- labels : list of str
- The labels of the check buttons.
- actives : list of bool, optional
- The initial check states of the buttons. The list must have the
- same length as *labels*. If not given, all buttons are unchecked.
- """
- AxesWidget.__init__(self, ax)
- ax.set_xticks([])
- ax.set_yticks([])
- ax.set_navigate(False)
- if actives is None:
- actives = [False] * len(labels)
- if len(labels) > 1:
- dy = 1. / (len(labels) + 1)
- ys = np.linspace(1 - dy, dy, len(labels))
- else:
- dy = 0.25
- ys = [0.5]
- axcolor = ax.get_facecolor()
- self.labels = []
- self.lines = []
- self.rectangles = []
- lineparams = {'color': 'k', 'linewidth': 1.25,
- 'transform': ax.transAxes, 'solid_capstyle': 'butt'}
- for y, label, active in zip(ys, labels, actives):
- t = ax.text(0.25, y, label, transform=ax.transAxes,
- horizontalalignment='left',
- verticalalignment='center')
- w, h = dy / 2, dy / 2
- x, y = 0.05, y - h / 2
- p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black',
- facecolor=axcolor, transform=ax.transAxes)
- l1 = Line2D([x, x + w], [y + h, y], **lineparams)
- l2 = Line2D([x, x + w], [y, y + h], **lineparams)
- l1.set_visible(active)
- l2.set_visible(active)
- self.labels.append(t)
- self.rectangles.append(p)
- self.lines.append((l1, l2))
- ax.add_patch(p)
- ax.add_line(l1)
- ax.add_line(l2)
- self.connect_event('button_press_event', self._clicked)
- self.cnt = 0
- self.observers = {}
- def _clicked(self, event):
- if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
- return
- for i, (p, t) in enumerate(zip(self.rectangles, self.labels)):
- if (t.get_window_extent().contains(event.x, event.y) or
- p.get_window_extent().contains(event.x, event.y)):
- self.set_active(i)
- break
- def set_active(self, index):
- """
- Toggle (activate or deactivate) a check button by index.
- Callbacks will be triggered if :attr:`eventson` is True.
- Parameters
- ----------
- index : int
- Index of the check button to toggle.
- Raises
- ------
- ValueError
- If *index* is invalid.
- """
- if not 0 <= index < len(self.labels):
- raise ValueError("Invalid CheckButton index: %d" % index)
- l1, l2 = self.lines[index]
- l1.set_visible(not l1.get_visible())
- l2.set_visible(not l2.get_visible())
- if self.drawon:
- self.ax.figure.canvas.draw()
- if not self.eventson:
- return
- for cid, func in self.observers.items():
- func(self.labels[index].get_text())
- def get_status(self):
- """
- Return a tuple of the status (True/False) of all of the check buttons.
- """
- return [l1.get_visible() for (l1, l2) in self.lines]
- def on_clicked(self, func):
- """
- Connect the callback function *func* to button click events.
- Returns a connection id, which can be used to disconnect the callback.
- """
- cid = self.cnt
- self.observers[cid] = func
- self.cnt += 1
- return cid
- def disconnect(self, cid):
- """Remove the observer with connection id *cid*."""
- try:
- del self.observers[cid]
- except KeyError:
- pass
- class TextBox(AxesWidget):
- """
- A GUI neutral text input box.
- For the text box to remain responsive you must keep a reference to it.
- Call `.on_text_change` to be updated whenever the text changes.
- Call `.on_submit` to be updated whenever the user hits enter or
- leaves the text entry field.
- Attributes
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- label : `.Text`
- color : color
- The color of the text box when not hovering.
- hovercolor : color
- The color of the text box when hovering.
- """
- @cbook.deprecated("3.3")
- @property
- def params_to_disable(self):
- return [key for key in mpl.rcParams if 'keymap' in key]
- def __init__(self, ax, label, initial='',
- color='.95', hovercolor='1', label_pad=.01):
- """
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The `~.axes.Axes` instance the button will be placed into.
- label : str
- Label for this text box.
- initial : str
- Initial value in the text box.
- color : color
- The color of the box.
- hovercolor : color
- The color of the box when the mouse is over it.
- label_pad : float
- The distance between the label and the right side of the textbox.
- """
- AxesWidget.__init__(self, ax)
- self.DIST_FROM_LEFT = .05
- self.label = ax.text(
- -label_pad, 0.5, label, transform=ax.transAxes,
- verticalalignment='center', horizontalalignment='right')
- self.text_disp = self.ax.text(
- self.DIST_FROM_LEFT, 0.5, initial, transform=self.ax.transAxes,
- verticalalignment='center', horizontalalignment='left')
- self.cnt = 0
- self.change_observers = {}
- self.submit_observers = {}
- ax.set(
- xlim=(0, 1), ylim=(0, 1), # s.t. cursor appears from first click.
- navigate=False, facecolor=color,
- xticks=[], yticks=[])
- self.cursor_index = 0
- self.cursor = ax.vlines(0, 0, 0, visible=False,
- transform=mpl.transforms.IdentityTransform())
- self.connect_event('button_press_event', self._click)
- self.connect_event('button_release_event', self._release)
- self.connect_event('motion_notify_event', self._motion)
- self.connect_event('key_press_event', self._keypress)
- self.connect_event('resize_event', self._resize)
- self.color = color
- self.hovercolor = hovercolor
- self.capturekeystrokes = False
- @property
- def text(self):
- return self.text_disp.get_text()
- def _rendercursor(self):
- # this is a hack to figure out where the cursor should go.
- # we draw the text up to where the cursor should go, measure
- # and save its dimensions, draw the real text, then put the cursor
- # at the saved dimensions
- # This causes a single extra draw if the figure has never been rendered
- # yet, which should be fine as we're going to repeatedly re-render the
- # figure later anyways.
- if self.ax.figure._cachedRenderer is None:
- self.ax.figure.canvas.draw()
- text = self.text_disp.get_text() # Save value before overwriting it.
- widthtext = text[:self.cursor_index]
- self.text_disp.set_text(widthtext or ",")
- bb = self.text_disp.get_window_extent()
- if not widthtext: # Use the comma for the height, but keep width to 0.
- bb.x1 = bb.x0
- self.cursor.set(
- segments=[[(bb.x1, bb.y0), (bb.x1, bb.y1)]], visible=True)
- self.text_disp.set_text(text)
- self.ax.figure.canvas.draw()
- def _notify_submit_observers(self):
- if self.eventson:
- for cid, func in self.submit_observers.items():
- func(self.text)
- def _release(self, event):
- if self.ignore(event):
- return
- if event.canvas.mouse_grabber != self.ax:
- return
- event.canvas.release_mouse(self.ax)
- def _keypress(self, event):
- if self.ignore(event):
- return
- if self.capturekeystrokes:
- key = event.key
- text = self.text
- if len(key) == 1:
- text = (text[:self.cursor_index] + key +
- text[self.cursor_index:])
- self.cursor_index += 1
- elif key == "right":
- if self.cursor_index != len(text):
- self.cursor_index += 1
- elif key == "left":
- if self.cursor_index != 0:
- self.cursor_index -= 1
- elif key == "home":
- self.cursor_index = 0
- elif key == "end":
- self.cursor_index = len(text)
- elif key == "backspace":
- if self.cursor_index != 0:
- text = (text[:self.cursor_index - 1] +
- text[self.cursor_index:])
- self.cursor_index -= 1
- elif key == "delete":
- if self.cursor_index != len(self.text):
- text = (text[:self.cursor_index] +
- text[self.cursor_index + 1:])
- self.text_disp.set_text(text)
- self._rendercursor()
- self._notify_change_observers()
- if key == "enter":
- self._notify_submit_observers()
- def set_val(self, val):
- newval = str(val)
- if self.text == newval:
- return
- self.text_disp.set_text(newval)
- self._rendercursor()
- self._notify_change_observers()
- self._notify_submit_observers()
- def _notify_change_observers(self):
- if self.eventson:
- for cid, func in self.change_observers.items():
- func(self.text)
- def begin_typing(self, x):
- self.capturekeystrokes = True
- # Disable keypress shortcuts, which may otherwise cause the figure to
- # be saved, closed, etc., until the user stops typing. The way to
- # achieve this depends on whether toolmanager is in use.
- stack = ExitStack() # Register cleanup actions when user stops typing.
- self._on_stop_typing = stack.close
- toolmanager = getattr(
- self.ax.figure.canvas.manager, "toolmanager", None)
- if toolmanager is not None:
- # If using toolmanager, lock keypresses, and plan to release the
- # lock when typing stops.
- toolmanager.keypresslock(self)
- stack.push(toolmanager.keypresslock.release, self)
- else:
- # If not using toolmanager, disable all keypress-related rcParams.
- # Avoid spurious warnings if keymaps are getting deprecated.
- with cbook._suppress_matplotlib_deprecation_warning():
- stack.enter_context(mpl.rc_context(
- {k: [] for k in mpl.rcParams if k.startswith("keymap.")}))
- def stop_typing(self):
- if self.capturekeystrokes:
- self._on_stop_typing()
- self._on_stop_typing = None
- notifysubmit = True
- else:
- notifysubmit = False
- self.capturekeystrokes = False
- self.cursor.set_visible(False)
- self.ax.figure.canvas.draw()
- if notifysubmit:
- # Because _notify_submit_observers might throw an error in the
- # user's code, only call it once we've already done our cleanup.
- self._notify_submit_observers()
- def position_cursor(self, x):
- # now, we have to figure out where the cursor goes.
- # approximate it based on assuming all characters the same length
- if len(self.text) == 0:
- self.cursor_index = 0
- else:
- bb = self.text_disp.get_window_extent()
- ratio = np.clip((x - bb.x0) / bb.width, 0, 1)
- self.cursor_index = int(len(self.text) * ratio)
- self._rendercursor()
- def _click(self, event):
- if self.ignore(event):
- return
- if event.inaxes != self.ax:
- self.stop_typing()
- return
- if not self.eventson:
- return
- if event.canvas.mouse_grabber != self.ax:
- event.canvas.grab_mouse(self.ax)
- if not self.capturekeystrokes:
- self.begin_typing(event.x)
- self.position_cursor(event.x)
- def _resize(self, event):
- self.stop_typing()
- def _motion(self, event):
- if self.ignore(event):
- return
- c = self.hovercolor if event.inaxes == self.ax else self.color
- if not colors.same_color(c, self.ax.get_facecolor()):
- self.ax.set_facecolor(c)
- if self.drawon:
- self.ax.figure.canvas.draw()
- def on_text_change(self, func):
- """
- When the text changes, call this *func* with event.
- A connection id is returned which can be used to disconnect.
- """
- cid = self.cnt
- self.change_observers[cid] = func
- self.cnt += 1
- return cid
- def on_submit(self, func):
- """
- When the user hits enter or leaves the submission box, call this
- *func* with event.
- A connection id is returned which can be used to disconnect.
- """
- cid = self.cnt
- self.submit_observers[cid] = func
- self.cnt += 1
- return cid
- def disconnect(self, cid):
- """Remove the observer with connection id *cid*."""
- for reg in [self.change_observers, self.submit_observers]:
- try:
- del reg[cid]
- except KeyError:
- pass
- class RadioButtons(AxesWidget):
- """
- A GUI neutral radio button.
- For the buttons to remain responsive you must keep a reference to this
- object.
- Connect to the RadioButtons with the `.on_clicked` method.
- Attributes
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- activecolor : color
- The color of the selected button.
- labels : list of `.Text`
- The button labels.
- circles : list of `~.patches.Circle`
- The buttons.
- value_selected : str
- The label text of the currently selected button.
- """
- def __init__(self, ax, labels, active=0, activecolor='blue'):
- """
- Add radio buttons to an `~.axes.Axes`.
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The axes to add the buttons to.
- labels : list of str
- The button labels.
- active : int
- The index of the initially selected button.
- activecolor : color
- The color of the selected button.
- """
- AxesWidget.__init__(self, ax)
- self.activecolor = activecolor
- self.value_selected = None
- ax.set_xticks([])
- ax.set_yticks([])
- ax.set_navigate(False)
- dy = 1. / (len(labels) + 1)
- ys = np.linspace(1 - dy, dy, len(labels))
- cnt = 0
- axcolor = ax.get_facecolor()
- # scale the radius of the circle with the spacing between each one
- circle_radius = dy / 2 - 0.01
- # default to hard-coded value if the radius becomes too large
- circle_radius = min(circle_radius, 0.05)
- self.labels = []
- self.circles = []
- for y, label in zip(ys, labels):
- t = ax.text(0.25, y, label, transform=ax.transAxes,
- horizontalalignment='left',
- verticalalignment='center')
- if cnt == active:
- self.value_selected = label
- facecolor = activecolor
- else:
- facecolor = axcolor
- p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
- facecolor=facecolor, transform=ax.transAxes)
- self.labels.append(t)
- self.circles.append(p)
- ax.add_patch(p)
- cnt += 1
- self.connect_event('button_press_event', self._clicked)
- self.cnt = 0
- self.observers = {}
- def _clicked(self, event):
- if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
- return
- pclicked = self.ax.transAxes.inverted().transform((event.x, event.y))
- distances = {}
- for i, (p, t) in enumerate(zip(self.circles, self.labels)):
- if (t.get_window_extent().contains(event.x, event.y)
- or np.linalg.norm(pclicked - p.center) < p.radius):
- distances[i] = np.linalg.norm(pclicked - p.center)
- if len(distances) > 0:
- closest = min(distances, key=distances.get)
- self.set_active(closest)
- def set_active(self, index):
- """
- Select button with number *index*.
- Callbacks will be triggered if :attr:`eventson` is True.
- """
- if 0 > index >= len(self.labels):
- raise ValueError("Invalid RadioButton index: %d" % index)
- self.value_selected = self.labels[index].get_text()
- for i, p in enumerate(self.circles):
- if i == index:
- color = self.activecolor
- else:
- color = self.ax.get_facecolor()
- p.set_facecolor(color)
- if self.drawon:
- self.ax.figure.canvas.draw()
- if not self.eventson:
- return
- for cid, func in self.observers.items():
- func(self.labels[index].get_text())
- def on_clicked(self, func):
- """
- Connect the callback function *func* to button click events.
- Returns a connection id, which can be used to disconnect the callback.
- """
- cid = self.cnt
- self.observers[cid] = func
- self.cnt += 1
- return cid
- def disconnect(self, cid):
- """Remove the observer with connection id *cid*."""
- try:
- del self.observers[cid]
- except KeyError:
- pass
- class SubplotTool(Widget):
- """
- A tool to adjust the subplot params of a `matplotlib.figure.Figure`.
- """
- def __init__(self, targetfig, toolfig):
- """
- Parameters
- ----------
- targetfig : `.Figure`
- The figure instance to adjust.
- toolfig : `.Figure`
- The figure instance to embed the subplot tool into.
- """
- self.targetfig = targetfig
- toolfig.subplots_adjust(left=0.2, right=0.9)
- toolfig.suptitle("Click on slider to adjust subplot param")
- self._sliders = []
- names = ["left", "bottom", "right", "top", "wspace", "hspace"]
- # The last subplot, removed below, keeps space for the "Reset" button.
- for name, ax in zip(names, toolfig.subplots(len(names) + 1)):
- ax.set_navigate(False)
- slider = Slider(ax, name,
- 0, 1, getattr(targetfig.subplotpars, name))
- slider.on_changed(self._on_slider_changed)
- self._sliders.append(slider)
- toolfig.axes[-1].remove()
- (self.sliderleft, self.sliderbottom, self.sliderright, self.slidertop,
- self.sliderwspace, self.sliderhspace) = self._sliders
- for slider in [self.sliderleft, self.sliderbottom,
- self.sliderwspace, self.sliderhspace]:
- slider.closedmax = False
- for slider in [self.sliderright, self.slidertop]:
- slider.closedmin = False
- # constraints
- self.sliderleft.slidermax = self.sliderright
- self.sliderright.slidermin = self.sliderleft
- self.sliderbottom.slidermax = self.slidertop
- self.slidertop.slidermin = self.sliderbottom
- bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075])
- self.buttonreset = Button(bax, 'Reset')
- # During reset there can be a temporary invalid state depending on the
- # order of the reset so we turn off validation for the resetting
- with cbook._setattr_cm(toolfig.subplotpars, validate=False):
- self.buttonreset.on_clicked(self._on_reset)
- def _on_slider_changed(self, _):
- self.targetfig.subplots_adjust(
- **{slider.label.get_text(): slider.val
- for slider in self._sliders})
- if self.drawon:
- self.targetfig.canvas.draw()
- def _on_reset(self, event):
- with ExitStack() as stack:
- # Temporarily disable drawing on self and self's sliders.
- stack.enter_context(cbook._setattr_cm(self, drawon=False))
- for slider in self._sliders:
- stack.enter_context(cbook._setattr_cm(slider, drawon=False))
- # Reset the slider to the initial position.
- for slider in self._sliders:
- slider.reset()
- # Draw the canvas.
- if self.drawon:
- event.canvas.draw()
- self.targetfig.canvas.draw()
- axleft = cbook.deprecated("3.3", name="axleft")(
- property(lambda self: self.sliderleft.ax))
- axright = cbook.deprecated("3.3", name="axright")(
- property(lambda self: self.sliderright.ax))
- axbottom = cbook.deprecated("3.3", name="axbottom")(
- property(lambda self: self.sliderbottom.ax))
- axtop = cbook.deprecated("3.3", name="axtop")(
- property(lambda self: self.slidertop.ax))
- axwspace = cbook.deprecated("3.3", name="axwspace")(
- property(lambda self: self.sliderwspace.ax))
- axhspace = cbook.deprecated("3.3", name="axhspace")(
- property(lambda self: self.sliderhspace.ax))
- @cbook.deprecated("3.3")
- def funcleft(self, val):
- self.targetfig.subplots_adjust(left=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- @cbook.deprecated("3.3")
- def funcright(self, val):
- self.targetfig.subplots_adjust(right=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- @cbook.deprecated("3.3")
- def funcbottom(self, val):
- self.targetfig.subplots_adjust(bottom=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- @cbook.deprecated("3.3")
- def functop(self, val):
- self.targetfig.subplots_adjust(top=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- @cbook.deprecated("3.3")
- def funcwspace(self, val):
- self.targetfig.subplots_adjust(wspace=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- @cbook.deprecated("3.3")
- def funchspace(self, val):
- self.targetfig.subplots_adjust(hspace=val)
- if self.drawon:
- self.targetfig.canvas.draw()
- class Cursor(AxesWidget):
- """
- A crosshair cursor that spans the axes and moves with mouse cursor.
- For the cursor to remain responsive you must keep a reference to it.
- Parameters
- ----------
- ax : `matplotlib.axes.Axes`
- The `~.axes.Axes` to attach the cursor to.
- horizOn : bool, default: True
- Whether to draw the horizontal line.
- vertOn : bool, default: True
- Whether to draw the vertical line.
- useblit : bool, default: False
- Use blitting for faster drawing if supported by the backend.
- Other Parameters
- ----------------
- **lineprops
- `.Line2D` properties that control the appearance of the lines.
- See also `~.Axes.axhline`.
- Examples
- --------
- See :doc:`/gallery/widgets/cursor`.
- """
- def __init__(self, ax, horizOn=True, vertOn=True, useblit=False,
- **lineprops):
- AxesWidget.__init__(self, ax)
- self.connect_event('motion_notify_event', self.onmove)
- self.connect_event('draw_event', self.clear)
- self.visible = True
- self.horizOn = horizOn
- self.vertOn = vertOn
- self.useblit = useblit and self.canvas.supports_blit
- if self.useblit:
- lineprops['animated'] = True
- self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
- self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
- self.background = None
- self.needclear = False
- def clear(self, event):
- """Internal event handler to clear the cursor."""
- if self.ignore(event):
- return
- if self.useblit:
- self.background = self.canvas.copy_from_bbox(self.ax.bbox)
- self.linev.set_visible(False)
- self.lineh.set_visible(False)
- def onmove(self, event):
- """Internal event handler to draw the cursor when the mouse moves."""
- if self.ignore(event):
- return
- if not self.canvas.widgetlock.available(self):
- return
- if event.inaxes != self.ax:
- self.linev.set_visible(False)
- self.lineh.set_visible(False)
- if self.needclear:
- self.canvas.draw()
- self.needclear = False
- return
- self.needclear = True
- if not self.visible:
- return
- self.linev.set_xdata((event.xdata, event.xdata))
- self.lineh.set_ydata((event.ydata, event.ydata))
- self.linev.set_visible(self.visible and self.vertOn)
- self.lineh.set_visible(self.visible and self.horizOn)
- self._update()
- def _update(self):
- if self.useblit:
- if self.background is not None:
- self.canvas.restore_region(self.background)
- self.ax.draw_artist(self.linev)
- self.ax.draw_artist(self.lineh)
- self.canvas.blit(self.ax.bbox)
- else:
- self.canvas.draw_idle()
- return False
- class MultiCursor(Widget):
- """
- Provide a vertical (default) and/or horizontal line cursor shared between
- multiple axes.
- For the cursor to remain responsive you must keep a reference to it.
- Example usage::
- from matplotlib.widgets import MultiCursor
- import matplotlib.pyplot as plt
- import numpy as np
- fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
- t = np.arange(0.0, 2.0, 0.01)
- ax1.plot(t, np.sin(2*np.pi*t))
- ax2.plot(t, np.sin(4*np.pi*t))
- multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1,
- horizOn=False, vertOn=True)
- plt.show()
- """
- def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True,
- **lineprops):
- self.canvas = canvas
- self.axes = axes
- self.horizOn = horizOn
- self.vertOn = vertOn
- xmin, xmax = axes[-1].get_xlim()
- ymin, ymax = axes[-1].get_ylim()
- xmid = 0.5 * (xmin + xmax)
- ymid = 0.5 * (ymin + ymax)
- self.visible = True
- self.useblit = useblit and self.canvas.supports_blit
- self.background = None
- self.needclear = False
- if self.useblit:
- lineprops['animated'] = True
- if vertOn:
- self.vlines = [ax.axvline(xmid, visible=False, **lineprops)
- for ax in axes]
- else:
- self.vlines = []
- if horizOn:
- self.hlines = [ax.axhline(ymid, visible=False, **lineprops)
- for ax in axes]
- else:
- self.hlines = []
- self.connect()
- def connect(self):
- """Connect events."""
- self._cidmotion = self.canvas.mpl_connect('motion_notify_event',
- self.onmove)
- self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear)
- def disconnect(self):
- """Disconnect events."""
- self.canvas.mpl_disconnect(self._cidmotion)
- self.canvas.mpl_disconnect(self._ciddraw)
- def clear(self, event):
- """Clear the cursor."""
- if self.ignore(event):
- return
- if self.useblit:
- self.background = (
- self.canvas.copy_from_bbox(self.canvas.figure.bbox))
- for line in self.vlines + self.hlines:
- line.set_visible(False)
- def onmove(self, event):
- if self.ignore(event):
- return
- if event.inaxes is None:
- return
- if not self.canvas.widgetlock.available(self):
- return
- self.needclear = True
- if not self.visible:
- return
- if self.vertOn:
- for line in self.vlines:
- line.set_xdata((event.xdata, event.xdata))
- line.set_visible(self.visible)
- if self.horizOn:
- for line in self.hlines:
- line.set_ydata((event.ydata, event.ydata))
- line.set_visible(self.visible)
- self._update()
- def _update(self):
- if self.useblit:
- if self.background is not None:
- self.canvas.restore_region(self.background)
- if self.vertOn:
- for ax, line in zip(self.axes, self.vlines):
- ax.draw_artist(line)
- if self.horizOn:
- for ax, line in zip(self.axes, self.hlines):
- ax.draw_artist(line)
- self.canvas.blit()
- else:
- self.canvas.draw_idle()
- class _SelectorWidget(AxesWidget):
- def __init__(self, ax, onselect, useblit=False, button=None,
- state_modifier_keys=None):
- AxesWidget.__init__(self, ax)
- self.visible = True
- self.onselect = onselect
- self.useblit = useblit and self.canvas.supports_blit
- self.connect_default_events()
- self.state_modifier_keys = dict(move=' ', clear='escape',
- square='shift', center='control')
- self.state_modifier_keys.update(state_modifier_keys or {})
- self.background = None
- self.artists = []
- if isinstance(button, Integral):
- self.validButtons = [button]
- else:
- self.validButtons = button
- # will save the data (position at mouseclick)
- self.eventpress = None
- # will save the data (pos. at mouserelease)
- self.eventrelease = None
- self._prev_event = None
- self.state = set()
- def set_active(self, active):
- AxesWidget.set_active(self, active)
- if active:
- self.update_background(None)
- def update_background(self, event):
- """Force an update of the background."""
- # If you add a call to `ignore` here, you'll want to check edge case:
- # `release` can call a draw event even when `ignore` is True.
- if self.useblit:
- self.background = self.canvas.copy_from_bbox(self.ax.bbox)
- def connect_default_events(self):
- """Connect the major canvas events to methods."""
- self.connect_event('motion_notify_event', self.onmove)
- self.connect_event('button_press_event', self.press)
- self.connect_event('button_release_event', self.release)
- self.connect_event('draw_event', self.update_background)
- self.connect_event('key_press_event', self.on_key_press)
- self.connect_event('key_release_event', self.on_key_release)
- self.connect_event('scroll_event', self.on_scroll)
- def ignore(self, event):
- # docstring inherited
- if not self.active or not self.ax.get_visible():
- return True
- # If canvas was locked
- if not self.canvas.widgetlock.available(self):
- return True
- if not hasattr(event, 'button'):
- event.button = None
- # Only do rectangle selection if event was triggered
- # with a desired button
- if (self.validButtons is not None
- and event.button not in self.validButtons):
- return True
- # If no button was pressed yet ignore the event if it was out
- # of the axes
- if self.eventpress is None:
- return event.inaxes != self.ax
- # If a button was pressed, check if the release-button is the same.
- if event.button == self.eventpress.button:
- return False
- # If a button was pressed, check if the release-button is the same.
- return (event.inaxes != self.ax or
- event.button != self.eventpress.button)
- def update(self):
- """Draw using blit() or draw_idle(), depending on ``self.useblit``."""
- if not self.ax.get_visible():
- return False
- if self.useblit:
- if self.background is not None:
- self.canvas.restore_region(self.background)
- for artist in self.artists:
- self.ax.draw_artist(artist)
- self.canvas.blit(self.ax.bbox)
- else:
- self.canvas.draw_idle()
- return False
- def _get_data(self, event):
- """Get the xdata and ydata for event, with limits."""
- if event.xdata is None:
- return None, None
- xdata = np.clip(event.xdata, *self.ax.get_xbound())
- ydata = np.clip(event.ydata, *self.ax.get_ybound())
- return xdata, ydata
- def _clean_event(self, event):
- """
- Preprocess an event:
- - Replace *event* by the previous event if *event* has no ``xdata``.
- - Clip ``xdata`` and ``ydata`` to the axes limits.
- - Update the previous event.
- """
- if event.xdata is None:
- event = self._prev_event
- else:
- event = copy.copy(event)
- event.xdata, event.ydata = self._get_data(event)
- self._prev_event = event
- return event
- def press(self, event):
- """Button press handler and validator."""
- if not self.ignore(event):
- event = self._clean_event(event)
- self.eventpress = event
- self._prev_event = event
- key = event.key or ''
- key = key.replace('ctrl', 'control')
- # move state is locked in on a button press
- if key == self.state_modifier_keys['move']:
- self.state.add('move')
- self._press(event)
- return True
- return False
- def _press(self, event):
- """Button press handler."""
- def release(self, event):
- """Button release event handler and validator."""
- if not self.ignore(event) and self.eventpress:
- event = self._clean_event(event)
- self.eventrelease = event
- self._release(event)
- self.eventpress = None
- self.eventrelease = None
- self.state.discard('move')
- return True
- return False
- def _release(self, event):
- """Button release event handler."""
- def onmove(self, event):
- """Cursor move event handler and validator."""
- if not self.ignore(event) and self.eventpress:
- event = self._clean_event(event)
- self._onmove(event)
- return True
- return False
- def _onmove(self, event):
- """Cursor move event handler."""
- def on_scroll(self, event):
- """Mouse scroll event handler and validator."""
- if not self.ignore(event):
- self._on_scroll(event)
- def _on_scroll(self, event):
- """Mouse scroll event handler."""
- def on_key_press(self, event):
- """Key press event handler and validator for all selection widgets."""
- if self.active:
- key = event.key or ''
- key = key.replace('ctrl', 'control')
- if key == self.state_modifier_keys['clear']:
- for artist in self.artists:
- artist.set_visible(False)
- self.update()
- return
- for (state, modifier) in self.state_modifier_keys.items():
- if modifier in key:
- self.state.add(state)
- self._on_key_press(event)
- def _on_key_press(self, event):
- """Key press event handler - for widget-specific key press actions."""
- def on_key_release(self, event):
- """Key release event handler and validator."""
- if self.active:
- key = event.key or ''
- for (state, modifier) in self.state_modifier_keys.items():
- if modifier in key:
- self.state.discard(state)
- self._on_key_release(event)
- def _on_key_release(self, event):
- """Key release event handler."""
- def set_visible(self, visible):
- """Set the visibility of our artists."""
- self.visible = visible
- for artist in self.artists:
- artist.set_visible(visible)
- class SpanSelector(_SelectorWidget):
- """
- Visually select a min/max range on a single axis and call a function with
- those values.
- To guarantee that the selector remains responsive, keep a reference to it.
- In order to turn off the SpanSelector, set ``span_selector.active`` to
- False. To turn it back on, set it to True.
- Parameters
- ----------
- ax : `matplotlib.axes.Axes`
- onselect : func(min, max), min/max are floats
- direction : {"horizontal", "vertical"}
- The direction along which to draw the span selector.
- minspan : float, default: None
- If selection is less than *minspan*, do not call *onselect*.
- useblit : bool, default: False
- If True, use the backend-dependent blitting features for faster
- canvas updates.
- rectprops : dict, default: None
- Dictionary of `matplotlib.patches.Patch` properties.
- onmove_callback : func(min, max), min/max are floats, default: None
- Called on mouse move while the span is being selected.
- span_stays : bool, default: False
- If True, the span stays visible after the mouse is released.
- button : `.MouseButton` or list of `.MouseButton`
- The mouse buttons which activate the span selector.
- Examples
- --------
- >>> import matplotlib.pyplot as plt
- >>> import matplotlib.widgets as mwidgets
- >>> fig, ax = plt.subplots()
- >>> ax.plot([1, 2, 3], [10, 50, 100])
- >>> def onselect(vmin, vmax):
- ... print(vmin, vmax)
- >>> rectprops = dict(facecolor='blue', alpha=0.5)
- >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal',
- ... rectprops=rectprops)
- >>> fig.show()
- See also: :doc:`/gallery/widgets/span_selector`
- """
- def __init__(self, ax, onselect, direction, minspan=None, useblit=False,
- rectprops=None, onmove_callback=None, span_stays=False,
- button=None):
- _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
- button=button)
- if rectprops is None:
- rectprops = dict(facecolor='red', alpha=0.5)
- rectprops['animated'] = self.useblit
- cbook._check_in_list(['horizontal', 'vertical'], direction=direction)
- self.direction = direction
- self.rect = None
- self.pressv = None
- self.rectprops = rectprops
- self.onmove_callback = onmove_callback
- self.minspan = minspan
- self.span_stays = span_stays
- # Needed when dragging out of axes
- self.prev = (0, 0)
- # Reset canvas so that `new_axes` connects events.
- self.canvas = None
- self.new_axes(ax)
- def new_axes(self, ax):
- """Set SpanSelector to operate on a new Axes."""
- self.ax = ax
- if self.canvas is not ax.figure.canvas:
- if self.canvas is not None:
- self.disconnect_events()
- self.canvas = ax.figure.canvas
- self.connect_default_events()
- if self.direction == 'horizontal':
- trans = blended_transform_factory(self.ax.transData,
- self.ax.transAxes)
- w, h = 0, 1
- else:
- trans = blended_transform_factory(self.ax.transAxes,
- self.ax.transData)
- w, h = 1, 0
- self.rect = Rectangle((0, 0), w, h,
- transform=trans,
- visible=False,
- **self.rectprops)
- if self.span_stays:
- self.stay_rect = Rectangle((0, 0), w, h,
- transform=trans,
- visible=False,
- **self.rectprops)
- self.stay_rect.set_animated(False)
- self.ax.add_patch(self.stay_rect)
- self.ax.add_patch(self.rect)
- self.artists = [self.rect]
- def ignore(self, event):
- # docstring inherited
- return _SelectorWidget.ignore(self, event) or not self.visible
- def _press(self, event):
- """on button press event"""
- self.rect.set_visible(self.visible)
- if self.span_stays:
- self.stay_rect.set_visible(False)
- # really force a draw so that the stay rect is not in
- # the blit background
- if self.useblit:
- self.canvas.draw()
- xdata, ydata = self._get_data(event)
- if self.direction == 'horizontal':
- self.pressv = xdata
- else:
- self.pressv = ydata
- self._set_span_xy(event)
- return False
- def _release(self, event):
- """on button release event"""
- if self.pressv is None:
- return
- self.rect.set_visible(False)
- if self.span_stays:
- self.stay_rect.set_x(self.rect.get_x())
- self.stay_rect.set_y(self.rect.get_y())
- self.stay_rect.set_width(self.rect.get_width())
- self.stay_rect.set_height(self.rect.get_height())
- self.stay_rect.set_visible(True)
- self.canvas.draw_idle()
- vmin = self.pressv
- xdata, ydata = self._get_data(event)
- if self.direction == 'horizontal':
- vmax = xdata or self.prev[0]
- else:
- vmax = ydata or self.prev[1]
- if vmin > vmax:
- vmin, vmax = vmax, vmin
- span = vmax - vmin
- if self.minspan is not None and span < self.minspan:
- return
- self.onselect(vmin, vmax)
- self.pressv = None
- return False
- def _onmove(self, event):
- """on motion notify event"""
- if self.pressv is None:
- return
- self._set_span_xy(event)
- if self.onmove_callback is not None:
- vmin = self.pressv
- xdata, ydata = self._get_data(event)
- if self.direction == 'horizontal':
- vmax = xdata or self.prev[0]
- else:
- vmax = ydata or self.prev[1]
- if vmin > vmax:
- vmin, vmax = vmax, vmin
- self.onmove_callback(vmin, vmax)
- self.update()
- return False
- def _set_span_xy(self, event):
- """Set the span coordinates."""
- x, y = self._get_data(event)
- if x is None:
- return
- self.prev = x, y
- if self.direction == 'horizontal':
- v = x
- else:
- v = y
- minv, maxv = v, self.pressv
- if minv > maxv:
- minv, maxv = maxv, minv
- if self.direction == 'horizontal':
- self.rect.set_x(minv)
- self.rect.set_width(maxv - minv)
- else:
- self.rect.set_y(minv)
- self.rect.set_height(maxv - minv)
- class ToolHandles:
- """
- Control handles for canvas tools.
- Parameters
- ----------
- ax : `matplotlib.axes.Axes`
- Matplotlib axes where tool handles are displayed.
- x, y : 1D arrays
- Coordinates of control handles.
- marker : str
- Shape of marker used to display handle. See `matplotlib.pyplot.plot`.
- marker_props : dict
- Additional marker properties. See `matplotlib.lines.Line2D`.
- """
- def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True):
- self.ax = ax
- props = dict(marker=marker, markersize=7, mfc='w', ls='none',
- alpha=0.5, visible=False, label='_nolegend_')
- props.update(marker_props if marker_props is not None else {})
- self._markers = Line2D(x, y, animated=useblit, **props)
- self.ax.add_line(self._markers)
- self.artist = self._markers
- @property
- def x(self):
- return self._markers.get_xdata()
- @property
- def y(self):
- return self._markers.get_ydata()
- def set_data(self, pts, y=None):
- """Set x and y positions of handles"""
- if y is not None:
- x = pts
- pts = np.array([x, y])
- self._markers.set_data(pts)
- def set_visible(self, val):
- self._markers.set_visible(val)
- def set_animated(self, val):
- self._markers.set_animated(val)
- def closest(self, x, y):
- """Return index and pixel distance to closest index."""
- pts = np.column_stack([self.x, self.y])
- # Transform data coordinates to pixel coordinates.
- pts = self.ax.transData.transform(pts)
- diff = pts - [x, y]
- dist = np.hypot(*diff.T)
- min_index = np.argmin(dist)
- return min_index, dist[min_index]
- class RectangleSelector(_SelectorWidget):
- """
- Select a rectangular region of an axes.
- For the cursor to remain responsive you must keep a reference to it.
- Examples
- --------
- :doc:`/gallery/widgets/rectangle_selector`
- """
- _shape_klass = Rectangle
- def __init__(self, ax, onselect, drawtype='box',
- minspanx=0, minspany=0, useblit=False,
- lineprops=None, rectprops=None, spancoords='data',
- button=None, maxdist=10, marker_props=None,
- interactive=False, state_modifier_keys=None):
- r"""
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- onselect : function
- A callback function that is called after a selection is completed.
- It must have the signature::
- def onselect(eclick: MouseEvent, erelease: MouseEvent)
- where *eclick* and *erelease* are the mouse click and release
- `.MouseEvent`\s that start and complete the selection.
- drawtype : {"box", "line", "none"}, default: "box"
- Whether to draw the full rectangle box, the diagonal line of the
- rectangle, or nothing at all.
- minspanx : float, default: 0
- Selections with an x-span less than *minspanx* are ignored.
- minspany : float, default: 0
- Selections with an y-span less than *minspany* are ignored.
- useblit : bool, default: False
- Whether to use blitting for faster drawing (if supported by the
- backend).
- lineprops : dict, optional
- Properties with which the line is drawn, if ``drawtype == "line"``.
- Default::
- dict(color="black", linestyle="-", linewidth=2, alpha=0.5)
- rectprops : dict, optional
- Properties with which the rectangle is drawn, if ``drawtype ==
- "box"``. Default::
- dict(facecolor="red", edgecolor="black", alpha=0.2, fill=True)
- spancoords : {"data", "pixels"}, default: "data"
- Whether to interpret *minspanx* and *minspany* in data or in pixel
- coordinates.
- button : `.MouseButton`, list of `.MouseButton`, default: all buttons
- Button(s) that trigger rectangle selection.
- maxdist : float, default: 10
- Distance in pixels within which the interactive tool handles can be
- activated.
- marker_props : dict
- Properties with which the interactive handles are drawn. Currently
- not implemented and ignored.
- interactive : bool, default: False
- Whether to draw a set of handles that allow interaction with the
- widget after it is drawn.
- state_modifier_keys : dict, optional
- Keyboard modifiers which affect the widget's behavior. Values
- amend the defaults.
- - "move": Move the existing shape, default: no modifier.
- - "clear": Clear the current shape, default: "escape".
- - "square": Makes the shape square, default: "shift".
- - "center": Make the initial point the center of the shape,
- default: "ctrl".
- "square" and "center" can be combined.
- """
- _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
- button=button,
- state_modifier_keys=state_modifier_keys)
- self.to_draw = None
- self.visible = True
- self.interactive = interactive
- if drawtype == 'none': # draw a line but make it invisible
- drawtype = 'line'
- self.visible = False
- if drawtype == 'box':
- if rectprops is None:
- rectprops = dict(facecolor='red', edgecolor='black',
- alpha=0.2, fill=True)
- rectprops['animated'] = self.useblit
- self.rectprops = rectprops
- self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
- **self.rectprops)
- self.ax.add_patch(self.to_draw)
- if drawtype == 'line':
- if lineprops is None:
- lineprops = dict(color='black', linestyle='-',
- linewidth=2, alpha=0.5)
- lineprops['animated'] = self.useblit
- self.lineprops = lineprops
- self.to_draw = Line2D([0, 0], [0, 0], visible=False,
- **self.lineprops)
- self.ax.add_line(self.to_draw)
- self.minspanx = minspanx
- self.minspany = minspany
- cbook._check_in_list(['data', 'pixels'], spancoords=spancoords)
- self.spancoords = spancoords
- self.drawtype = drawtype
- self.maxdist = maxdist
- if rectprops is None:
- props = dict(mec='r')
- else:
- props = dict(mec=rectprops.get('edgecolor', 'r'))
- self._corner_order = ['NW', 'NE', 'SE', 'SW']
- xc, yc = self.corners
- self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props,
- useblit=self.useblit)
- self._edge_order = ['W', 'N', 'E', 'S']
- xe, ye = self.edge_centers
- self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
- marker_props=props,
- useblit=self.useblit)
- xc, yc = self.center
- self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
- marker_props=props,
- useblit=self.useblit)
- self.active_handle = None
- self.artists = [self.to_draw, self._center_handle.artist,
- self._corner_handles.artist,
- self._edge_handles.artist]
- if not self.interactive:
- self.artists = [self.to_draw]
- self._extents_on_press = None
- def _press(self, event):
- """on button press event"""
- # make the drawn box/line visible get the click-coordinates,
- # button, ...
- if self.interactive and self.to_draw.get_visible():
- self._set_active_handle(event)
- else:
- self.active_handle = None
- if self.active_handle is None or not self.interactive:
- # Clear previous rectangle before drawing new rectangle.
- self.update()
- if not self.interactive:
- x = event.xdata
- y = event.ydata
- self.extents = x, x, y, y
- self.set_visible(self.visible)
- def _release(self, event):
- """on button release event"""
- if not self.interactive:
- self.to_draw.set_visible(False)
- # update the eventpress and eventrelease with the resulting extents
- x1, x2, y1, y2 = self.extents
- self.eventpress.xdata = x1
- self.eventpress.ydata = y1
- xy1 = self.ax.transData.transform([x1, y1])
- self.eventpress.x, self.eventpress.y = xy1
- self.eventrelease.xdata = x2
- self.eventrelease.ydata = y2
- xy2 = self.ax.transData.transform([x2, y2])
- self.eventrelease.x, self.eventrelease.y = xy2
- # calculate dimensions of box or line
- if self.spancoords == 'data':
- spanx = abs(self.eventpress.xdata - self.eventrelease.xdata)
- spany = abs(self.eventpress.ydata - self.eventrelease.ydata)
- elif self.spancoords == 'pixels':
- spanx = abs(self.eventpress.x - self.eventrelease.x)
- spany = abs(self.eventpress.y - self.eventrelease.y)
- else:
- cbook._check_in_list(['data', 'pixels'],
- spancoords=self.spancoords)
- # check if drawn distance (if it exists) is not too small in
- # either x or y-direction
- if (self.drawtype != 'none'
- and (self.minspanx is not None and spanx < self.minspanx
- or self.minspany is not None and spany < self.minspany)):
- for artist in self.artists:
- artist.set_visible(False)
- self.update()
- return
- # call desired function
- self.onselect(self.eventpress, self.eventrelease)
- self.update()
- return False
- def _onmove(self, event):
- """on motion notify event if box/line is wanted"""
- # resize an existing shape
- if self.active_handle and self.active_handle != 'C':
- x1, x2, y1, y2 = self._extents_on_press
- if self.active_handle in ['E', 'W'] + self._corner_order:
- x2 = event.xdata
- if self.active_handle in ['N', 'S'] + self._corner_order:
- y2 = event.ydata
- # move existing shape
- elif (('move' in self.state or self.active_handle == 'C')
- and self._extents_on_press is not None):
- x1, x2, y1, y2 = self._extents_on_press
- dx = event.xdata - self.eventpress.xdata
- dy = event.ydata - self.eventpress.ydata
- x1 += dx
- x2 += dx
- y1 += dy
- y2 += dy
- # new shape
- else:
- center = [self.eventpress.xdata, self.eventpress.ydata]
- center_pix = [self.eventpress.x, self.eventpress.y]
- dx = (event.xdata - center[0]) / 2.
- dy = (event.ydata - center[1]) / 2.
- # square shape
- if 'square' in self.state:
- dx_pix = abs(event.x - center_pix[0])
- dy_pix = abs(event.y - center_pix[1])
- if not dx_pix:
- return
- maxd = max(abs(dx_pix), abs(dy_pix))
- if abs(dx_pix) < maxd:
- dx *= maxd / (abs(dx_pix) + 1e-6)
- if abs(dy_pix) < maxd:
- dy *= maxd / (abs(dy_pix) + 1e-6)
- # from center
- if 'center' in self.state:
- dx *= 2
- dy *= 2
- # from corner
- else:
- center[0] += dx
- center[1] += dy
- x1, x2, y1, y2 = (center[0] - dx, center[0] + dx,
- center[1] - dy, center[1] + dy)
- self.extents = x1, x2, y1, y2
- @property
- def _rect_bbox(self):
- if self.drawtype == 'box':
- x0 = self.to_draw.get_x()
- y0 = self.to_draw.get_y()
- width = self.to_draw.get_width()
- height = self.to_draw.get_height()
- return x0, y0, width, height
- else:
- x, y = self.to_draw.get_data()
- x0, x1 = min(x), max(x)
- y0, y1 = min(y), max(y)
- return x0, y0, x1 - x0, y1 - y0
- @property
- def corners(self):
- """Corners of rectangle from lower left, moving clockwise."""
- x0, y0, width, height = self._rect_bbox
- xc = x0, x0 + width, x0 + width, x0
- yc = y0, y0, y0 + height, y0 + height
- return xc, yc
- @property
- def edge_centers(self):
- """Midpoint of rectangle edges from left, moving clockwise."""
- x0, y0, width, height = self._rect_bbox
- w = width / 2.
- h = height / 2.
- xe = x0, x0 + w, x0 + width, x0 + w
- ye = y0 + h, y0, y0 + h, y0 + height
- return xe, ye
- @property
- def center(self):
- """Center of rectangle"""
- x0, y0, width, height = self._rect_bbox
- return x0 + width / 2., y0 + height / 2.
- @property
- def extents(self):
- """Return (xmin, xmax, ymin, ymax)."""
- x0, y0, width, height = self._rect_bbox
- xmin, xmax = sorted([x0, x0 + width])
- ymin, ymax = sorted([y0, y0 + height])
- return xmin, xmax, ymin, ymax
- @extents.setter
- def extents(self, extents):
- # Update displayed shape
- self.draw_shape(extents)
- # Update displayed handles
- self._corner_handles.set_data(*self.corners)
- self._edge_handles.set_data(*self.edge_centers)
- self._center_handle.set_data(*self.center)
- self.set_visible(self.visible)
- self.update()
- def draw_shape(self, extents):
- x0, x1, y0, y1 = extents
- xmin, xmax = sorted([x0, x1])
- ymin, ymax = sorted([y0, y1])
- xlim = sorted(self.ax.get_xlim())
- ylim = sorted(self.ax.get_ylim())
- xmin = max(xlim[0], xmin)
- ymin = max(ylim[0], ymin)
- xmax = min(xmax, xlim[1])
- ymax = min(ymax, ylim[1])
- if self.drawtype == 'box':
- self.to_draw.set_x(xmin)
- self.to_draw.set_y(ymin)
- self.to_draw.set_width(xmax - xmin)
- self.to_draw.set_height(ymax - ymin)
- elif self.drawtype == 'line':
- self.to_draw.set_data([xmin, xmax], [ymin, ymax])
- def _set_active_handle(self, event):
- """Set active handle based on the location of the mouse event"""
- # Note: event.xdata/ydata in data coordinates, event.x/y in pixels
- c_idx, c_dist = self._corner_handles.closest(event.x, event.y)
- e_idx, e_dist = self._edge_handles.closest(event.x, event.y)
- m_idx, m_dist = self._center_handle.closest(event.x, event.y)
- if 'move' in self.state:
- self.active_handle = 'C'
- self._extents_on_press = self.extents
- # Set active handle as closest handle, if mouse click is close enough.
- elif m_dist < self.maxdist * 2:
- self.active_handle = 'C'
- elif c_dist > self.maxdist and e_dist > self.maxdist:
- self.active_handle = None
- return
- elif c_dist < e_dist:
- self.active_handle = self._corner_order[c_idx]
- else:
- self.active_handle = self._edge_order[e_idx]
- # Save coordinates of rectangle at the start of handle movement.
- x1, x2, y1, y2 = self.extents
- # Switch variables so that only x2 and/or y2 are updated on move.
- if self.active_handle in ['W', 'SW', 'NW']:
- x1, x2 = x2, event.xdata
- if self.active_handle in ['N', 'NW', 'NE']:
- y1, y2 = y2, event.ydata
- self._extents_on_press = x1, x2, y1, y2
- @property
- def geometry(self):
- """
- Return an array of shape (2, 5) containing the
- x (``RectangleSelector.geometry[1, :]``) and
- y (``RectangleSelector.geometry[0, :]``) coordinates
- of the four corners of the rectangle starting and ending
- in the top left corner.
- """
- if hasattr(self.to_draw, 'get_verts'):
- xfm = self.ax.transData.inverted()
- y, x = xfm.transform(self.to_draw.get_verts()).T
- return np.array([x, y])
- else:
- return np.array(self.to_draw.get_data())
- class EllipseSelector(RectangleSelector):
- """
- Select an elliptical region of an axes.
- For the cursor to remain responsive you must keep a reference to it.
- Example usage::
- import numpy as np
- import matplotlib.pyplot as plt
- from matplotlib.widgets import EllipseSelector
- def onselect(eclick, erelease):
- "eclick and erelease are matplotlib events at press and release."
- print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
- print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
- print('used button : ', eclick.button)
- def toggle_selector(event):
- print(' Key pressed.')
- if event.key in ['Q', 'q'] and toggle_selector.ES.active:
- print('EllipseSelector deactivated.')
- toggle_selector.RS.set_active(False)
- if event.key in ['A', 'a'] and not toggle_selector.ES.active:
- print('EllipseSelector activated.')
- toggle_selector.ES.set_active(True)
- x = np.arange(100.) / 99
- y = np.sin(x)
- fig, ax = plt.subplots()
- ax.plot(x, y)
- toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line')
- fig.canvas.mpl_connect('key_press_event', toggle_selector)
- plt.show()
- """
- _shape_klass = Ellipse
- def draw_shape(self, extents):
- x1, x2, y1, y2 = extents
- xmin, xmax = sorted([x1, x2])
- ymin, ymax = sorted([y1, y2])
- center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.]
- a = (xmax - xmin) / 2.
- b = (ymax - ymin) / 2.
- if self.drawtype == 'box':
- self.to_draw.center = center
- self.to_draw.width = 2 * a
- self.to_draw.height = 2 * b
- else:
- rad = np.deg2rad(np.arange(31) * 12)
- x = a * np.cos(rad) + center[0]
- y = b * np.sin(rad) + center[1]
- self.to_draw.set_data(x, y)
- @property
- def _rect_bbox(self):
- if self.drawtype == 'box':
- x, y = self.to_draw.center
- width = self.to_draw.width
- height = self.to_draw.height
- return x - width / 2., y - height / 2., width, height
- else:
- x, y = self.to_draw.get_data()
- x0, x1 = min(x), max(x)
- y0, y1 = min(y), max(y)
- return x0, y0, x1 - x0, y1 - y0
- class LassoSelector(_SelectorWidget):
- """
- Selection curve of an arbitrary shape.
- For the selector to remain responsive you must keep a reference to it.
- The selected path can be used in conjunction with `~.Path.contains_point`
- to select data points from an image.
- In contrast to `Lasso`, `LassoSelector` is written with an interface
- similar to `RectangleSelector` and `SpanSelector`, and will continue to
- interact with the axes until disconnected.
- Example usage::
- ax = subplot(111)
- ax.plot(x, y)
- def onselect(verts):
- print(verts)
- lasso = LassoSelector(ax, onselect)
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- onselect : function
- Whenever the lasso is released, the *onselect* function is called and
- passed the vertices of the selected path.
- button : `.MouseButton` or list of `.MouseButton`, optional
- The mouse buttons used for rectangle selection. Default is ``None``,
- which corresponds to all buttons.
- """
- def __init__(self, ax, onselect=None, useblit=True, lineprops=None,
- button=None):
- _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
- button=button)
- self.verts = None
- if lineprops is None:
- lineprops = dict()
- # self.useblit may be != useblit, if the canvas doesn't support blit.
- lineprops.update(animated=self.useblit, visible=False)
- self.line = Line2D([], [], **lineprops)
- self.ax.add_line(self.line)
- self.artists = [self.line]
- def onpress(self, event):
- self.press(event)
- def _press(self, event):
- self.verts = [self._get_data(event)]
- self.line.set_visible(True)
- def onrelease(self, event):
- self.release(event)
- def _release(self, event):
- if self.verts is not None:
- self.verts.append(self._get_data(event))
- self.onselect(self.verts)
- self.line.set_data([[], []])
- self.line.set_visible(False)
- self.verts = None
- def _onmove(self, event):
- if self.verts is None:
- return
- self.verts.append(self._get_data(event))
- self.line.set_data(list(zip(*self.verts)))
- self.update()
- class PolygonSelector(_SelectorWidget):
- """
- Select a polygon region of an axes.
- Place vertices with each mouse click, and make the selection by completing
- the polygon (clicking on the first vertex). Hold the *ctrl* key and click
- and drag a vertex to reposition it (the *ctrl* key is not necessary if the
- polygon has already been completed). Hold the *shift* key and click and
- drag anywhere in the axes to move all vertices. Press the *esc* key to
- start a new polygon.
- For the selector to remain responsive you must keep a reference to it.
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- onselect : function
- When a polygon is completed or modified after completion,
- the *onselect* function is called and passed a list of the vertices as
- ``(xdata, ydata)`` tuples.
- useblit : bool, default: False
- lineprops : dict, default: \
- ``dict(color='k', linestyle='-', linewidth=2, alpha=0.5)``.
- Artist properties for the line representing the edges of the polygon.
- markerprops : dict, default: \
- ``dict(marker='o', markersize=7, mec='k', mfc='k', alpha=0.5)``.
- Artist properties for the markers drawn at the vertices of the polygon.
- vertex_select_radius : float, default: 15px
- A vertex is selected (to complete the polygon or to move a vertex) if
- the mouse click is within *vertex_select_radius* pixels of the vertex.
- Examples
- --------
- :doc:`/gallery/widgets/polygon_selector_demo`
- """
- def __init__(self, ax, onselect, useblit=False,
- lineprops=None, markerprops=None, vertex_select_radius=15):
- # The state modifiers 'move', 'square', and 'center' are expected by
- # _SelectorWidget but are not supported by PolygonSelector
- # Note: could not use the existing 'move' state modifier in-place of
- # 'move_all' because _SelectorWidget automatically discards 'move'
- # from the state on button release.
- state_modifier_keys = dict(clear='escape', move_vertex='control',
- move_all='shift', move='not-applicable',
- square='not-applicable',
- center='not-applicable')
- _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
- state_modifier_keys=state_modifier_keys)
- self._xs, self._ys = [0], [0]
- self._polygon_completed = False
- if lineprops is None:
- lineprops = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
- lineprops['animated'] = self.useblit
- self.line = Line2D(self._xs, self._ys, **lineprops)
- self.ax.add_line(self.line)
- if markerprops is None:
- markerprops = dict(mec='k', mfc=lineprops.get('color', 'k'))
- self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys,
- useblit=self.useblit,
- marker_props=markerprops)
- self._active_handle_idx = -1
- self.vertex_select_radius = vertex_select_radius
- self.artists = [self.line, self._polygon_handles.artist]
- self.set_visible(True)
- def _press(self, event):
- """Button press event handler"""
- # Check for selection of a tool handle.
- if ((self._polygon_completed or 'move_vertex' in self.state)
- and len(self._xs) > 0):
- h_idx, h_dist = self._polygon_handles.closest(event.x, event.y)
- if h_dist < self.vertex_select_radius:
- self._active_handle_idx = h_idx
- # Save the vertex positions at the time of the press event (needed to
- # support the 'move_all' state modifier).
- self._xs_at_press, self._ys_at_press = self._xs.copy(), self._ys.copy()
- def _release(self, event):
- """Button release event handler"""
- # Release active tool handle.
- if self._active_handle_idx >= 0:
- self._active_handle_idx = -1
- # Complete the polygon.
- elif (len(self._xs) > 3
- and self._xs[-1] == self._xs[0]
- and self._ys[-1] == self._ys[0]):
- self._polygon_completed = True
- # Place new vertex.
- elif (not self._polygon_completed
- and 'move_all' not in self.state
- and 'move_vertex' not in self.state):
- self._xs.insert(-1, event.xdata)
- self._ys.insert(-1, event.ydata)
- if self._polygon_completed:
- self.onselect(self.verts)
- def onmove(self, event):
- """Cursor move event handler and validator"""
- # Method overrides _SelectorWidget.onmove because the polygon selector
- # needs to process the move callback even if there is no button press.
- # _SelectorWidget.onmove include logic to ignore move event if
- # eventpress is None.
- if not self.ignore(event):
- event = self._clean_event(event)
- self._onmove(event)
- return True
- return False
- def _onmove(self, event):
- """Cursor move event handler"""
- # Move the active vertex (ToolHandle).
- if self._active_handle_idx >= 0:
- idx = self._active_handle_idx
- self._xs[idx], self._ys[idx] = event.xdata, event.ydata
- # Also update the end of the polygon line if the first vertex is
- # the active handle and the polygon is completed.
- if idx == 0 and self._polygon_completed:
- self._xs[-1], self._ys[-1] = event.xdata, event.ydata
- # Move all vertices.
- elif 'move_all' in self.state and self.eventpress:
- dx = event.xdata - self.eventpress.xdata
- dy = event.ydata - self.eventpress.ydata
- for k in range(len(self._xs)):
- self._xs[k] = self._xs_at_press[k] + dx
- self._ys[k] = self._ys_at_press[k] + dy
- # Do nothing if completed or waiting for a move.
- elif (self._polygon_completed
- or 'move_vertex' in self.state or 'move_all' in self.state):
- return
- # Position pending vertex.
- else:
- # Calculate distance to the start vertex.
- x0, y0 = self.line.get_transform().transform((self._xs[0],
- self._ys[0]))
- v0_dist = np.hypot(x0 - event.x, y0 - event.y)
- # Lock on to the start vertex if near it and ready to complete.
- if len(self._xs) > 3 and v0_dist < self.vertex_select_radius:
- self._xs[-1], self._ys[-1] = self._xs[0], self._ys[0]
- else:
- self._xs[-1], self._ys[-1] = event.xdata, event.ydata
- self._draw_polygon()
- def _on_key_press(self, event):
- """Key press event handler"""
- # Remove the pending vertex if entering the 'move_vertex' or
- # 'move_all' mode
- if (not self._polygon_completed
- and ('move_vertex' in self.state or 'move_all' in self.state)):
- self._xs, self._ys = self._xs[:-1], self._ys[:-1]
- self._draw_polygon()
- def _on_key_release(self, event):
- """Key release event handler"""
- # Add back the pending vertex if leaving the 'move_vertex' or
- # 'move_all' mode (by checking the released key)
- if (not self._polygon_completed
- and
- (event.key == self.state_modifier_keys.get('move_vertex')
- or event.key == self.state_modifier_keys.get('move_all'))):
- self._xs.append(event.xdata)
- self._ys.append(event.ydata)
- self._draw_polygon()
- # Reset the polygon if the released key is the 'clear' key.
- elif event.key == self.state_modifier_keys.get('clear'):
- event = self._clean_event(event)
- self._xs, self._ys = [event.xdata], [event.ydata]
- self._polygon_completed = False
- self.set_visible(True)
- def _draw_polygon(self):
- """Redraw the polygon based on the new vertex positions."""
- self.line.set_data(self._xs, self._ys)
- # Only show one tool handle at the start and end vertex of the polygon
- # if the polygon is completed or the user is locked on to the start
- # vertex.
- if (self._polygon_completed
- or (len(self._xs) > 3
- and self._xs[-1] == self._xs[0]
- and self._ys[-1] == self._ys[0])):
- self._polygon_handles.set_data(self._xs[:-1], self._ys[:-1])
- else:
- self._polygon_handles.set_data(self._xs, self._ys)
- self.update()
- @property
- def verts(self):
- """The polygon vertices, as a list of ``(x, y)`` pairs."""
- return list(zip(self._xs[:-1], self._ys[:-1]))
- class Lasso(AxesWidget):
- """
- Selection curve of an arbitrary shape.
- The selected path can be used in conjunction with
- `~matplotlib.path.Path.contains_point` to select data points from an image.
- Unlike `LassoSelector`, this must be initialized with a starting
- point *xy*, and the `Lasso` events are destroyed upon release.
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The parent axes for the widget.
- xy : (float, float)
- Coordinates of the start of the lasso.
- callback : callable
- Whenever the lasso is released, the *callback* function is called and
- passed the vertices of the selected path.
- """
- def __init__(self, ax, xy, callback=None, useblit=True):
- AxesWidget.__init__(self, ax)
- self.useblit = useblit and self.canvas.supports_blit
- if self.useblit:
- self.background = self.canvas.copy_from_bbox(self.ax.bbox)
- x, y = xy
- self.verts = [(x, y)]
- self.line = Line2D([x], [y], linestyle='-', color='black', lw=2)
- self.ax.add_line(self.line)
- self.callback = callback
- self.connect_event('button_release_event', self.onrelease)
- self.connect_event('motion_notify_event', self.onmove)
- def onrelease(self, event):
- if self.ignore(event):
- return
- if self.verts is not None:
- self.verts.append((event.xdata, event.ydata))
- if len(self.verts) > 2:
- self.callback(self.verts)
- self.ax.lines.remove(self.line)
- self.verts = None
- self.disconnect_events()
- def onmove(self, event):
- if self.ignore(event):
- return
- if self.verts is None:
- return
- if event.inaxes != self.ax:
- return
- if event.button != 1:
- return
- self.verts.append((event.xdata, event.ydata))
- self.line.set_data(list(zip(*self.verts)))
- if self.useblit:
- self.canvas.restore_region(self.background)
- self.ax.draw_artist(self.line)
- self.canvas.blit(self.ax.bbox)
- else:
- self.canvas.draw_idle()
|