"""
GUI neutral widgets
===================
Widgets that are designed to work for any of the GUI backends.
All of these widgets require you to predefine a :class:`matplotlib.axes.Axes`
instance and pass that as the first arg. 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 __future__ import (absolute_import, division, print_function,
unicode_literals)
import copy
import six
from six.moves import zip
import numpy as np
from matplotlib import rcParams
from .patches import Circle, Rectangle, Ellipse
from .lines import Line2D
from .transforms import blended_transform_factory
[docs]class LockDraw(object):
"""
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. The module level "lock"
allows someone to grab the lock and prevent other widgets from
drawing. Use ``matplotlib.widgets.lock(someobj)`` to prevent
other widgets from drawing while you're interacting with the canvas.
"""
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
[docs] def release(self, o):
"""release the lock"""
if not self.available(o):
raise ValueError('you do not own this lock')
self._owner = None
[docs] def available(self, o):
"""drawing is available to *o*"""
return not self.locked() or self.isowner(o)
[docs] def isowner(self, o):
"""Return True if *o* owns this lock"""
return self._owner is o
[docs] def locked(self):
"""Return True if the lock is currently held by an owner"""
return self._owner is not None
[docs]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='%1.2f',
closedmin=True, closedmax=True, slidermin=None,
slidermax=None, dragging=True, valstep=None, **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, optional, default: 0.5
The slider initial position.
valfmt : str, optional, default: "%1.2f"
Used to format the slider value, fprint format string.
closedmin : bool, optional, default: True
Indicate whether the slider interval is closed on the bottom.
closedmax : bool, optional, default: True
Indicate whether the slider interval is closed on the top.
slidermin : Slider, optional, default: None
Do not allow the current slider to have a value less than
the value of the Slider `slidermin`.
slidermax : Slider, optional, default: None
Do not allow the current slider to have a value greater than
the value of the Slider `slidermax`.
dragging : bool, optional, default: True
If True the slider can be dragged by the mouse.
valstep : float, optional, default: None
If given, the slider will snap to multiples of `valstep`.
Notes
-----
Additional kwargs are passed on to ``self.poly`` which is the
:class:`~matplotlib.patches.Rectangle` that draws the slider
knob. See the :class:`~matplotlib.patches.Rectangle` documentation for
valid property names (e.g., `facecolor`, `edgecolor`, `alpha`).
"""
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)))
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
self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs)
self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1)
self.valfmt = valfmt
ax.set_yticks([])
ax.set_xlim((valmin, valmax))
ax.set_xticks([])
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)
self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
verticalalignment='center',
horizontalalignment='right')
self.valtext = ax.text(1.02, 0.5, valfmt % 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 self.val is with given bounds."""
if self.valstep:
val = np.round((val - self.valmin)/self.valstep)*self.valstep
val += self.valmin
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):
return
if 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
val = self._value_in_bounds(event.xdata)
if (val is not None) and (val != self.val):
self.set_val(val)
[docs] def set_val(self, val):
"""
Set slider value to *val*
Parameters
----------
val : float
"""
xy = self.poly.xy
xy[2] = val, 1
xy[3] = val, 0
self.poly.xy = xy
self.valtext.set_text(self.valfmt % val)
if self.drawon:
self.ax.figure.canvas.draw_idle()
self.val = val
if not self.eventson:
return
for cid, func in six.iteritems(self.observers):
func(val)
[docs] 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
-------
cid : int
Connection id (which can be used to disconnect *func*)
"""
cid = self.cnt
self.observers[cid] = func
self.cnt += 1
return cid
[docs] 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
[docs] def reset(self):
"""Reset the slider to the initial value"""
if (self.val != self.valinit):
self.set_val(self.valinit)
[docs]class TextBox(AxesWidget):
"""
A GUI neutral text input box.
For the text box to remain responsive you must keep a reference to it.
The following attributes are accessible:
*ax*
The :class:`matplotlib.axes.Axes` the button renders into.
*label*
A :class:`matplotlib.text.Text` instance.
*color*
The color of the text box when not hovering.
*hovercolor*
The color of the text box when hovering.
Call :meth:`on_text_change` to be updated whenever the text changes.
Call :meth:`on_submit` to be updated whenever the user hits enter or
leaves the text entry field.
"""
def __init__(self, ax, label, initial='',
color='.95', hovercolor='1', label_pad=.01):
"""
Parameters
----------
ax : matplotlib.axes.Axes
The :class:`matplotlib.axes.Axes` instance the button
will be placed into.
label : str
Label for this text box. Accepts string.
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.params_to_disable = [key for key in rcParams if u'keymap' in key]
self.text = initial
self.label = ax.text(-label_pad, 0.5, label,
verticalalignment='center',
horizontalalignment='right',
transform=ax.transAxes)
self.text_disp = self._make_text_disp(self.text)
self.cnt = 0
self.change_observers = {}
self.submit_observers = {}
# If these lines are removed, the cursor won't appear the first
# time the box is clicked:
self.ax.set_xlim(0, 1)
self.ax.set_ylim(0, 1)
self.cursor_index = 0
# Because this is initialized, _render_cursor
# can assume that cursor exists.
self.cursor = self.ax.vlines(0, 0, 0)
self.cursor.set_visible(False)
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)
ax.set_navigate(False)
ax.set_facecolor(color)
ax.set_xticks([])
ax.set_yticks([])
self.color = color
self.hovercolor = hovercolor
self._lastcolor = color
self.capturekeystrokes = False
def _make_text_disp(self, string):
return self.ax.text(self.DIST_FROM_LEFT, 0.5, string,
verticalalignment='center',
horizontalalignment='left',
transform=self.ax.transAxes)
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
widthtext = self.text[:self.cursor_index]
no_text = False
if(widthtext == "" or widthtext == " " or widthtext == " "):
no_text = widthtext == ""
widthtext = ","
wt_disp = self._make_text_disp(widthtext)
self.ax.figure.canvas.draw()
bb = wt_disp.get_window_extent()
inv = self.ax.transData.inverted()
bb = inv.transform(bb)
wt_disp.set_visible(False)
if no_text:
bb[1, 0] = bb[0, 0]
# hack done
self.cursor.set_visible(False)
self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1])
self.ax.figure.canvas.draw()
def _notify_submit_observers(self):
for cid, func in six.iteritems(self.submit_observers):
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
if(len(key) == 1):
self.text = (self.text[:self.cursor_index] + key +
self.text[self.cursor_index:])
self.cursor_index += 1
elif key == "right":
if self.cursor_index != len(self.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(self.text)
elif(key == "backspace"):
if self.cursor_index != 0:
self.text = (self.text[:self.cursor_index - 1] +
self.text[self.cursor_index:])
self.cursor_index -= 1
elif(key == "delete"):
if self.cursor_index != len(self.text):
self.text = (self.text[:self.cursor_index] +
self.text[self.cursor_index + 1:])
self.text_disp.remove()
self.text_disp = self._make_text_disp(self.text)
self._rendercursor()
self._notify_change_observers()
if key == "enter":
self._notify_submit_observers()
[docs] def set_val(self, val):
newval = str(val)
if self.text == newval:
return
self.text = newval
self.text_disp.remove()
self.text_disp = self._make_text_disp(self.text)
self._rendercursor()
self._notify_change_observers()
self._notify_submit_observers()
def _notify_change_observers(self):
for cid, func in six.iteritems(self.change_observers):
func(self.text)
[docs] def begin_typing(self, x):
self.capturekeystrokes = True
# disable command keys so that the user can type without
# command keys causing figure to be saved, etc
self.reset_params = {}
for key in self.params_to_disable:
self.reset_params[key] = rcParams[key]
rcParams[key] = []
[docs] def stop_typing(self):
notifysubmit = False
# because _notify_submit_users might throw an error in the
# user's code, we only want to call it once we've already done
# our cleanup.
if self.capturekeystrokes:
# since the user is no longer typing,
# reactivate the standard command keys
for key in self.params_to_disable:
rcParams[key] = self.reset_params[key]
notifysubmit = True
self.capturekeystrokes = False
self.cursor.set_visible(False)
self.ax.figure.canvas.draw()
if notifysubmit:
self._notify_submit_observers()
[docs] 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()
trans = self.ax.transData
inv = self.ax.transData.inverted()
bb = trans.transform(inv.transform(bb))
text_start = bb[0, 0]
text_end = bb[1, 0]
ratio = (x - text_start) / (text_end - text_start)
if ratio < 0:
ratio = 0
if ratio > 1:
ratio = 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
if event.inaxes == self.ax:
c = self.hovercolor
else:
c = self.color
if c != self._lastcolor:
self.ax.set_facecolor(c)
self._lastcolor = c
if self.drawon:
self.ax.figure.canvas.draw()
[docs] 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
[docs] def on_submit(self, func):
"""
When the user hits enter or leaves the submision 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
[docs] 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
[docs]class Cursor(AxesWidget):
"""
A horizontal and vertical line that spans the axes and moves with
the pointer. You can turn off the hline or vline respectively with
the following attributes:
*horizOn*
Controls the visibility of the horizontal line
*vertOn*
Controls the visibility of the horizontal line
and the visibility of the cursor itself with the *visible* attribute.
For the cursor to remain responsive you must keep a reference to
it.
"""
def __init__(self, ax, horizOn=True, vertOn=True, useblit=False,
**lineprops):
"""
Add a cursor to *ax*. If ``useblit=True``, use the backend-dependent
blitting features for faster updates. *lineprops* is a dictionary of
line properties.
"""
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
[docs] def clear(self, event):
"""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)
[docs] def onmove(self, event):
"""on mouse motion draw the cursor if visible"""
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
[docs]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
from pylab import figure, show, np
t = np.arange(0.0, 2.0, 0.01)
s1 = np.sin(2*np.pi*t)
s2 = np.sin(4*np.pi*t)
fig = figure()
ax1 = fig.add_subplot(211)
ax1.plot(t, s1)
ax2 = fig.add_subplot(212, sharex=ax1)
ax2.plot(t, s2)
multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1,
horizOn=False, vertOn=True)
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()
[docs] 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)
[docs] def disconnect(self):
"""disconnect events"""
self.canvas.mpl_disconnect(self._cidmotion)
self.canvas.mpl_disconnect(self._ciddraw)
[docs] 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)
[docs] 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(self.canvas.figure.bbox)
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, int):
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):
"""return *True* if *event* should be ignored"""
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:
if 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 newfangled blit or oldfangled draw depending on
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
x0, x1 = self.ax.get_xbound()
y0, y1 = self.ax.get_ybound()
xdata = max(x0, event.xdata)
xdata = min(x1, xdata)
ydata = max(y0, event.ydata)
ydata = min(y1, ydata)
return xdata, ydata
def _clean_event(self, event):
"""Clean up an event
Use prev event if there is no xdata
Limit the xdata and ydata to the axes limits
Set the prev 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"""
pass
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"""
pass
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"""
pass
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"""
pass
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 - use for widget-specific key press actions.
"""
pass
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"""
pass
def set_visible(self, visible):
""" Set the visibility of our artists """
self.visible = visible
for artist in self.artists:
artist.set_visible(visible)
[docs]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=False`. To
turn it back on, set `span_selector.active=True`.
Parameters
----------
ax : :class:`matplotlib.axes.Axes` object
onselect : func(min, max), min/max are floats
direction : "horizontal" or "vertical"
The axis along which to draw the span selector
minspan : float, default is None
If selection is less than *minspan*, do not call *onselect*
useblit : bool, default is False
If True, use the backend-dependent blitting features for faster
canvas updates.
rectprops : dict, default is None
Dictionary of :class:`matplotlib.patches.Patch` properties
onmove_callback : func(min, max), min/max are floats, default is None
Called on mouse move while the span is being selected
span_stays : bool, default is False
If True, the span stays visible after the mouse is released
button : int or list of ints
Determines which mouse buttons activate the span selector
1 = left mouse button\n
2 = center mouse button (scroll wheel)\n
3 = right mouse button\n
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: :ref:`sphx_glr_gallery_widgets_span_selector.py`
"""
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
if direction not in ['horizontal', 'vertical']:
raise ValueError("direction must be 'horizontal' or 'vertical'")
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)
[docs] 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]
[docs] def ignore(self, event):
"""return *True* if *event* should be ignored"""
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
return False
def _release(self, event):
"""on button release event"""
if self.pressv is None:
return
self.buttonDown = False
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
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)
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
[docs]class RectangleSelector(_SelectorWidget):
"""
Select a rectangular region of an axes.
For the cursor to remain responsive you must keep a reference to
it.
Example usage::
from matplotlib.widgets import RectangleSelector
from pylab import *
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.RS.active:
print(' RectangleSelector deactivated.')
toggle_selector.RS.set_active(False)
if event.key in ['A', 'a'] and not toggle_selector.RS.active:
print(' RectangleSelector activated.')
toggle_selector.RS.set_active(True)
x = arange(100)/(99.0)
y = sin(x)
fig = figure
ax = subplot(111)
ax.plot(x,y)
toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='line')
connect('key_press_event', toggle_selector)
show()
"""
_shape_klass = Rectangle
def __init__(self, ax, onselect, drawtype='box',
minspanx=None, minspany=None, useblit=False,
lineprops=None, rectprops=None, spancoords='data',
button=None, maxdist=10, marker_props=None,
interactive=False, state_modifier_keys=None):
"""
Create a selector in *ax*. When a selection is made, clear
the span and call onselect with::
onselect(pos_1, pos_2)
and clear the drawn box/line. The ``pos_1`` and ``pos_2`` are
arrays of length 2 containing the x- and y-coordinate.
If *minspanx* is not *None* then events smaller than *minspanx*
in x direction are ignored (it's the same for y).
The rectangle is drawn with *rectprops*; default::
rectprops = dict(facecolor='red', edgecolor = 'black',
alpha=0.2, fill=True)
The line is drawn with *lineprops*; default::
lineprops = dict(color='black', linestyle='-',
linewidth = 2, alpha=0.5)
Use *drawtype* if you want the mouse to draw a line,
a box or nothing between click and actual position by setting
``drawtype = 'line'``, ``drawtype='box'`` or ``drawtype = 'none'``.
Drawing a line would result in a line from vertex A to vertex C in
a rectangle ABCD.
*spancoords* is one of 'data' or 'pixels'. If 'data', *minspanx*
and *minspanx* will be interpreted in the same coordinates as
the x and y axis. If 'pixels', they are in pixels.
*button* is a list of integers indicating which mouse buttons should
be used for rectangle selection. You can also specify a single
integer if only a single button is desired. Default is *None*,
which does not limit which button can be used.
Note, typically:
1 = left mouse button
2 = center mouse button (scroll wheel)
3 = right mouse button
*interactive* will draw a set of handles and allow you interact
with the widget after it is drawn.
*state_modifier_keys* are keyboard modifiers that affect the behavior
of the widget.
The defaults are:
dict(move=' ', clear='escape', square='shift', center='ctrl')
Keyboard modifiers, which:
'move': Move the existing shape.
'clear': Clear the current shape.
'square': Makes the shape square.
'center': Make the initial point the center of the shape.
'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':
drawtype = 'line' # draw a line but make it
self.visible = False # invisible
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
if spancoords not in ('data', 'pixels'):
raise ValueError("'spancoords' must be 'data' or 'pixels'")
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 drawed 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()
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_point([x1, y1])
self.eventpress.x, self.eventpress.y = xy1
self.eventrelease.xdata = x2
self.eventrelease.ydata = y2
xy2 = self.ax.transData.transform_point([x2, y2])
self.eventrelease.x, self.eventrelease.y = xy2
if self.spancoords == 'data':
xmin, ymin = self.eventpress.xdata, self.eventpress.ydata
xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata
# calculate dimensions of box or line get values in the right
# order
elif self.spancoords == 'pixels':
xmin, ymin = self.eventpress.x, self.eventpress.y
xmax, ymax = self.eventrelease.x, self.eventrelease.y
else:
raise ValueError('spancoords must be "data" or "pixels"')
if xmin > xmax:
xmin, xmax = xmax, xmin
if ymin > ymax:
ymin, ymax = ymax, ymin
spanx = xmax - xmin
spany = ymax - ymin
xproblems = self.minspanx is not None and spanx < self.minspanx
yproblems = self.minspany is not None and spany < self.minspany
# check if drawn distance (if it exists) is not too small in
# either x or y-direction
if self.drawtype != 'none' and (xproblems or yproblems):
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 not 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()
[docs] 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):
"""
Returns numpy.ndarray of shape (2,5) containing
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())
[docs]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::
from matplotlib.widgets import EllipseSelector
from pylab import *
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 = arange(100)/(99.0)
y = sin(x)
fig = figure
ax = subplot(111)
ax.plot(x,y)
toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line')
connect('key_press_event', toggle_selector)
show()
"""
_shape_klass = Ellipse
[docs] 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
[docs]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 : :class:`~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 : List[Int], optional
A list of integers indicating which mouse buttons should be used for
rectangle selection. You can also specify a single integer if only a
single button is desired. Default is ``None``, which does not limit
which button can be used.
Note, typically:
- 1 = left mouse button
- 2 = center mouse button (scroll wheel)
- 3 = right mouse button
"""
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()
if useblit:
lineprops['animated'] = True
self.line = Line2D([], [], **lineprops)
self.line.set_visible(False)
self.ax.add_line(self.line)
self.artists = [self.line]
[docs] def onpress(self, event):
self.press(event)
def _press(self, event):
self.verts = [self._get_data(event)]
self.line.set_visible(True)
[docs] 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()
[docs]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 : :class:`~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, optional
lineprops : dict, optional
The line for the sides of the polygon is drawn with the properties
given by `lineprops`. The default is ``dict(color='k', linestyle='-',
linewidth=2, alpha=0.5)``.
markerprops : dict, optional
The markers for the vertices of the polygon are drawn with the
properties given by `markerprops`. The default is ``dict(marker='o',
markersize=7, mec='k', mfc='k', alpha=0.5)``.
vertex_select_radius : float, optional
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. The default radius is 15 pixels.
See Also
--------
:ref:`sphx_glr_gallery_widgets_polygon_selector_demo.py`
"""
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[:], self._ys[:]
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)
[docs] 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.sqrt((x0 - event.x) ** 2 + (y0 - event.y) ** 2)
# 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):
"""Get the polygon vertices.
Returns
-------
list
A list of the vertices of the polygon as ``(xdata, ydata)`` tuples.
"""
return list(zip(self._xs[:-1], self._ys[:-1]))
[docs]class Lasso(AxesWidget):
"""Selection curve of an arbitrary shape.
The selected path can be used in conjunction with
:func:`~matplotlib.path.Path.contains_point` to select data points
from an image.
Unlike :class:`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 : array
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)
[docs] 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()
[docs] 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()