How to add hovering annotations to a plot
Asked Answered
S

13

249

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?

Sapir answered 26/10, 2011 at 20:38 Comment(1)
People ending up here through search might also want to check this answer, which is rather complex, but might be suitable depending on the requirements.Neopythagoreanism
N
252

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.

Neopythagoreanism answered 7/11, 2017 at 20:23 Comment(27)
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.Joyejoyful
@Joyejoyful 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).Neopythagoreanism
@Neopythagoreanism 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.Karyolysis
@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.Neopythagoreanism
Pardon, @ImportanceOfBeingErnest, you are right. Thank you. This is really good code.Saleme
For a bar plot you don't need .contains at all. You just take the information from the bar itself.Neopythagoreanism
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 of an bar graph, annotated in the middle of each bar upon hovering.Westerfield
I upvoted... though, ind is an empty dictionary in a 3d scatter plot. Any idea how to get this to work there too?Squires
@Squires 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.Neopythagoreanism
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?Meri
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 codeMeri
@Meri 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.Neopythagoreanism
@Konstantin Yes this solution will work when using %matplotlib notebook in an IPython/Jupyter notebook.Neopythagoreanism
@ImportanceOfBeingErnest: Thanks for the solution.. but the annotations disappear when zoppend into the plot and hovered.. any idea?Anorthic
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?Pair
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.Pair
@Pair (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.Neopythagoreanism
I copy-pasted the code as is and it didn't work in a jupyter notebook version 5.7.0.Yuzik
@MortezaMilani Jupyter will by default show png images, which are of course not interactive.Neopythagoreanism
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: repl.it/repls/ConsciousRewardingPublisher @NeopythagoreanismBarby
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 ifOrdonez
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?Afterword
It seems like a very slow code. Is there any way to make it faster?Binding
You can add annot.set_wrap(True) into this solution if your labels run off the edge of the plot.Krohn
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?Melon
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]]])Politick
Worked once, non-interactively, having marked jupyterlab nb as trusted, now does nothing.Border
S
76

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()
Seminal answered 14/7, 2016 at 14:59 Comment(6)
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.Midge
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?Woodwaxen
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.Irreformable
@Seminal 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?Plumbo
@NikanaReklawyks I added an answer which actually answers the question.Neopythagoreanism
+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().Drunken
N
47
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

Noodlehead answered 21/4, 2020 at 6:53 Comment(0)
S
40

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()
Stockstill answered 26/10, 2011 at 22:15 Comment(5)
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!Sapir
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?Barely
@Barely See my answerSnub
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?Blake
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 3Curvature
S
16

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

Snub answered 1/8, 2015 at 17:13 Comment(0)
R
16

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

Rodrich answered 18/1, 2019 at 15:54 Comment(5)
The source for this (unattributed) is mplcursors.readthedocs.io/en/stable/examples/hover.htmlEmlen
I couldn't get this working in jupyter lab. Does it perhaps work in a jupyter notebook but not in jupyter lab?Koffman
Hmm... not sure it's a big deal not attributing code snippets from a libraries documentation.Bryce
@Koffman See #50150062 for jupyterlab - you can add "%matplotlib widget" to make this work.Bryce
Is it possible to show other data then X and Y coordinate? For example a full node name.Rawinsonde
T
7

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

Triolet answered 5/6, 2017 at 13:22 Comment(6)
Please include sample code and do not just link to external sources with no context or information. See the Help Center for more information.Urinary
unfortunately mpld3 is no longer being actively maintained as of July 2017Hairsplitting
Code sample fails with a TypeError: array([1.]) is not JSON serializable.Plata
@Plata just follow the trick here https://mcmap.net/q/118812/-mpld3-with-python-error MPLD3 is a simple solution for this and once the above answer is followed, it works.Devon
@Devon Unfortunately, mpl3d seems to be abandonned.Plata
@Plata you're right I'm afraid. Stil lthe code can be run, fortunately.Devon
H
7

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()
Habsburg answered 4/5, 2018 at 14:37 Comment(1)
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.Sacaton
O
3

I have made a multi-line annotation system to add to: https://mcmap.net/q/115955/-how-to-add-hovering-annotations-to-a-plot. 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()
Ordnance answered 24/2, 2019 at 10:33 Comment(1)
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 ?Surefooted
D
3

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://mcmap.net/q/115955/-how-to-add-hovering-annotations-to-a-plot
# 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://mcmap.net/q/118813/-add-information-to-matplotlib-navigation-toolbar-status-bar
# https://mcmap.net/q/118814/-matplotlib-path-contains_point
# https://mcmap.net/q/118815/-how-to-regain-control-using-mpl_disconnect-to-end-custom-event_handling-in-matplotlib
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()
Dirty answered 7/1, 2021 at 0:2 Comment(0)
T
2

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

Tattletale answered 2/4, 2022 at 11:33 Comment(0)
P
1

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()
Panhellenic answered 25/10, 2022 at 15:44 Comment(0)
K
0

Yet another alternative is to use Plotly which is really intuitive and easy to use and has the great advantage of being able to save your plot with the hovering behavior as an html file.

plotly with hovering name result

import plotly.express as px

fig = px.scatter(
            x=[0, 1, 2, 3, 4], 
            y=[0, 1, 4, 9, 16],
            hover_name=['zero', 'one', 'foo', 'bar', 'baz']
)
fig.show()
fig.write_html('scatter_plot_with_hover.html')

Here are some intuitive examples: https://plotly.com/python/line-and-scatter/

And here is the detailed documentation: https://plotly.com/python-api-reference/generated/plotly.express.scatter.html

Kamp answered 11/10, 2023 at 8:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.