46

In MATLAB, one can use datacursormode to add annotation to a graph when user mouses over. Is there such thing in matplotlib? Or I need to write my own event using matplotlib.text.Annotation?

sodd
  • 12,482
  • 3
  • 54
  • 62
Ping C
  • 561
  • 1
  • 5
  • 4

1 Answers1

64

Late Edit / Shameless Plug: This is now available (with much more functionality) as mpldatacursor. Calling mpldatacursor.datacursor() will enable it for all matplotlib artists (including basic support for z-values in images, etc).


As far as I know, there isn't one already implemented, but it's not too hard to write something similar:

import matplotlib.pyplot as plt

class DataCursor(object):
    text_template = 'x: %0.2f\ny: %0.2f'
    x, y = 0.0, 0.0
    xoffset, yoffset = -20, 20
    text_template = 'x: %0.2f\ny: %0.2f'

    def __init__(self, ax):
        self.ax = ax
        self.annotation = ax.annotate(self.text_template, 
                xy=(self.x, self.y), xytext=(self.xoffset, self.yoffset), 
                textcoords='offset points', ha='right', va='bottom',
                bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')
                )
        self.annotation.set_visible(False)

    def __call__(self, event):
        self.event = event
        # xdata, ydata = event.artist.get_data()
        # self.x, self.y = xdata[event.ind], ydata[event.ind]
        self.x, self.y = event.mouseevent.xdata, event.mouseevent.ydata
        if self.x is not None:
            self.annotation.xy = self.x, self.y
            self.annotation.set_text(self.text_template % (self.x, self.y))
            self.annotation.set_visible(True)
            event.canvas.draw()

fig = plt.figure()
line, = plt.plot(range(10), 'ro-')
fig.canvas.mpl_connect('pick_event', DataCursor(plt.gca()))
line.set_picker(5) # Tolerance in points

Datacursor-ish thing in matplotlib

As it seems like at least a few people are using this, I've added an updated version below.

The new version has a simpler usage and a lot more documentation (i.e. a tiny bit, at least).

Basically you'd use it similar to this:

plt.figure()
plt.subplot(2,1,1)
line1, = plt.plot(range(10), 'ro-')
plt.subplot(2,1,2)
line2, = plt.plot(range(10), 'bo-')

DataCursor([line1, line2])

plt.show()

The main differences are that a) there's no need to manually call line.set_picker(...), b) there's no need to manually call fig.canvas.mpl_connect, and c) this version handles multiple axes and multiple figures.

from matplotlib import cbook

class DataCursor(object):
    """A simple data cursor widget that displays the x,y location of a
    matplotlib artist when it is selected."""
    def __init__(self, artists, tolerance=5, offsets=(-20, 20), 
                 template='x: %0.2f\ny: %0.2f', display_all=False):
        """Create the data cursor and connect it to the relevant figure.
        "artists" is the matplotlib artist or sequence of artists that will be 
            selected. 
        "tolerance" is the radius (in points) that the mouse click must be
            within to select the artist.
        "offsets" is a tuple of (x,y) offsets in points from the selected
            point to the displayed annotation box
        "template" is the format string to be used. Note: For compatibility
            with older versions of python, this uses the old-style (%) 
            formatting specification.
        "display_all" controls whether more than one annotation box will
            be shown if there are multiple axes.  Only one will be shown
            per-axis, regardless. 
        """
        self.template = template
        self.offsets = offsets
        self.display_all = display_all
        if not cbook.iterable(artists):
            artists = [artists]
        self.artists = artists
        self.axes = tuple(set(art.axes for art in self.artists))
        self.figures = tuple(set(ax.figure for ax in self.axes))

        self.annotations = {}
        for ax in self.axes:
            self.annotations[ax] = self.annotate(ax)

        for artist in self.artists:
            artist.set_picker(tolerance)
        for fig in self.figures:
            fig.canvas.mpl_connect('pick_event', self)

    def annotate(self, ax):
        """Draws and hides the annotation box for the given axis "ax"."""
        annotation = ax.annotate(self.template, xy=(0, 0), ha='right',
                xytext=self.offsets, textcoords='offset points', va='bottom',
                bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')
                )
        annotation.set_visible(False)
        return annotation

    def __call__(self, event):
        """Intended to be called through "mpl_connect"."""
        # Rather than trying to interpolate, just display the clicked coords
        # This will only be called if it's within "tolerance", anyway.
        x, y = event.mouseevent.xdata, event.mouseevent.ydata
        annotation = self.annotations[event.artist.axes]
        if x is not None:
            if not self.display_all:
                # Hide any other annotation boxes...
                for ann in self.annotations.values():
                    ann.set_visible(False)
            # Update the annotation in the current axis..
            annotation.xy = x, y
            annotation.set_text(self.template % (x, y))
            annotation.set_visible(True)
            event.canvas.draw()

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    plt.figure()
    plt.subplot(2,1,1)
    line1, = plt.plot(range(10), 'ro-')
    plt.subplot(2,1,2)
    line2, = plt.plot(range(10), 'bo-')

    DataCursor([line1, line2])

    plt.show()
Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • Joe, I commented out `xdata, ydata = event.artist.get_data()` because it doesn't appear to be used, and raised a [question](http://stackoverflow.com/q/8956794/190597). Hope that's okay. – unutbu Jan 21 '12 at 22:59
  • Absolutely, thanks! I shouldn't have left it in there. Also, I should probably update this... It would make more sense to pass in a particular artist rather than an axis. – Joe Kington Jan 22 '12 at 18:18
  • I'd be interested to see what you have in mind. Wouldn't you need the axis to call `ax.annotate`? – unutbu Jan 22 '12 at 21:21
  • 2
    You can access it with `artist.axes`. Give me just a bit, I'll add it. If people find it useful, I may try submitting the new, cleaned-up version for inclusion in `matplotlib.widgets`. I don't know if the devs would think it's a good idea or not, but I think I'll ask, anyway. – Joe Kington Jan 22 '12 at 21:25
  • This is really cool. Also check out the matplotlib example [here](http://matplotlib.sourceforge.net/examples/pylab_examples/cursor_demo.html). – Mark Mikofski Aug 11 '12 at 03:46
  • 1
    Does this work for 2D images shown by `imshow`? i.e. does it show the z-value of the displayed image? Currently, the viewer only shows x and y coordinates, which is somewhat unfortunate... – rubenvb Feb 20 '13 at 15:48
  • @rubenvb - Have a look at `mpldatacursor.ImageDataCursor` here: https://github.com/joferkington/mpldatacursor – Joe Kington Feb 22 '13 at 04:04
  • also related link: https://matplotlib.org/gallery/misc/cursor_demo_sgskip.html (this page links to the https://github.com/joferkington/mpldatacursor and another github) – Trevor Boyd Smith Apr 19 '19 at 15:39