Source code for cytoflow.utility.matplotlib_widgets

#!/usr/bin/env python3.4
# coding: latin-1

# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2021
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

'''
cytoflow.utility.matplotlib_widgets
-----------------------------------

Additional widgets to be used with matplotlib
'''

import time
from matplotlib.lines import Line2D
from matplotlib.widgets import AxesWidget, _SelectorWidget
from matplotlib.transforms import blended_transform_factory


[docs]class PolygonSelector(AxesWidget): """Selection polygon. The selected path can be used in conjunction with :func:`~matplotlib.path.Path.contains_point` to select data points from an image. Parameters ---------- ax : :class:`~matplotlib.axes.Axes` The parent axes for the widget. callback : callable When the user double-clicks, the polygon closes and the ``callback`` function is called and passed the vertices of the selected path. """ def __init__(self, ax, callback=None, useblit=True): AxesWidget.__init__(self, ax) self.useblit = useblit and self.canvas.supports_blit self.verts = [] self.drawing = False self.line = None self.callback = callback self.last_click_time = time.time() self.connect_event('button_press_event', self.onpress) self.connect_event('motion_notify_event', self.onmove)
[docs] def onpress(self, event): if self.ignore(event): return if not self.drawing: # start over self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.verts = [(event.xdata, event.ydata)] self.line = Line2D([event.xdata], [event.ydata], linestyle='-', color='black', lw=1) self.ax.add_line(self.line) self.drawing = True else: self.verts.append((event.xdata, event.ydata)) self.line.set_data(list(zip(*self.verts))) if event.dblclick or (time.time() - self.last_click_time < 0.3): self.callback(self.verts) self.ax.lines.remove(self.line) self.drawing = False self.last_click_time = time.time()
[docs] def onmove(self, event): if self.ignore(event): return if not self.drawing: return if event.inaxes != self.ax: return self.line.set_data(list(zip(*(self.verts + [(event.xdata, event.ydata)])))) 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()
[docs]class SpanSelector(_SelectorWidget): """ Adapted from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/widgets.py Visually select a min/max range on a single axis and call a function with those values. To guarantee that the selector remains responsive, keep a reference to it. In order to turn off the SpanSelector, set `span_selector.active=False`. To turn it back on, set `span_selector.active=True`. Parameters ---------- ax : :class:`matplotlib.axes.Axes` object onselect : func(min, max), min/max are floats direction : "horizontal" or "vertical" The axis along which to draw the span selector minspan : float, default is None If selection is less than *minspan*, do not call *onselect* useblit : bool, default is False If True, use the backend-dependent blitting features for faster canvas updates. onmove_callback : func(min, max), min/max are floats, default is None Called on mouse move while the span is being selected span_stays : bool, default is False If True, the span stays visible after the mouse is released button : int or list of ints Determines which mouse buttons activate the span selector 1 = left mouse button\n 2 = center mouse button (scroll wheel)\n 3 = right mouse button\n Examples -------- >>> import matplotlib.pyplot as plt >>> import matplotlib.widgets as mwidgets >>> fig, ax = plt.subplots() >>> ax.plot([1, 2, 3], [10, 50, 100]) >>> def onselect(vmin, vmax): print(vmin, vmax) >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal') >>> fig.show() See also: :ref:`sphx_glr_gallery_widgets_span_selector.py` """ def __init__(self, ax, onselect, minspan=None, useblit=False, onmove_callback=None, span_stays=False, button=None): _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, button=button) self.rect = None self.pressv = None self.onmove_callback = onmove_callback self.minspan = minspan self.span_stays = span_stays # Needed when dragging out of axes self.prev = 0 # Reset canvas so that `new_axes` connects events. self.canvas = None self.new_axes(ax)
[docs] def new_axes(self, ax): """Set SpanSelector to operate on a new Axes""" self.ax = ax if self.canvas is not ax.figure.canvas: if self.canvas is not None: self.disconnect_events() self.canvas = ax.figure.canvas self.connect_default_events() trans = blended_transform_factory(self.ax.transData, self.ax.transAxes) self.low_line = Line2D((0, 0), (0, 1), transform = trans, visible = False, color = 'blue', linewidth = 2) self.high_line = Line2D((0, 0), (0, 1), transform = trans, visible = False, color = 'blue', linewidth = 2) self.hline = Line2D((0, 0), (0.5, 0.5), transform = trans, visible = False, color = 'blue', linewidth = 2) if self.span_stays: self.stay_low_line = Line2D((0, 0), (0, 1), transform = trans, visible = False, color = 'blue', linewidth = 2) self.stay_high_line = Line2D((0, 0), (0, 1), transform = trans, visible = False, color = 'blue', linewidth = 2) self.stay_hline = Line2D((0, 0), (0.5, 0.5), transform = trans, visible = False, color = 'blue', linewidth = 2) self.stay_low_line.set_animated(False) self.stay_high_line.set_animated(False) self.stay_hline.set_animated(False) self.ax.add_line(self.stay_low_line) self.ax.add_line(self.stay_high_line) self.ax.add_line(self.stay_hline) self.ax.add_line(self.low_line) self.ax.add_line(self.high_line) self.ax.add_line(self.hline) self.artists = [self.low_line, self.high_line, self.hline]
[docs] def ignore(self, event): """return *True* if *event* should be ignored""" return _SelectorWidget.ignore(self, event) or not self.visible
def _press(self, event): """on button press event""" self.low_line.set_visible(self.visible) self.high_line.set_visible(self.visible) self.hline.set_visible(self.visible) if self.span_stays: self.stay_low_line.set_visible(False) self.stay_high_line.set_visibile(False) self.stay_hline.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, _ = self._get_data(event) self.pressv = xdata self._set_span_xy(event) return False def _release(self, event): """on button release event""" if self.pressv is None: return self.buttonDown = False self.low_line.set_visible(False) self.high_line.set_visible(False) self.hline.set_visible(False) if self.span_stays: self.stay_high_line.set_xdata(self.high_line.get_xdata()) self.stay_low_line.set_xdata(self.low_line.get_xdata()) self.stay_hline.set_xdata(self.hline.get_xdata()) self.stay_high_line.set_visible(True) self.stay_low_line.set_visible(True) self.stay_hline.set_visible(True) self.canvas.draw_idle() vmin = self.pressv xdata, _ = self._get_data(event) vmax = xdata or self.prev if vmin > vmax: vmin, vmax = vmax, vmin span = vmax - vmin if self.minspan is not None and span < self.minspan: return self.onselect(vmin, vmax) self.pressv = None return False def _onmove(self, event): """on motion notify event""" if self.pressv is None: return self._set_span_xy(event) if self.onmove_callback is not None: vmin = self.pressv xdata, _ = self._get_data(event) vmax = xdata or self.prev if vmin > vmax: vmin, vmax = vmax, vmin self.onmove_callback(vmin, vmax) self.update() return False def _set_span_xy(self, event): """Setting the span coordinates""" x, _ = self._get_data(event) if x is None: return self.prev = x v = x minv, maxv = v, self.pressv if minv > maxv: minv, maxv = maxv, minv self.low_line.set_xdata([minv, minv]) self.high_line.set_xdata([maxv, maxv]) self.hline.set_xdata([minv, maxv])
[docs]class Cursor(AxesWidget): """ A horizontal and vertical line that spans the axes and moves with the pointer. You can turn off the hline or vline respectively with the following attributes: *horizOn* Controls the visibility of the horizontal line *vertOn* Controls the visibility of the horizontal line and the visibility of the cursor itself with the *visible* attribute. For the cursor to remain responsive you must keep a reference to it. Adapted from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/widgets.py """ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, **lineprops): """ Add a cursor to *ax*. If ``useblit=True``, use the backend- dependent blitting features for faster updates (GTKAgg only for now). *lineprops* is a dictionary of line properties. .. plot :: mpl_examples/widgets/cursor.py """ # TODO: Is the GTKAgg limitation still true? AxesWidget.__init__(self, ax) self.connect_event('motion_notify_event', self.onmove) self.connect_event('draw_event', self.clear) self.visible = True self.horizOn = horizOn self.vertOn = vertOn self.useblit = useblit and self.canvas.supports_blit self.moved = False if self.useblit: lineprops['animated'] = True self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops) self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops) self.background = None self.needclear = False
[docs] def clear(self, event): """clear the cursor""" if self.ignore(event): return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) if not self.moved: return self.linev.set_visible(False) self.lineh.set_visible(False) self.moved = False
[docs] def onmove(self, event): """on mouse motion draw the cursor if visible""" if self.ignore(event): return if not self.canvas.widgetlock.available(self): return if event.inaxes != self.ax: self.linev.set_visible(False) self.lineh.set_visible(False) if self.needclear: self.canvas.draw() self.needclear = False return self.needclear = True if not self.visible: return self.linev.set_xdata((event.xdata, event.xdata)) self.lineh.set_ydata((event.ydata, event.ydata)) self.linev.set_visible(self.visible and self.vertOn) self.lineh.set_visible(self.visible and self.horizOn) self._update()
def _update(self): if self.useblit: if self.background is not None: self.canvas.restore_region(self.background) self.ax.draw_artist(self.linev) self.ax.draw_artist(self.lineh) self.canvas.blit(self.ax.bbox) else: self.canvas.draw_idle() self.moved = True return False