2838 lines
93 KiB
Python
2838 lines
93 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
import copy
|
|
from numbers import Integral
|
|
|
|
import numpy as np
|
|
|
|
from . import cbook, rcParams
|
|
from .lines import Line2D
|
|
from .patches import Circle, Rectangle, Ellipse
|
|
from .transforms import blended_transform_factory
|
|
|
|
|
|
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. 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(object):
|
|
"""
|
|
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, lambda self, active: self.set_active(active),
|
|
doc="Is the widget active?")
|
|
|
|
def ignore(self, event):
|
|
"""Return True if event should be ignored.
|
|
|
|
This method (or a version of it) should be called at the beginning
|
|
of any event callback.
|
|
"""
|
|
return not self.active
|
|
|
|
|
|
class AxesWidget(Widget):
|
|
"""Widget that is connected to a single
|
|
:class:`~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* : :class:`~matplotlib.axes.Axes`
|
|
The parent axes for the widget
|
|
*canvas* : :class:`~matplotlib.backend_bases.FigureCanvasBase` subclass
|
|
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 :meth:`on_clicked` to connect to the button.
|
|
|
|
Attributes
|
|
----------
|
|
ax
|
|
The :class:`matplotlib.axes.Axes` the button renders into.
|
|
label
|
|
A :class:`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. Accepts string.
|
|
|
|
image : array, mpl image, Pillow Image
|
|
The image to place in the button, if not *None*.
|
|
Can be any legal arg to imshow (numpy array,
|
|
matplotlib Image instance, or Pillow Image).
|
|
|
|
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
|
|
|
|
self._lastcolor = color
|
|
|
|
def _click(self, event):
|
|
if self.ignore(event):
|
|
return
|
|
if event.inaxes != self.ax:
|
|
return
|
|
if 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):
|
|
return
|
|
if event.canvas.mouse_grabber != self.ax:
|
|
return
|
|
event.canvas.release_mouse(self.ax)
|
|
if not self.eventson:
|
|
return
|
|
if event.inaxes != self.ax:
|
|
return
|
|
for cid, func in self.observers.items():
|
|
func(event)
|
|
|
|
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()
|
|
|
|
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='%1.2f',
|
|
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, 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`.
|
|
|
|
orientation : str, 'horizontal' or 'vertical', default: 'horizontal'
|
|
The orientation of the slider.
|
|
|
|
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)))
|
|
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)
|
|
|
|
self.valfmt = valfmt
|
|
ax.set_yticks([])
|
|
if orientation == 'vertical':
|
|
ax.set_ylim((valmin, valmax))
|
|
else:
|
|
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)
|
|
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, valfmt % 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, 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 *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) 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 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.valfmt % 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
|
|
-------
|
|
cid : 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):
|
|
"""
|
|
A GUI neutral set of check buttons.
|
|
|
|
For the check buttons to remain responsive you must keep a
|
|
reference to this object.
|
|
|
|
The following attributes are exposed
|
|
|
|
*ax*
|
|
The :class:`matplotlib.axes.Axes` instance the buttons are
|
|
located in
|
|
|
|
*labels*
|
|
List of :class:`matplotlib.text.Text` instances
|
|
|
|
*lines*
|
|
List of (line1, line2) tuples 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.
|
|
|
|
*rectangles*
|
|
List of :class:`matplotlib.patches.Rectangle` instances
|
|
|
|
Connect to the CheckButtons with the :meth:`on_clicked` method
|
|
"""
|
|
def __init__(self, ax, labels, actives=None):
|
|
"""
|
|
Add check buttons to :class:`matplotlib.axes.Axes` instance *ax*
|
|
|
|
Parameters
|
|
----------
|
|
ax : `~matplotlib.axes.Axes`
|
|
The parent axes for the widget.
|
|
|
|
labels : List[str]
|
|
The labels of the check buttons.
|
|
|
|
actives : List[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):
|
|
"""
|
|
Directly (de)activate a check button by index.
|
|
|
|
*index* is an index into the original label list
|
|
that this object was constructed with.
|
|
Raises ValueError if *index* is invalid.
|
|
|
|
Callbacks will be triggered if :attr:`eventson` is True.
|
|
|
|
"""
|
|
if 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.
|
|
|
|
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 `~.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 '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):
|
|
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
|
|
|
|
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()
|
|
|
|
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):
|
|
if self.eventson:
|
|
for cid, func in self.change_observers.items():
|
|
func(self.text)
|
|
|
|
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] = []
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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 :meth:`on_clicked` method.
|
|
|
|
|
|
Attributes
|
|
----------
|
|
ax
|
|
The containing `~.axes.Axes` instance.
|
|
activecolor
|
|
The color of the selected button.
|
|
labels
|
|
A list of `~.text.Text` instances containing the button labels.
|
|
circles
|
|
A list of `~.patches.Circle` instances defining 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
|
|
|
|
# defaul to hard-coded value if the radius becomes too large
|
|
if(circle_radius > 0.05):
|
|
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
|
|
xy = self.ax.transAxes.inverted().transform_point((event.x, event.y))
|
|
pclicked = np.array([xy[0], xy[1]])
|
|
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 :class:`matplotlib.figure.Figure`.
|
|
"""
|
|
def __init__(self, targetfig, toolfig):
|
|
"""
|
|
*targetfig*
|
|
The figure instance to adjust.
|
|
|
|
*toolfig*
|
|
The figure instance to embed the subplot tool into. If
|
|
*None*, a default figure will be created. If you are using
|
|
this from the GUI
|
|
"""
|
|
# FIXME: The docstring seems to just abruptly end without...
|
|
|
|
self.targetfig = targetfig
|
|
toolfig.subplots_adjust(left=0.2, right=0.9)
|
|
|
|
self.axleft = toolfig.add_subplot(711)
|
|
self.axleft.set_title('Click on slider to adjust subplot param')
|
|
self.axleft.set_navigate(False)
|
|
|
|
self.sliderleft = Slider(self.axleft, 'left',
|
|
0, 1, targetfig.subplotpars.left,
|
|
closedmax=False)
|
|
self.sliderleft.on_changed(self.funcleft)
|
|
|
|
self.axbottom = toolfig.add_subplot(712)
|
|
self.axbottom.set_navigate(False)
|
|
self.sliderbottom = Slider(self.axbottom,
|
|
'bottom', 0, 1,
|
|
targetfig.subplotpars.bottom,
|
|
closedmax=False)
|
|
self.sliderbottom.on_changed(self.funcbottom)
|
|
|
|
self.axright = toolfig.add_subplot(713)
|
|
self.axright.set_navigate(False)
|
|
self.sliderright = Slider(self.axright, 'right', 0, 1,
|
|
targetfig.subplotpars.right,
|
|
closedmin=False)
|
|
self.sliderright.on_changed(self.funcright)
|
|
|
|
self.axtop = toolfig.add_subplot(714)
|
|
self.axtop.set_navigate(False)
|
|
self.slidertop = Slider(self.axtop, 'top', 0, 1,
|
|
targetfig.subplotpars.top,
|
|
closedmin=False)
|
|
self.slidertop.on_changed(self.functop)
|
|
|
|
self.axwspace = toolfig.add_subplot(715)
|
|
self.axwspace.set_navigate(False)
|
|
self.sliderwspace = Slider(self.axwspace, 'wspace',
|
|
0, 1, targetfig.subplotpars.wspace,
|
|
closedmax=False)
|
|
self.sliderwspace.on_changed(self.funcwspace)
|
|
|
|
self.axhspace = toolfig.add_subplot(716)
|
|
self.axhspace.set_navigate(False)
|
|
self.sliderhspace = Slider(self.axhspace, 'hspace',
|
|
0, 1, targetfig.subplotpars.hspace,
|
|
closedmax=False)
|
|
self.sliderhspace.on_changed(self.funchspace)
|
|
|
|
# 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')
|
|
|
|
sliders = (self.sliderleft, self.sliderbottom, self.sliderright,
|
|
self.slidertop, self.sliderwspace, self.sliderhspace,)
|
|
|
|
def func(event):
|
|
thisdrawon = self.drawon
|
|
|
|
self.drawon = False
|
|
|
|
# store the drawon state of each slider
|
|
bs = []
|
|
for slider in sliders:
|
|
bs.append(slider.drawon)
|
|
slider.drawon = False
|
|
|
|
# reset the slider to the initial position
|
|
for slider in sliders:
|
|
slider.reset()
|
|
|
|
# reset drawon
|
|
for slider, b in zip(sliders, bs):
|
|
slider.drawon = b
|
|
|
|
# draw the canvas
|
|
self.drawon = thisdrawon
|
|
if self.drawon:
|
|
toolfig.canvas.draw()
|
|
self.targetfig.canvas.draw()
|
|
|
|
# during reset there can be a temporary invalid state
|
|
# depending on the order of the reset so we turn off
|
|
# validation for the resetting
|
|
validate = toolfig.subplotpars.validate
|
|
toolfig.subplotpars.validate = False
|
|
self.buttonreset.on_clicked(func)
|
|
toolfig.subplotpars.validate = validate
|
|
|
|
def funcleft(self, val):
|
|
self.targetfig.subplots_adjust(left=val)
|
|
if self.drawon:
|
|
self.targetfig.canvas.draw()
|
|
|
|
def funcright(self, val):
|
|
self.targetfig.subplots_adjust(right=val)
|
|
if self.drawon:
|
|
self.targetfig.canvas.draw()
|
|
|
|
def funcbottom(self, val):
|
|
self.targetfig.subplots_adjust(bottom=val)
|
|
if self.drawon:
|
|
self.targetfig.canvas.draw()
|
|
|
|
def functop(self, val):
|
|
self.targetfig.subplots_adjust(top=val)
|
|
if self.drawon:
|
|
self.targetfig.canvas.draw()
|
|
|
|
def funcwspace(self, val):
|
|
self.targetfig.subplots_adjust(wspace=val)
|
|
if self.drawon:
|
|
self.targetfig.canvas.draw()
|
|
|
|
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, optional, default: True
|
|
Whether to draw the horizontal line.
|
|
vertOn : bool, optional, default: True
|
|
Whether to draw the vertical line.
|
|
useblit : bool, optional, 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(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, 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):
|
|
"""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."""
|
|
|
|
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=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: :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):
|
|
"""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
|
|
|
|
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
|
|
|
|
@cbook.deprecated("3.1")
|
|
@property
|
|
def buttonDown(self):
|
|
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):
|
|
"""Setting 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(object):
|
|
"""Control handles for canvas tools.
|
|
|
|
Parameters
|
|
----------
|
|
ax : :class:`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 :class:`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.transpose((self.x, self.y))
|
|
# Transform data coordinates to pixel coordinates.
|
|
pts = self.ax.transData.transform(pts)
|
|
diff = pts - ((x, y))
|
|
if diff.ndim == 2:
|
|
dist = np.sqrt(np.sum(diff ** 2, axis=1))
|
|
return np.argmin(dist), np.min(dist)
|
|
else:
|
|
return 0, np.sqrt(np.sum(diff ** 2))
|
|
|
|
|
|
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::
|
|
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.widgets import RectangleSelector
|
|
|
|
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 = np.arange(100.) / 99
|
|
y = np.sin(x)
|
|
fig, ax = plt.subplots()
|
|
ax.plot(x, y)
|
|
|
|
toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='line')
|
|
fig.canvas.mpl_connect('key_press_event', toggle_selector)
|
|
plt.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
|
|
|
|
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 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()
|
|
|
|
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_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:
|
|
cbook._check_in_list(['data', 'pixels'],
|
|
spancoords=self.spancoords)
|
|
|
|
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()
|
|
|
|
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())
|
|
|
|
|
|
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 : :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]
|
|
|
|
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 : :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.
|
|
|
|
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[:], 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)
|
|
|
|
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):
|
|
"""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]))
|
|
|
|
|
|
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 : (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()
|