241

I am using matplotlib to make scatter plots. Each point on the scatter plot is associated with a named object. I would like to be able to see the name of an object when I hover my cursor over the point on the scatter plot associated with that object. In particular, it would be nice to be able to quickly see the names of the points that are outliers. The closest thing I have been able to find while searching here is the annotate command, but that appears to create a fixed label on the plot. Unfortunately, with the number of points that I have, the scatter plot would be unreadable if I labeled each point. Does anyone know of a way to create labels that only appear when the cursor hovers in the vicinity of that point?

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
jdmcbr
  • 5,964
  • 6
  • 28
  • 38
  • 4
    People ending up here through search might also want to check [this answer](https://stackoverflow.com/a/13306887/4124317), which is rather complex, but might be suitable depending on the requirements. – ImportanceOfBeingErnest Nov 07 '17 at 21:03

12 Answers12

246

Here is a code that uses a scatter and shows an annotation upon hovering over the scatter points.

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

x = np.random.rand(15)
y = np.random.rand(15)
names = np.array(list("ABCDEFGHIJKLMNO"))
c = np.random.randint(1,5,size=15)

norm = plt.Normalize(1,4)
cmap = plt.cm.RdYlGn

fig,ax = plt.subplots()
sc = plt.scatter(x,y,c=c, s=100, cmap=cmap, norm=norm)

annot = ax.annotate("", xy=(0,0), xytext=(20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(ind):
    
    pos = sc.get_offsets()[ind["ind"][0]]
    annot.xy = pos
    text = "{}, {}".format(" ".join(list(map(str,ind["ind"]))), 
                           " ".join([names[n] for n in ind["ind"]]))
    annot.set_text(text)
    annot.get_bbox_patch().set_facecolor(cmap(norm(c[ind["ind"][0]])))
    annot.get_bbox_patch().set_alpha(0.4)
    

def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax:
        cont, ind = sc.contains(event)
        if cont:
            update_annot(ind)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()

fig.canvas.mpl_connect("motion_notify_event", hover)

plt.show()

enter image description here

Because people also want to use this solution for a line plot instead of a scatter, the following would be the same solution for plot (which works slightly differently).

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

x = np.sort(np.random.rand(15))
y = np.sort(np.random.rand(15))
names = np.array(list("ABCDEFGHIJKLMNO"))

norm = plt.Normalize(1,4)
cmap = plt.cm.RdYlGn

fig,ax = plt.subplots()
line, = plt.plot(x,y, marker="o")

annot = ax.annotate("", xy=(0,0), xytext=(-20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(ind):
    x,y = line.get_data()
    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    text = "{}, {}".format(" ".join(list(map(str,ind["ind"]))), 
                           " ".join([names[n] for n in ind["ind"]]))
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax:
        cont, ind = line.contains(event)
        if cont:
            update_annot(ind)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()

fig.canvas.mpl_connect("motion_notify_event", hover)

plt.show()

In case someone is looking for a solution for lines in twin axes, refer to How to make labels appear when hovering over a point in multiple axis?

In case someone is looking for a solution for bar plots, please refer to e.g. this answer.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • 1
    Very nice! One note, I noticed that `ind["ind"]` is actually a list of indexes for all points under the curser. This means that the above code actually gives you access to all points at a given position, and not just the top most point. For instance, if you have two overlapping points the text could read `1 2, B C` or even `1 2 3, B C D` if you had 3 overlapping points. – Jvinniec Nov 14 '17 at 13:45
  • @Jvinniec Exactly, there is deliberately one such case in the above plot (the green and red dot at x ~ 0.4). If you hover it it'll display `0 8, A I`, (see [picture](https://i.stack.imgur.com/Ssc4o.png)). – ImportanceOfBeingErnest Nov 14 '17 at 13:50
  • 1
    @ImportanceOfBeingErnest this is a great code, but when hovering and moving on a point it calls `fig.canvas.draw_idle()` many times (it even changes the cursor to idle). I solved it storing the previous index and checking if `ind["ind"][0] == prev_ind`. Then only update if you move from one point to another (update text), stop hovering (make the annotation invisible) or start hovering (make annotation visible). With this change it's way more clean and efficient. –  Dec 14 '17 at 15:02
  • @SembeiNorimaki To make this code more efficient, one would use ***blitting***. However, this is not meant to be a full add-on code, but rather a solution to the question. As an answer to the question it should also be understandable to the many readers here. – ImportanceOfBeingErnest Dec 14 '17 at 15:08
  • Pardon, @ImportanceOfBeingErnest, you are right. Thank you. This is really good code. – Ursa Major Apr 10 '18 at 01:36
  • For a bar plot you don't need `.contains` at all. You just take the information from the bar itself. – ImportanceOfBeingErnest Apr 17 '18 at 16:14
  • Because `bar`-like graphs returns a container of `artist` objects, the code above cannot be straightforwardly duplicated. For the record, one can find [here an example](https://gist.github.com/eboisseau/d5a3e4ebdf0659292be51594aaa29fd2) of an **bar graph**, annotated in the middle of each bar upon hovering. – ebosi Apr 17 '18 at 16:14
  • I upvoted... though, ind is an empty dictionary in a 3d scatter plot. Any idea how to get this to work there too? – jsfa11 May 07 '18 at 16:18
  • @jsfa11 I currently wouldn't know how to do the same for a 3D plot. Maybe there is an easy solution that I'm not aware of; or, one would need to find out if *the projection* of a point is hovered on (which might be a bit more cumbersome). In any case I would suggest to ask a new question about the 3D case. – ImportanceOfBeingErnest May 07 '18 at 16:43
  • Is it only me or is the run code snippet not working? Does it appear automatically and it is excpected to sometimes not work or is there a bug in the code that it the feature doesn't work? – Hakaishin May 15 '18 at 08:40
  • So you get a working example that you can immediately see when you press run code? I don't think so. I believe you that it is working, but just leave it inside comment tags, like this it is confusing, because user try to run the code – Hakaishin May 15 '18 at 10:14
  • @Hakaishin Sorry I misunderstood your comment. The example is working if you copy&paste it into a file on your computer and run it via python. You cannot of course not run it inside this website, because Stackoverflow does not have a python interpreter available. – ImportanceOfBeingErnest May 15 '18 at 10:17
  • 7
    @Konstantin Yes this solution will work when using `%matplotlib notebook` in an IPython/Jupyter notebook. – ImportanceOfBeingErnest Aug 25 '18 at 08:30
  • @ImportanceOfBeingErnest: Thanks for the solution.. but the annotations disappear when zoppend into the plot and hovered.. any idea? – Zakir Apr 22 '19 at 07:21
  • I wanted to modify the horizontal and vertical alignment depending on the axes quadrant, so that the label always points towards the center. I added the `annot.set_ha`, `annot.set_va` and modified xytext, but they are not aligned properly when redrawn. The box is aligned to the xy coordinates (end of the arrow) instead of the start of the arrow. I end up with the box and the arrow on the correct quadrant but they are one on top of the other, the box hides the arrow. Any idea why? – OriolAbril Jun 26 '19 at 14:19
  • I tried modifying the order of `annot.set_hat` and `annot.xytext` but the result is the same. However, if I call `ax.annotate` with the same parameters, the arrow and text are aligned properly. – OriolAbril Jun 26 '19 at 14:20
  • 3
    @OriolAbril (and everyone else), If you have a problem that arose when modifying the code from this answer, please ask a question about it, link to this answer and show the code you have attempted. I have no way to know what's wrong with each of your codes without actually seeing it. – ImportanceOfBeingErnest Jun 26 '19 at 14:29
  • 1
    I copy-pasted the code as is and it didn't work in a jupyter notebook version 5.7.0. – Morteza Milani Jul 09 '19 at 21:51
  • @MortezaMilani Jupyter will by default show png images, which are of course not interactive. – ImportanceOfBeingErnest Jul 09 '19 at 21:53
  • Howdy I was trying to use this for some code. I only made a few mods, so I thought it would work but it does not. I am using python 3. Here is a link to what I am trying to do: https://repl.it/repls/ConsciousRewardingPublisher @ImportanceOfBeingErnest – new_programmer_22 Oct 06 '19 at 01:33
  • in the line snippet, when you iterate the lines in the hover method, if a line is found (if cont: ...) need to break at the end of the if – omer schleifer Oct 16 '19 at 07:59
  • I'm trying to apply this to a seaborn heatmap. `cont, ind = heat.contains(event)` returns `True,None`. How do I get it to return valid indexes? – Mitchell van Zuylen Feb 28 '20 at 10:35
  • It seems like a very slow code. Is there any way to make it faster? – brienna Apr 10 '20 at 18:17
  • You can add `annot.set_wrap(True)` into this solution if your labels run off the edge of the plot. – forgetso Dec 01 '20 at 19:07
  • How would you pass the attributes to update_annot and hover if these functions are not on the same file as the main plot code? – Delosari Apr 21 '21 at 00:17
  • This doesn't work for 3D scatter plots because the offsets (positions) of scatter points don't keep the same order as in the input data. On the other hand, the Path3DCollection returned by 3D scatter has an _offsets3d field which retains the input array ordering. It's possible to do the same sorting to figure out the internal ordering in the collection: `xs, ys, zs = sc._offsets3d; vxs, vys, vzs = proj_transform(xs, ys, zs, ax.get_proj()); sorted_z_indices = np.argsort(vzs)[::-1]; annot.set_text(names[sorted_z_indices[ind['ind'][0]]])` – user3208430 Apr 01 '22 at 20:47
  • Worked once, non-interactively, having marked jupyterlab nb as trusted, now does nothing. – jtlz2 Aug 11 '23 at 07:38
75

This solution works when hovering a line without the need to click it:

import matplotlib.pyplot as plt

# Need to create as global variable so our callback(on_plot_hover) can access
fig = plt.figure()
plot = fig.add_subplot(111)

# create some curves
for i in range(4):
    # Giving unique ids to each data member
    plot.plot(
        [i*1,i*2,i*3,i*4],
        gid=i)

def on_plot_hover(event):
    # Iterating over each data member plotted
    for curve in plot.get_lines():
        # Searching which data member corresponds to current mouse position
        if curve.contains(event)[0]:
            print("over %s" % curve.get_gid())
            
fig.canvas.mpl_connect('motion_notify_event', on_plot_hover)           
plt.show()
Guillaume Jacquenot
  • 11,217
  • 6
  • 43
  • 49
mbernasocchi
  • 813
  • 1
  • 6
  • 10
  • 3
    Very useful +1ed. You probably need to 'debounce' this because the motion_notify_event will repeat for motion inside the curve area. Simply checking that the curve object is equal to the previous curve seems to work. – bvanlew Dec 05 '16 at 10:58
  • 6
    Hmm - this didn't work out-of-the-box for me (so few things do with `matplotlib`...) - does this work with `ipython`/`jupyter` notebooks? Does it also work when there are multiple subplots? What about on a bar-chart rather than a line-graph? – dwanderson Jan 24 '17 at 20:17
  • 19
    This prints the label into the console when hovering. What about **making the label appear on the picture** when hovering ? I understood that to be the question. – Nikana Reklawyks Feb 06 '17 at 19:14
  • @mbernasocchi thank a lot, what do I need to feed in the gid argument if I want to see a histogram (a different one for each point in the scatter) or, even better, a heat-map of a 2D histogram? – Amitai Jun 28 '17 at 13:28
  • 1
    @NikanaReklawyks I added [an answer](https://stackoverflow.com/a/47166787/4124317) which actually answers the question. – ImportanceOfBeingErnest Nov 07 '17 at 20:52
  • +1, for easy to understand and replicable code. Also note that `plot.get_lines()` also gives the points not only the lines, if you have plotted the the points using `plot.plot()`. – warriorUSP Aug 18 '19 at 20:33
41
import matplotlib.pyplot as plt
import pandas_datareader as web  # only for test data; must be installed with conda or pip
from mplcursors import cursor  # separate package must be installed

# reproducible sample data as a pandas dataframe
df = web.DataReader('aapl', data_source='yahoo', start='2021-03-09', end='2022-06-13')

plt.figure(figsize=(12, 7))
plt.plot(df.index, df.Close)
cursor(hover=True)
plt.show()

enter image description here

Pandas

ax = df.plot(y='Close', figsize=(10, 7))
cursor(hover=True)
plt.show()

enter image description here

Seaborn

  • Works with axes-level plots like sns.lineplot, and figure-level plots like sns.relplot.
import seaborn as sns

# load sample data
tips = sns.load_dataset('tips')

sns.relplot(data=tips, x="total_bill", y="tip", hue="day", col="time")
cursor(hover=True)
plt.show()

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Yuchao Jiang
  • 3,522
  • 30
  • 23
39

From http://matplotlib.sourceforge.net/examples/event_handling/pick_event_demo.html :

from matplotlib.pyplot import figure, show
import numpy as npy
from numpy.random import rand


if 1: # picking on a scatter plot (matplotlib.collections.RegularPolyCollection)

    x, y, c, s = rand(4, 100)
    def onpick3(event):
        ind = event.ind
        print('onpick3 scatter:', ind, npy.take(x, ind), npy.take(y, ind))

    fig = figure()
    ax1 = fig.add_subplot(111)
    col = ax1.scatter(x, y, 100*s, c, picker=True)
    #fig.savefig('pscoll.eps')
    fig.canvas.mpl_connect('pick_event', onpick3)

show()
endolith
  • 25,479
  • 34
  • 128
  • 192
cyborg
  • 9,989
  • 4
  • 38
  • 56
  • 1
    This does just what I need, thank you! As a bonus, in order to get it implemented, I rewrote my program so that instead of creating two separate scatter plots in different colors on the same figure to represent two sets of data, I copied the example's method for assigning color to a point. This made my program a bit simpler to read, and less code. Now off to find a guide to converting a color to a number! – jdmcbr Oct 27 '11 at 01:09
  • 1
    This is for scatter plots. What about line plots? I tried to make it work on them but it does not. Is there a worksaround? – Sohaib Aug 28 '14 at 05:12
  • @Sohaib See my answer – texasflood Aug 01 '15 at 17:14
  • I have a question on this. When I scatter-plot my points like this: plt.scatter(X_reduced[y == i, 0], X_reduced[y == i, 1], c=c, label=target_name, picker=True) with a zip for i, c and target_name, is then the order of my indexes messed up? And I cant look up anymore to which datapoint it belongs? – Chris Nov 05 '15 at 13:01
  • This doesn't seem to work for jupyter 5 notebooks with ipython 5. Is there an easy way to fix that? The `print` statement should also use parens for compatibility with python 3 – nealmcb Apr 30 '17 at 03:15
15

A slight edit on an example provided in http://matplotlib.org/users/shell.html:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), '-', picker=5)  # 5 points tolerance


def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    print('onpick points:', *zip(xdata[ind], ydata[ind]))


fig.canvas.mpl_connect('pick_event', onpick)

plt.show()

This plots a straight line plot, as Sohaib was asking

endolith
  • 25,479
  • 34
  • 128
  • 192
texasflood
  • 1,571
  • 1
  • 13
  • 22
15

The other answers did not address my need for properly showing tooltips in a recent version of Jupyter inline matplotlib figure. This one works though:

import matplotlib.pyplot as plt
import numpy as np
import mplcursors
np.random.seed(42)

fig, ax = plt.subplots()
ax.scatter(*np.random.random((2, 26)))
ax.set_title("Mouse over a point")
crs = mplcursors.cursor(ax,hover=True)

crs.connect("add", lambda sel: sel.annotation.set_text(
    'Point {},{}'.format(sel.target[0], sel.target[1])))
plt.show()

Leading to something like the following picture when going over a point with mouse: enter image description here

Farzad Vertigo
  • 2,458
  • 1
  • 29
  • 32
  • 7
    The source for this (unattributed) is https://mplcursors.readthedocs.io/en/stable/examples/hover.html – Victoria Stuart Jun 19 '19 at 18:41
  • 2
    I couldn't get this working in jupyter lab. Does it perhaps work in a jupyter notebook but not in jupyter lab? – MD004 Jan 23 '20 at 19:19
  • Hmm... not sure it's a big deal not attributing code snippets from a libraries documentation. – Att Righ Nov 12 '20 at 18:41
  • 1
    @MD004 See https://stackoverflow.com/questions/50149562/jupyterlab-interactive-plot for jupyterlab - you can add "%matplotlib widget" to make this work. – Att Righ Nov 12 '20 at 18:42
  • Is it possible to show other data then X and Y coordinate? For example a full node name. – Natan Oct 23 '22 at 16:49
6

mpld3 solves it for me.

import matplotlib.pyplot as plt
import numpy as np
import mpld3

fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))
N = 100

scatter = ax.scatter(np.random.normal(size=N),
                 np.random.normal(size=N),
                 c=np.random.random(size=N),
                 s=1000 * np.random.random(size=N),
                 alpha=0.3,
                 cmap=plt.cm.jet)
ax.grid(color='white', linestyle='solid')

ax.set_title("Scatter Plot (with tooltips!)", size=20)

labels = ['point {0}'.format(i + 1) for i in range(N)]
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.show()

You can check this example: https://mpld3.github.io/examples/scatter_tooltip.html

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Julian
  • 2,490
  • 24
  • 20
  • Please include sample code and do not just link to external sources with no context or information. See [the Help Center](https://stackoverflow.com/help/how-to-answer) for more information. – Joseph Farah Jun 09 '17 at 02:25
  • 5
    unfortunately mpld3 is no longer being actively maintained as of July 2017 – Ben Lindsay Jul 25 '17 at 02:27
  • Code sample fails with a `TypeError: array([1.]) is not JSON serializable`. – P-Gn Feb 16 '18 at 09:31
  • @P-Gn just follow the trick here https://stackoverflow.com/questions/48015030/mpld3-with-python-error MPLD3 is a simple solution for this and once the above answer is followed, it works. – Zalakain Apr 24 '19 at 15:05
  • 1
    @Zalakain Unfortunately, [mpl3d seems to be abandonned](https://github.com/mpld3/mpld3/commits/master). – P-Gn May 02 '19 at 12:00
  • @P-Gn you're right I'm afraid. Stil lthe code can be run, fortunately. – Zalakain May 03 '19 at 11:30
6

mplcursors worked for me. mplcursors provides clickable annotation for matplotlib. It is heavily inspired from mpldatacursor (https://github.com/joferkington/mpldatacursor), with a much simplified API

import matplotlib.pyplot as plt
import numpy as np
import mplcursors

data = np.outer(range(10), range(1, 5))

fig, ax = plt.subplots()
lines = ax.plot(data)
ax.set_title("Click somewhere on a line.\nRight-click to deselect.\n"
             "Annotations can be dragged.")

mplcursors.cursor(lines) # or just mplcursors.cursor()

plt.show()
Enayat
  • 3,904
  • 1
  • 33
  • 47
  • I use this myself, by far the easiest solution for someone in a hurry. I just plotted 70 labels and `matplotlib` makes every 10th line the same colour, such a pain. `mplcursors` sorts it out though. – ajsp May 06 '19 at 11:04
2

I have made a multi-line annotation system to add to: https://stackoverflow.com/a/47166787/10302020. for the most up to date version: https://github.com/AidenBurgess/MultiAnnotationLineGraph

Simply change the data in the bottom section.

import matplotlib.pyplot as plt


def update_annot(ind, line, annot, ydata):
    x, y = line.get_data()
    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    # Get x and y values, then format them to be displayed
    x_values = " ".join(list(map(str, ind["ind"])))
    y_values = " ".join(str(ydata[n]) for n in ind["ind"])
    text = "{}, {}".format(x_values, y_values)
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event, line_info):
    line, annot, ydata = line_info
    vis = annot.get_visible()
    if event.inaxes == ax:
        # Draw annotations if cursor in right position
        cont, ind = line.contains(event)
        if cont:
            update_annot(ind, line, annot, ydata)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            # Don't draw annotations
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()


def plot_line(x, y):
    line, = plt.plot(x, y, marker="o")
    # Annotation style may be changed here
    annot = ax.annotate("", xy=(0, 0), xytext=(-20, 20), textcoords="offset points",
                        bbox=dict(boxstyle="round", fc="w"),
                        arrowprops=dict(arrowstyle="->"))
    annot.set_visible(False)
    line_info = [line, annot, y]
    fig.canvas.mpl_connect("motion_notify_event",
                           lambda event: hover(event, line_info))


# Your data values to plot
x1 = range(21)
y1 = range(0, 21)
x2 = range(21)
y2 = range(0, 42, 2)
# Plot line graphs
fig, ax = plt.subplots()
plot_line(x1, y1)
plot_line(x2, y2)
plt.show()
Bobs
  • 43
  • 6
  • i had a single line plot and i wanted tool tip to show nearest plotted point when i hover anywhere in the graph . so i removed x2,y2 plot . this is very useful . I have one issue though , when my mouse is in region between 2 points then i see tool tip showing both points . in the order x1 x2 y1 y2 . why does that happen ? – user1371666 Aug 18 '23 at 10:41
2

showing object information in matplotlib statusbar

enter image description here

Features

  • no extra libraries needed
  • clean plot
  • no overlap of labels and artists
  • supports multi artist labeling
  • can handle artists from different plotting calls (like scatter, plot, add_patch)
  • code in library style

Code

### imports
import matplotlib as mpl
import matplotlib.pylab as plt
import numpy as np


# https://stackoverflow.com/a/47166787/7128154
# https://matplotlib.org/3.3.3/api/collections_api.html#matplotlib.collections.PathCollection
# https://matplotlib.org/3.3.3/api/path_api.html#matplotlib.path.Path
# https://stackoverflow.com/questions/15876011/add-information-to-matplotlib-navigation-toolbar-status-bar
# https://stackoverflow.com/questions/36730261/matplotlib-path-contains-point
# https://stackoverflow.com/a/36335048/7128154
class StatusbarHoverManager:
    """
    Manage hover information for mpl.axes.Axes object based on appearing
    artists.

    Attributes
    ----------
    ax : mpl.axes.Axes
        subplot to show status information
    artists : list of mpl.artist.Artist
        elements on the subplot, which react to mouse over
    labels : list (list of strings) or strings
        each element on the top level corresponds to an artist.
        if the artist has items
        (i.e. second return value of contains() has key 'ind'),
        the element has to be of type list.
        otherwise the element if of type string
    cid : to reconnect motion_notify_event
    """
    def __init__(self, ax):
        assert isinstance(ax, mpl.axes.Axes)


        def hover(event):
            if event.inaxes != ax:
                return
            info = 'x={:.2f}, y={:.2f}'.format(event.xdata, event.ydata)
            ax.format_coord = lambda x, y: info
        cid = ax.figure.canvas.mpl_connect("motion_notify_event", hover)

        self.ax = ax
        self.cid = cid
        self.artists = []
        self.labels = []

    def add_artist_labels(self, artist, label):
        if isinstance(artist, list):
            assert len(artist) == 1
            artist = artist[0]

        self.artists += [artist]
        self.labels += [label]

        def hover(event):
            if event.inaxes != self.ax:
                return
            info = 'x={:.2f}, y={:.2f}'.format(event.xdata, event.ydata)
            for aa, artist in enumerate(self.artists):
                cont, dct = artist.contains(event)
                if not cont:
                    continue
                inds = dct.get('ind')
                if inds is not None:  # artist contains items
                    for ii in inds:
                        lbl = self.labels[aa][ii]
                        info += ';   artist [{:d}, {:d}]: {:}'.format(
                            aa, ii, lbl)
                else:
                    lbl = self.labels[aa]
                    info += ';   artist [{:d}]: {:}'.format(aa, lbl)
            self.ax.format_coord = lambda x, y: info

        self.ax.figure.canvas.mpl_disconnect(self.cid)
        self.cid = self.ax.figure.canvas.mpl_connect(
            "motion_notify_event", hover)



def demo_StatusbarHoverManager():
    fig, ax = plt.subplots()
    shm = StatusbarHoverManager(ax)

    poly = mpl.patches.Polygon(
        [[0,0], [3, 5], [5, 4], [6,1]], closed=True, color='green', zorder=0)
    artist = ax.add_patch(poly)
    shm.add_artist_labels(artist, 'polygon')

    artist = ax.scatter([2.5, 1, 2, 3], [6, 1, 1, 7], c='blue', s=10**2)
    lbls = ['point ' + str(ii) for ii in range(4)]
    shm.add_artist_labels(artist, lbls)

    artist = ax.plot(
        [0, 0, 1, 5, 3], [0, 1, 1, 0, 2], marker='o', color='red')
    lbls = ['segment ' + str(ii) for ii in range(5)]
    shm.add_artist_labels(artist, lbls)

    plt.show()


# --- main
if __name__== "__main__":
    demo_StatusbarHoverManager()
Markus Dutschke
  • 9,341
  • 4
  • 63
  • 58
1

Based off Markus Dutschke" and "ImportanceOfBeingErnest", I (imo) simplified the code and made it more modular.

Also this doesn't require additional packages to be installed.

import matplotlib.pylab as plt
import numpy as np

plt.close('all')
fh, ax = plt.subplots()

#Generate some data
y,x = np.histogram(np.random.randn(10000), bins=500)
x = x[:-1]
colors = ['#0000ff', '#00ff00','#ff0000']
x2, y2 = x,y/10
x3, y3 = x, np.random.randn(500)*10+40

#Plot
h1 = ax.plot(x, y, color=colors[0])
h2 = ax.plot(x2, y2, color=colors[1])
h3 = ax.scatter(x3, y3, color=colors[2], s=1)

artists = h1 + h2 + [h3] #concatenating lists
labels = [list('ABCDE'*100),list('FGHIJ'*100),list('klmno'*100)] #define labels shown

#___ Initialize annotation arrow
annot = ax.annotate("", xy=(0,0), xytext=(20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def on_plot_hover(event):
    if event.inaxes != ax: #exit if mouse is not on figure
        return
    is_vis = annot.get_visible() #check if an annotation is visible
    # x,y = event.xdata,event.ydata #coordinates of mouse in graph
    for ii, artist in enumerate(artists):
        is_contained, dct = artist.contains(event)

        if(is_contained):
            if('get_data' in dir(artist)): #for plot
                data = list(zip(*artist.get_data()))
            elif('get_offsets' in dir(artist)): #for scatter
                data = artist.get_offsets().data

            inds = dct['ind'] #get which data-index is under the mouse
            #___ Set Annotation settings
            xy = data[inds[0]] #get 1st position only
            annot.xy = xy
            annot.set_text(f'pos={xy},text={labels[ii][inds[0]]}')
            annot.get_bbox_patch().set_edgecolor(colors[ii])
            annot.get_bbox_patch().set_alpha(0.7)
            annot.set_visible(True)
            fh.canvas.draw_idle()
        else:
             if is_vis:
                 annot.set_visible(False) #disable when not hovering
                 fh.canvas.draw_idle()

fh.canvas.mpl_connect('motion_notify_event', on_plot_hover)

Giving the following result: Plotting 2 gaussians and 1 scatter

0

I have adapted ImportanceOfBeingErnest's answer to work with patches and classes. Features:

  • The entire framework is contained inside of a single class, so all of the used variables are only available within their relevant scopes.
  • Can create multiple distinct sets of patches
  • Hovering over a patch prints patch collection name and patch subname
  • Hovering over a patch highlights all patches of that collection by changing their edge color to black

Patches solution example

Note: For my applications, the overlap is not relevant, thus only one object's name is displayed at a time. Feel free to extend to multiple objects if you wish, it is not too hard.

Usage

fig, ax = plt.subplots(tight_layout=True)

ap = annotated_patches(fig, ax)
ap.add_patches('Azure', 'circle', 'blue', np.random.uniform(0, 1, (4,2)), 'ABCD', 0.1)
ap.add_patches('Lava', 'rect', 'red', np.random.uniform(0, 1, (3,2)), 'EFG', 0.1, 0.05)
ap.add_patches('Emerald', 'rect', 'green', np.random.uniform(0, 1, (3,2)), 'HIJ', 0.05, 0.1)

plt.axis('equal')
plt.axis('off')

plt.show()

Implementation

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.collections import PatchCollection

np.random.seed(1)


class annotated_patches:
    def __init__(self, fig, ax):
        self.fig = fig
        self.ax = ax

        self.annot = self.ax.annotate("", xy=(0,0),
                            xytext=(20,20),
                            textcoords="offset points",
                            bbox=dict(boxstyle="round", fc="w"),
                            arrowprops=dict(arrowstyle="->"))
        
        self.annot.set_visible(False)
        
        self.collectionsDict = {}
        self.coordsDict = {}
        self.namesDict = {}
        self.isActiveDict = {}

        self.motionCallbackID = self.fig.canvas.mpl_connect("motion_notify_event", self.hover)

    def add_patches(self, groupName, kind, color, xyCoords, names, *params):
        if kind=='circle':
            circles = [mpatches.Circle(xy, *params, ec="none") for xy in xyCoords]
            thisCollection = PatchCollection(circles, facecolor=color, alpha=0.5, edgecolor=None)
            ax.add_collection(thisCollection)
        elif kind == 'rect':
            rectangles = [mpatches.Rectangle(xy, *params, ec="none") for xy in xyCoords] 
            thisCollection = PatchCollection(rectangles, facecolor=color, alpha=0.5, edgecolor=None)
            ax.add_collection(thisCollection)
        else:
            raise ValueError('Unexpected kind', kind)
            
        self.collectionsDict[groupName] = thisCollection
        self.coordsDict[groupName] = xyCoords
        self.namesDict[groupName] = names
        self.isActiveDict[groupName] = False
        
    def update_annot(self, groupName, patchIdxs):
        self.annot.xy = self.coordsDict[groupName][patchIdxs[0]]
        self.annot.set_text(groupName + ': ' + self.namesDict[groupName][patchIdxs[0]])
        
        # Set edge color
        self.collectionsDict[groupName].set_edgecolor('black')
        self.isActiveDict[groupName] = True

    def hover(self, event):
        vis = self.annot.get_visible()
        updatedAny = False
        if event.inaxes == self.ax:            
            for groupName, collection in self.collectionsDict.items():
                cont, ind = collection.contains(event)
                if cont:
                    self.update_annot(groupName, ind["ind"])
                    self.annot.set_visible(True)
                    self.fig.canvas.draw_idle()
                    updatedAny = True
                else:
                    if self.isActiveDict[groupName]:
                        collection.set_edgecolor(None)
                        self.isActiveDict[groupName] = True
                    
            if (not updatedAny) and vis:
                self.annot.set_visible(False)
                self.fig.canvas.draw_idle()
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Aleksejs Fomins
  • 688
  • 1
  • 8
  • 20