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