コード例 #1
0
ファイル: _mplcursors.py プロジェクト: bbgky/mplcursors
class Cursor:
    """A cursor for selecting artists on a matplotlib figure.
    """

    _keep_alive = WeakKeyDictionary()

    def __init__(self,
                 artists,
                 *,
                 multiple=False,
                 highlight=False,
                 hover=False,
                 bindings=default_bindings):
        """Construct a cursor.

        Parameters
        ----------

        artists : List[Artist]
            A list of artists that can be selected by this cursor.

        multiple : bool
            Whether multiple artists can be "on" at the same time (defaults to
            False).

        highlight : bool
            Whether to also highlight the selected artist.  If so,
            "highlighter" artists will be placed as the first item in the
            :attr:`extras` attribute of the `Selection`.

        bindings : dict
            A mapping of button and keybindings to actions.  Valid entries are:

            =================== ===============================================
            'select'            mouse button to select an artist (default: 1)
            'deselect'          mouse button to deselect an artist (default: 3)
            'left'              move to the previous point in the selected
                                path, or to the left in the selected image
                                (default: shift+left)
            'right'             move to the next point in the selected path, or
                                to the right in the selected image
                                (default: shift+right)
            'up'                move up in the selected image
                                (default: shift+up)
            'down'              move down in the selected image
                                (default: shift+down)
            'toggle_visibility' toggle visibility of all cursors (default: d)
            'toggle_enabled'    toggle whether the cursor is active
                                (default: t)
            =================== ===============================================

        hover : bool
            Whether to select artists upon hovering instead of by clicking.
        """

        artists = list(artists)
        # Be careful with GC.
        self._artists = [weakref.ref(artist) for artist in artists]

        for artist in artists:
            type(self)._keep_alive.setdefault(artist, []).append(self)

        self._multiple = multiple
        self._highlight = highlight

        self._axes = {artist.axes for artist in artists}
        self._enabled = True
        self._selections = []
        self._callbacks = CallbackRegistry()

        connect_pairs = [("key_press_event", self._on_key_press)]
        if hover:
            if multiple:
                raise ValueError("`hover` and `multiple` are incompatible")
            connect_pairs += [("motion_notify_event",
                               self._on_select_button_press)]
        else:
            connect_pairs += [("button_press_event", self._on_button_press)]
        self._disconnect_cids = [
            partial(canvas.mpl_disconnect, canvas.mpl_connect(*pair))
            for pair in connect_pairs
            for canvas in {artist.figure.canvas
                           for artist in artists}
        ]

        bindings = {**default_bindings, **bindings}
        if set(bindings) != set(default_bindings):
            raise ValueError("Unknown bindings")
        actually_bound = {k: v for k, v in bindings.items() if v is not None}
        if len(set(actually_bound.values())) != len(actually_bound):
            raise ValueError("Duplicate bindings")
        self._bindings = bindings

    @property
    def enabled(self):
        """Whether clicks are registered for picking and unpicking events.
        """
        return self._enabled

    @enabled.setter
    def enabled(self, value):
        self._enabled = value

    @property
    def artists(self):
        """The tuple of selectable artists.
        """
        return tuple(filter(None, (ref() for ref in self._artists)))

    @property
    def selections(self):
        """The tuple of current `Selection`\\s.
        """
        return tuple(self._selections)

    def add_selection(self, pi):
        """Create an annotation for a `Selection` and register it.

        Returns a new `Selection`, that has been registered by the `Cursor`,
        with the added annotation set in the :attr:`annotation` field and, if
        applicable, the highlighting artist in the :attr:`extras` field.

        Emits the ``"add"`` event with the new `Selection` as argument.
        """
        # pi: "pick_info", i.e. an incomplete selection.
        ann = pi.artist.axes.annotate(_pick_info.get_ann_text(*pi),
                                      xy=pi.target,
                                      **default_annotation_kwargs)
        ann.draggable(use_blit=True)
        extras = []
        if self._highlight:
            extras.append(self.add_highlight(pi.artist))
        if not self._multiple:
            while self._selections:
                self._remove_selection(self._selections[-1])
        sel = pi._replace(annotation=ann, extras=extras)
        self._selections.append(sel)
        self._callbacks.process("add", sel)
        sel.artist.figure.canvas.draw_idle()
        return sel

    def add_highlight(self, artist):
        """Create, add and return a highlighting artist.

        It is up to the caller to register the artist with the proper
        `Selection` in order to ensure cleanup upon deselection.
        """
        hl = copy.copy(artist)
        hl.set(**default_highlight_kwargs)
        artist.axes.add_artist(hl)
        return hl

    def connect(self, event, func=None):
        """Connect a callback to a `Cursor` event; return the callback id.

        Two classes of event can be emitted, both with a `Selection` as single
        argument:

            - ``"add"`` when a `Selection` is added, and
            - ``"remove"`` when a `Selection` is removed.

        The callback registry relies on :mod:`matplotlib`'s implementation; in
        particular, only weak references are kept for bound methods.

        This method is can also be used as a decorator::

            @cursor.connect("add")
            def on_add(sel):
                ...
        """
        if event not in ["add", "remove"]:
            raise ValueError("Invalid cursor event: {}".format(event))
        if func is None:
            return partial(self.connect, event)
        return self._callbacks.connect(event, func)

    def disconnect(self, cid):
        """Disconnect a previously connected callback id.
        """
        self._callbacks.disconnect(cid)

    def remove(self):
        """Remove all `Selection`\\s and disconnect all callbacks.
        """
        for disconnect_cid in self._disconnect_cids:
            disconnect_cid()
        while self._selections:
            self._remove_selection(self._selections[-1])

    def _on_button_press(self, event):
        if event.button == self._bindings["select"]:
            self._on_select_button_press(event)
        if event.button == self._bindings["deselect"]:
            self._on_deselect_button_press(event)

    def _filter_mouse_event(self, event):
        # Accept the event iff we are enabled, and either
        #   - no other widget is active, and this is not the second click of a
        #     double click (to prevent double selection), or
        #   - another widget is active, and this is a double click (to bypass
        #     the widget lock).
        return (self.enabled
                and event.canvas.widgetlock.locked() == event.dblclick)

    def _on_select_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        # Work around lack of support for twinned axes.
        per_axes_event = {
            ax: _reassigned_axes_event(event, ax)
            for ax in self._axes
        }
        pis = []
        for artist in self.artists:
            if (artist.axes is None  # Removed or figure-level artist.
                    or event.canvas is not artist.figure.canvas
                    or not artist.axes.contains(event)[0]):  # Cropped by axes.
                continue
            pi = _pick_info.compute_pick(artist, per_axes_event[artist.axes])
            if pi:
                pis.append(pi)
        if not pis:
            return
        self.add_selection(min(pis, key=lambda pi: pi.dist))

    def _on_deselect_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        for sel in self._selections:
            ann = sel.annotation
            if event.canvas is not ann.figure.canvas:
                continue
            contained, _ = ann.contains(event)
            if contained:
                self._remove_selection(sel)

    def _on_key_press(self, event):
        if event.key == self._bindings["toggle_enabled"]:
            self.enabled = not self.enabled
        elif event.key == self._bindings["toggle_visibility"]:
            for sel in self._selections:
                sel.annotation.set_visible(not sel.annotation.get_visible())
                sel.annotation.figure.canvas.draw_idle()
        if self._selections:
            sel = self._selections[-1]
        else:
            return
        for key in ["left", "right", "up", "down"]:
            if event.key == self._bindings[key]:
                self._remove_selection(sel)
                self.add_selection(_pick_info.move(*sel, key=key))
                break

    def _remove_selection(self, sel):
        self._selections.remove(sel)
        # Work around matplotlib/matplotlib#6785.
        draggable = sel.annotation._draggable
        try:
            draggable.disconnect()
            sel.annotation.figure.canvas.mpl_disconnect(
                sel.annotation._draggable._c1)
        except AttributeError:
            pass
        # (end of workaround).
        # <artist>.figure will be unset so we save them first.
        figures = {artist.figure for artist in [sel.annotation, *sel.extras]}
        # ValueError is raised if the artist has already been removed.
        with suppress(ValueError):
            sel.annotation.remove()
        for artist in sel.extras:
            with suppress(ValueError):
                artist.remove()
        self._callbacks.process("remove", sel)
        for figure in figures:
            figure.canvas.draw_idle()
コード例 #2
0
class Cursor:
    """A cursor for selecting Matplotlib artists.

    Attributes
    ----------
    bindings : dict
        See the *bindings* keyword argument to the constructor.
    annotation_kwargs : dict
        See the *annotation_kwargs* keyword argument to the constructor.
    annotation_positions : dict
        See the *annotation_positions* keyword argument to the constructor.
    highlight_kwargs : dict
        See the *highlight_kwargs* keyword argument to the constructor.
    """

    _keep_alive = WeakKeyDictionary()

    def __init__(self,
                 artists,
                 *,
                 multiple=False,
                 highlight=False,
                 hover=False,
                 bindings=None,
                 annotation_kwargs=None,
                 annotation_positions=None,
                 highlight_kwargs=None):
        """Construct a cursor.

        Parameters
        ----------

        artists : List[Artist]
            A list of artists that can be selected by this cursor.

        multiple : bool, optional
            Whether multiple artists can be "on" at the same time (defaults to
            False).

        highlight : bool, optional
            Whether to also highlight the selected artist.  If so,
            "highlighter" artists will be placed as the first item in the
            :attr:`extras` attribute of the `Selection`.

        hover : bool, optional
            Whether to select artists upon hovering instead of by clicking.
            (Hovering over an artist while a button is pressed will not trigger
            a selection; right clicking on an annotation will still remove it.)

        bindings : dict, optional
            A mapping of button and keybindings to actions.  Valid entries are:

            ================ ==================================================
            'select'         mouse button to select an artist
                             (default: 1)
            'deselect'       mouse button to deselect an artist
                             (default: 3)
            'left'           move to the previous point in the selected path,
                             or to the left in the selected image
                             (default: shift+left)
            'right'          move to the next point in the selected path, or to
                             the right in the selected image
                             (default: shift+right)
            'up'             move up in the selected image
                             (default: shift+up)
            'down'           move down in the selected image
                             (default: shift+down)
            'toggle_enabled' toggle whether the cursor is active
                             (default: e)
            'toggle_visible' toggle default cursor visibility and apply it to
                             all cursors (default: v)
            ================ ==================================================

            Missing entries will be set to the defaults.  In order to not
            assign any binding to an action, set it to ``None``.

        annotation_kwargs : dict, optional
            Keyword argments passed to the `annotate
            <matplotlib.axes.Axes.annotate>` call.

        annotation_positions : List[dict], optional
            List of positions tried by the annotation positioning algorithm.

        highlight_kwargs : dict, optional
            Keyword arguments used to create a highlighted artist.
        """

        artists = list(artists)
        # Be careful with GC.
        self._artists = [weakref.ref(artist) for artist in artists]

        for artist in artists:
            type(self)._keep_alive.setdefault(artist, set()).add(self)

        self._multiple = multiple
        self._highlight = highlight

        self._visible = True
        self._enabled = True
        self._selections = []
        self._last_auto_position = None
        self._callbacks = CallbackRegistry()

        connect_pairs = [("key_press_event", self._on_key_press)]
        if hover:
            if multiple:
                raise ValueError("'hover' and 'multiple' are incompatible")
            connect_pairs += [
                ("motion_notify_event", self._hover_handler),
                ("button_press_event", self._hover_handler),
            ]
        else:
            connect_pairs += [("button_press_event", self._nonhover_handler)]
        self._disconnectors = [
            partial(canvas.mpl_disconnect, canvas.mpl_connect(*pair))
            for pair in connect_pairs
            for canvas in {artist.figure.canvas
                           for artist in artists}
        ]

        bindings = dict(
            ChainMap(bindings if bindings is not None else {},
                     _default_bindings))
        unknown_bindings = set(bindings) - set(_default_bindings)
        if unknown_bindings:
            raise ValueError("Unknown binding(s): {}".format(", ".join(
                sorted(unknown_bindings))))
        duplicate_bindings = [
            k for k, v in Counter(list(bindings.values())).items() if v > 1
        ]
        if duplicate_bindings:
            raise ValueError("Duplicate binding(s): {}".format(", ".join(
                sorted(map(str, duplicate_bindings)))))
        self.bindings = bindings

        self.annotation_kwargs = (annotation_kwargs
                                  if annotation_kwargs is not None else
                                  copy.deepcopy(_default_annotation_kwargs))
        self.annotation_positions = (
            annotation_positions if annotation_positions is not None else
            copy.deepcopy(_default_annotation_positions))
        self.highlight_kwargs = (highlight_kwargs
                                 if highlight_kwargs is not None else
                                 copy.deepcopy(_default_highlight_kwargs))

    @property
    def artists(self):
        """The tuple of selectable artists.
        """
        # Work around matplotlib/matplotlib#6982: `cla()` does not clear
        # `.axes`.
        return tuple(filter(_is_alive, (ref() for ref in self._artists)))

    @property
    def enabled(self):
        """Whether clicks are registered for picking and unpicking events.
        """
        return self._enabled

    @enabled.setter
    def enabled(self, value):
        self._enabled = value

    @property
    def selections(self):
        """The tuple of current `Selection`\\s.
        """
        for sel in self._selections:
            if sel.annotation.axes is None:
                raise RuntimeError("Annotation unexpectedly removed; "
                                   "use 'cursor.remove_selection' instead")
        return tuple(self._selections)

    @property
    def visible(self):
        """Whether selections are visible by default.

        Setting this property also updates the visibility status of current
        selections.
        """
        return self._visible

    @visible.setter
    def visible(self, value):
        self._visible = value
        for sel in self.selections:
            sel.annotation.set_visible(value)
            sel.annotation.figure.canvas.draw_idle()

    def add_selection(self, pi):
        """Create an annotation for a `Selection` and register it.

        Returns a new `Selection`, that has been registered by the `Cursor`,
        with the added annotation set in the :attr:`annotation` field and, if
        applicable, the highlighting artist in the :attr:`extras` field.

        Emits the ``"add"`` event with the new `Selection` as argument.  When
        the event is emitted, the position of the annotation is temporarily
        set to ``(nan, nan)``; if this position is not explicitly set by a
        callback, then a suitable position will be automatically computed.

        Likewise, if the text alignment is not explicitly set but the position
        is, then a suitable alignment will be automatically computed.
        """
        # pi: "pick_info", i.e. an incomplete selection.
        # Pre-fetch the figure and axes, as callbacks may actually unset them.
        figure = pi.artist.figure
        axes = pi.artist.axes
        if axes.get_renderer_cache() is None:
            figure.canvas.draw()  # Needed by draw_artist below anyways.
        renderer = pi.artist.axes.get_renderer_cache()
        ann = pi.artist.axes.annotate(_pick_info.get_ann_text(*pi),
                                      xy=pi.target,
                                      xytext=(np.nan, np.nan),
                                      ha=_MarkedStr("center"),
                                      va=_MarkedStr("center"),
                                      visible=self.visible,
                                      **self.annotation_kwargs)
        ann.draggable(use_blit=True)
        extras = []
        if self._highlight:
            hl = self.add_highlight(*pi)
            if hl:
                extras.append(hl)
        sel = pi._replace(annotation=ann, extras=extras)
        self._selections.append(sel)
        self._callbacks.process("add", sel)

        # Check that `ann.axes` is still set, as callbacks may have removed the
        # annotation.
        if ann.axes and ann.xyann == (np.nan, np.nan):
            fig_bbox = figure.get_window_extent()
            ax_bbox = axes.get_window_extent()
            overlaps = []
            for idx, annotation_position in enumerate(
                    self.annotation_positions):
                ann.set(**annotation_position)
                # Work around matplotlib/matplotlib#7614: position update is
                # missing.
                ann.update_positions(renderer)
                bbox = ann.get_window_extent(renderer)
                overlaps.append((
                    _get_rounded_intersection_area(fig_bbox, bbox),
                    _get_rounded_intersection_area(ax_bbox, bbox),
                    # Avoid needlessly jumping around by breaking ties using
                    # the last used position as default.
                    idx == self._last_auto_position,
                ))
            auto_position = max(range(len(overlaps)), key=overlaps.__getitem__)
            ann.set(**self.annotation_positions[auto_position])
            self._last_auto_position = auto_position
        else:
            if isinstance(ann.get_ha(), _MarkedStr):
                ann.set_ha({
                    -1: "right",
                    0: "center",
                    1: "left"
                }[np.sign(np.nan_to_num(ann.xyann[0]))])
            if isinstance(ann.get_va(), _MarkedStr):
                ann.set_va({
                    -1: "top",
                    0: "center",
                    1: "bottom"
                }[np.sign(np.nan_to_num(ann.xyann[1]))])

        if (extras or len(self.selections) > 1 and not self._multiple
                or not figure.canvas.supports_blit):
            # Either:
            #  - there may be more things to draw, or
            #  - annotation removal will make a full redraw necessary, or
            #  - blitting is not (yet) supported.
            figure.canvas.draw_idle()
        elif ann.axes:
            # Fast path, only needed if the annotation has not been immediately
            # removed.
            figure.draw_artist(ann)
            # Explicit argument needed on MacOSX backend.
            figure.canvas.blit(figure.bbox)
        # Removal comes after addition so that the fast blitting path works.
        if not self._multiple:
            for sel in self.selections[:-1]:
                self.remove_selection(sel)
        return sel

    def add_highlight(self, artist, *args, **kwargs):
        """Create, add and return a highlighting artist.

        This method is should be called with an "unpacked" `Selection`,
        possibly with some fields set to None.

        It is up to the caller to register the artist with the proper
        `Selection` in order to ensure cleanup upon deselection.
        """
        hl = _pick_info.make_highlight(
            artist, *args,
            **ChainMap({"highlight_kwargs": self.highlight_kwargs}, kwargs))
        if hl:
            artist.axes.add_artist(hl)
            return hl

    def connect(self, event, func=None):
        """Connect a callback to a `Cursor` event; return the callback id.

        Two classes of event can be emitted, both with a `Selection` as single
        argument:

            - ``"add"`` when a `Selection` is added, and
            - ``"remove"`` when a `Selection` is removed.

        The callback registry relies on Matplotlib's implementation; in
        particular, only weak references are kept for bound methods.

        This method is can also be used as a decorator::

            @cursor.connect("add")
            def on_add(sel):
                ...

        Examples of callbacks::

            # Change the annotation text and alignment:
            lambda sel: sel.annotation.set(
                text=sel.artist.get_label(),  # or use e.g. sel.target.index
                ha="center", va="bottom")

            # Make label non-draggable:
            lambda sel: sel.draggable(False)
        """
        if event not in ["add", "remove"]:
            raise ValueError("Invalid cursor event: {}".format(event))
        if func is None:
            return partial(self.connect, event)
        return self._callbacks.connect(event, func)

    def disconnect(self, cid):
        """Disconnect a previously connected callback id.
        """
        self._callbacks.disconnect(cid)

    def remove(self):
        """Remove a cursor.

        Remove all `Selection`\\s, disconnect all callbacks, and allow the
        cursor to be garbage collected.
        """
        for disconnectors in self._disconnectors:
            disconnectors()
        for sel in self.selections:
            self.remove_selection(sel)
        for s in type(self)._keep_alive.values():
            with suppress(KeyError):
                s.remove(self)

    def _nonhover_handler(self, event):
        if event.name == "button_press_event":
            if event.button == self.bindings["select"]:
                self._on_select_button_press(event)
            if event.button == self.bindings["deselect"]:
                self._on_deselect_button_press(event)

    def _hover_handler(self, event):
        if event.name == "motion_notify_event" and event.button is None:
            # Filter away events where the mouse is pressed, in particular to
            # avoid conflicts between hover and draggable.
            self._on_select_button_press(event)
        elif (event.name == "button_press_event"
              and event.button == self.bindings["deselect"]):
            # Still allow removing the annotation by right clicking.
            self._on_deselect_button_press(event)

    def _filter_mouse_event(self, event):
        # Accept the event iff we are enabled, and either
        #   - no other widget is active, and this is not the second click of a
        #     double click (to prevent double selection), or
        #   - another widget is active, and this is a double click (to bypass
        #     the widget lock).
        return self.enabled and event.canvas.widgetlock.locked(
        ) == event.dblclick

    def _on_select_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        # Work around lack of support for twinned axes.
        per_axes_event = {
            ax: _reassigned_axes_event(event, ax)
            for ax in {artist.axes
                       for artist in self.artists}
        }
        pis = []
        for artist in self.artists:
            if (artist.axes is None  # Removed or figure-level artist.
                    or event.canvas is not artist.figure.canvas
                    or not artist.axes.contains(event)[0]):  # Cropped by axes.
                continue
            pi = _pick_info.compute_pick(artist, per_axes_event[artist.axes])
            if pi:
                pis.append(pi)
        if not pis:
            return
        self.add_selection(min(pis, key=lambda pi: pi.dist))

    def _on_deselect_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        for sel in self.selections:
            ann = sel.annotation
            if event.canvas is not ann.figure.canvas:
                continue
            contained, _ = ann.contains(event)
            if contained:
                self.remove_selection(sel)

    def _on_key_press(self, event):
        if event.key == self.bindings["toggle_enabled"]:
            self.enabled = not self.enabled
        elif event.key == self.bindings["toggle_visible"]:
            self.visible = not self.visible
        try:
            sel = self.selections[-1]
        except IndexError:
            return
        for key in ["left", "right", "up", "down"]:
            if event.key == self.bindings[key]:
                self.remove_selection(sel)
                self.add_selection(_pick_info.move(*sel, key=key))
                break

    def remove_selection(self, sel):
        """Remove a `Selection`.
        """
        self._selections.remove(sel)
        # <artist>.figure will be unset so we save them first.
        figures = {artist.figure for artist in [sel.annotation] + sel.extras}
        # ValueError is raised if the artist has already been removed.
        with suppress(ValueError):
            sel.annotation.remove()
        for artist in sel.extras:
            with suppress(ValueError):
                artist.remove()
        self._callbacks.process("remove", sel)
        for figure in figures:
            figure.canvas.draw_idle()
コード例 #3
0
"""
cbook即为cookbook,是一些小工具组成的库

"""
from matplotlib.cbook import CallbackRegistry

callbacks = CallbackRegistry()
sum = lambda x, y: print(f'{x}+{y}={x + y}')
mul = lambda x, y: print(f"{x} * {y}={x * y}")
id_sum = callbacks.connect("sum", sum)
id_mul = callbacks.connect("mul", mul)
callbacks.process('sum', 3, 4)
callbacks.process("mul", 5, 6)
callbacks.disconnect(id_sum)
callbacks.process("sum", 7, 8)
コード例 #4
0
ファイル: widgets.py プロジェクト: ianhi/mpl-interactions
class scatter_selector(AxesWidget):
    """
    A widget for selecting a point in a scatter plot. callback will receive (index, (x, y))
    """

    def __init__(self, ax, x, y, pickradius=5, which_button=1, **kwargs):
        """
        Create the scatter plot and selection machinery.

        Parameters
        ----------
        ax : Axes
            The Axes on which to make the scatter plot
        x, y : float or array-like, shape (n, )
            The data positions.
        pickradius : float
            Pick radius, in points.
        which_button : int, default: 1
            Where 1=left, 2=middle, 3=right

        Other Parameters
        ----------------
        **kwargs : arguments to scatter
            Other keyword arguments are passed directly to the ``ax.scatter`` command

        """
        super().__init__(ax)
        self.scatter = ax.scatter(x, y, **kwargs, picker=True)
        self.scatter.set_pickradius(pickradius)
        self._observers = CallbackRegistry()
        self._x = x
        self._y = y
        self._button = which_button
        self.connect_event("pick_event", self._on_pick)
        self._init_val()

    def _init_val(self):
        self.val = (0, (self._x[0], self._y[0]))

    def _on_pick(self, event):
        if event.mouseevent.button == self._button:
            idx = event.ind[0]
            x = self._x[idx]
            y = self._y[idx]
            self._process(idx, (x, y))

    def _process(idx, val):
        self._observers.process("picked", idx, val)

    def on_changed(self, func):
        """
        When a point is clicked calll *func* with the newly selected point

        Parameters
        ----------
        func : callable
            Function to call when slider is changed.
            The function must accept a (int, tuple(float, float)) as its arguments.

        Returns
        -------
        int
            Connection id (which can be used to disconnect *func*)
        """
        return self._observers.connect("picked", lambda idx, val: func(idx, val))
コード例 #5
0
ファイル: _mplcursors.py プロジェクト: bbgky/mplcursors
class Cursor:
    """A cursor for selecting artists on a matplotlib figure.
    """

    _keep_alive = WeakKeyDictionary()

    def __init__(self,
                 artists,
                 *,
                 multiple=False,
                 highlight=False,
                 hover=False,
                 bindings=default_bindings):
        """Construct a cursor.

        Parameters
        ----------

        artists : List[Artist]
            A list of artists that can be selected by this cursor.

        multiple : bool
            Whether multiple artists can be "on" at the same time (defaults to
            False).

        highlight : bool
            Whether to also highlight the selected artist.  If so,
            "highlighter" artists will be placed as the first item in the
            :attr:`extras` attribute of the `Selection`.

        bindings : dict
            A mapping of button and keybindings to actions.  Valid entries are:

            =================== ===============================================
            'select'            mouse button to select an artist (default: 1)
            'deselect'          mouse button to deselect an artist (default: 3)
            'left'              move to the previous point in the selected
                                path, or to the left in the selected image
                                (default: shift+left)
            'right'             move to the next point in the selected path, or
                                to the right in the selected image
                                (default: shift+right)
            'up'                move up in the selected image
                                (default: shift+up)
            'down'              move down in the selected image
                                (default: shift+down)
            'toggle_visibility' toggle visibility of all cursors (default: d)
            'toggle_enabled'    toggle whether the cursor is active
                                (default: t)
            =================== ===============================================

        hover : bool
            Whether to select artists upon hovering instead of by clicking.
        """

        artists = list(artists)
        # Be careful with GC.
        self._artists = [weakref.ref(artist) for artist in artists]

        for artist in artists:
            type(self)._keep_alive.setdefault(artist, []).append(self)

        self._multiple = multiple
        self._highlight = highlight

        self._axes = {artist.axes for artist in artists}
        self._enabled = True
        self._selections = []
        self._callbacks = CallbackRegistry()

        connect_pairs = [("key_press_event", self._on_key_press)]
        if hover:
            if multiple:
                raise ValueError("`hover` and `multiple` are incompatible")
            connect_pairs += [
                ("motion_notify_event", self._on_select_button_press)]
        else:
            connect_pairs += [
                ("button_press_event", self._on_button_press)]
        self._disconnect_cids = [
            partial(canvas.mpl_disconnect, canvas.mpl_connect(*pair))
            for pair in connect_pairs
            for canvas in {artist.figure.canvas for artist in artists}]

        bindings = {**default_bindings, **bindings}
        if set(bindings) != set(default_bindings):
            raise ValueError("Unknown bindings")
        actually_bound = {k: v for k, v in bindings.items() if v is not None}
        if len(set(actually_bound.values())) != len(actually_bound):
            raise ValueError("Duplicate bindings")
        self._bindings = bindings

    @property
    def enabled(self):
        """Whether clicks are registered for picking and unpicking events.
        """
        return self._enabled

    @enabled.setter
    def enabled(self, value):
        self._enabled = value

    @property
    def artists(self):
        """The tuple of selectable artists.
        """
        return tuple(filter(None, (ref() for ref in self._artists)))

    @property
    def selections(self):
        """The tuple of current `Selection`\\s.
        """
        return tuple(self._selections)

    def add_selection(self, pi):
        """Create an annotation for a `Selection` and register it.

        Returns a new `Selection`, that has been registered by the `Cursor`,
        with the added annotation set in the :attr:`annotation` field and, if
        applicable, the highlighting artist in the :attr:`extras` field.

        Emits the ``"add"`` event with the new `Selection` as argument.
        """
        # pi: "pick_info", i.e. an incomplete selection.
        ann = pi.artist.axes.annotate(
            _pick_info.get_ann_text(*pi),
            xy=pi.target,
            **default_annotation_kwargs)
        ann.draggable(use_blit=True)
        extras = []
        if self._highlight:
            extras.append(self.add_highlight(pi.artist))
        if not self._multiple:
            while self._selections:
                self._remove_selection(self._selections[-1])
        sel = pi._replace(annotation=ann, extras=extras)
        self._selections.append(sel)
        self._callbacks.process("add", sel)
        sel.artist.figure.canvas.draw_idle()
        return sel

    def add_highlight(self, artist):
        """Create, add and return a highlighting artist.

        It is up to the caller to register the artist with the proper
        `Selection` in order to ensure cleanup upon deselection.
        """
        hl = copy.copy(artist)
        hl.set(**default_highlight_kwargs)
        artist.axes.add_artist(hl)
        return hl

    def connect(self, event, func=None):
        """Connect a callback to a `Cursor` event; return the callback id.

        Two classes of event can be emitted, both with a `Selection` as single
        argument:

            - ``"add"`` when a `Selection` is added, and
            - ``"remove"`` when a `Selection` is removed.

        The callback registry relies on :mod:`matplotlib`'s implementation; in
        particular, only weak references are kept for bound methods.

        This method is can also be used as a decorator::

            @cursor.connect("add")
            def on_add(sel):
                ...
        """
        if event not in ["add", "remove"]:
            raise ValueError("Invalid cursor event: {}".format(event))
        if func is None:
            return partial(self.connect, event)
        return self._callbacks.connect(event, func)

    def disconnect(self, cid):
        """Disconnect a previously connected callback id.
        """
        self._callbacks.disconnect(cid)

    def remove(self):
        """Remove all `Selection`\\s and disconnect all callbacks.
        """
        for disconnect_cid in self._disconnect_cids:
            disconnect_cid()
        while self._selections:
            self._remove_selection(self._selections[-1])

    def _on_button_press(self, event):
        if event.button == self._bindings["select"]:
            self._on_select_button_press(event)
        if event.button == self._bindings["deselect"]:
            self._on_deselect_button_press(event)

    def _filter_mouse_event(self, event):
        # Accept the event iff we are enabled, and either
        #   - no other widget is active, and this is not the second click of a
        #     double click (to prevent double selection), or
        #   - another widget is active, and this is a double click (to bypass
        #     the widget lock).
        return (self.enabled
                and event.canvas.widgetlock.locked() == event.dblclick)

    def _on_select_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        # Work around lack of support for twinned axes.
        per_axes_event = {ax: _reassigned_axes_event(event, ax)
                          for ax in self._axes}
        pis = []
        for artist in self.artists:
            if (artist.axes is None  # Removed or figure-level artist.
                    or event.canvas is not artist.figure.canvas
                    or not artist.axes.contains(event)[0]):  # Cropped by axes.
                continue
            pi = _pick_info.compute_pick(artist, per_axes_event[artist.axes])
            if pi:
                pis.append(pi)
        if not pis:
            return
        self.add_selection(min(pis, key=lambda pi: pi.dist))

    def _on_deselect_button_press(self, event):
        if not self._filter_mouse_event(event):
            return
        for sel in self._selections:
            ann = sel.annotation
            if event.canvas is not ann.figure.canvas:
                continue
            contained, _ = ann.contains(event)
            if contained:
                self._remove_selection(sel)

    def _on_key_press(self, event):
        if event.key == self._bindings["toggle_enabled"]:
            self.enabled = not self.enabled
        elif event.key == self._bindings["toggle_visibility"]:
            for sel in self._selections:
                sel.annotation.set_visible(not sel.annotation.get_visible())
                sel.annotation.figure.canvas.draw_idle()
        if self._selections:
            sel = self._selections[-1]
        else:
            return
        for key in ["left", "right", "up", "down"]:
            if event.key == self._bindings[key]:
                self._remove_selection(sel)
                self.add_selection(_pick_info.move(*sel, key=key))
                break

    def _remove_selection(self, sel):
        self._selections.remove(sel)
        # Work around matplotlib/matplotlib#6785.
        draggable = sel.annotation._draggable
        try:
            draggable.disconnect()
            sel.annotation.figure.canvas.mpl_disconnect(
                sel.annotation._draggable._c1)
        except AttributeError:
            pass
        # (end of workaround).
        # <artist>.figure will be unset so we save them first.
        figures = {artist.figure for artist in [sel.annotation, *sel.extras]}
        # ValueError is raised if the artist has already been removed.
        with suppress(ValueError):
            sel.annotation.remove()
        for artist in sel.extras:
            with suppress(ValueError):
                artist.remove()
        self._callbacks.process("remove", sel)
        for figure in figures:
            figure.canvas.draw_idle()