25

I have a 2-d scatter plot of points, that correspond to images. I was wondering if there's an easy way to display the corresponding image (as a popup or tooltip) when you hover your mouse over each point? I tried plotly but found out you need to manually edit javascript to get the hover event to work. Is there a simple solution just with matplotlib or some other common package?

Alex R.
  • 1,397
  • 3
  • 18
  • 33
  • Possible duplicate of [Possible to make labels appear when hovering over a point in matplotlib?](http://stackoverflow.com/questions/7908636/possible-to-make-labels-appear-when-hovering-over-a-point-in-matplotlib) – fuglede Mar 17 '17 at 22:03
  • 1
    @fuglede: It looks like that solution involves clicking, whereas I'd prefer hovering. Unfortunately the second answer with hovering does not seem to work in Jupyter – Alex R. Mar 17 '17 at 22:12
  • I guess you need the `motion_notify_event`: http://matplotlib.org/users/event_handling.html – Peter Wood Mar 17 '17 at 22:32
  • The first solution is about clicking, but the other one addresses hovering. – fuglede Mar 18 '17 at 12:45

2 Answers2

42

Find here a complete solution on how to display an image on hover events. It uses a 'motion_notify_event' to detect when the mouse is over a scatter point (hovering). If this is the case, it displays an image annotation with a corresponding image next to the hovered scatter point.

import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)

# Generate data x, y for scatter and an array of images.
x = np.arange(20)
y = np.random.rand(len(x))
arr = np.empty((len(x),10,10))
for i in range(len(x)):
    f = np.random.rand(5,5)
    arr[i, 0:5,0:5] = f
    arr[i, 5:,0:5] =np.flipud(f)
    arr[i, 5:,5:] =np.fliplr(np.flipud(f))
    arr[i, 0:5:,5:] = np.fliplr(f)

# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
line, = ax.plot(x,y, ls="", marker="o")

# create the annotations box
im = OffsetImage(arr[0,:,:], zoom=5)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
        boxcoords="offset points",  pad=0.3,  arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)

def hover(event):
    # if the mouse is over the scatter points
    if line.contains(event)[0]:
        # find out the index within the array from the event
        ind, = line.contains(event)[1]["ind"]
        # get the figure size
        w,h = fig.get_size_inches()*fig.dpi
        ws = (event.x > w/2.)*-1 + (event.x <= w/2.) 
        hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
        # if event occurs in the top or right quadrant of the figure,
        # change the annotation box position relative to mouse.
        ab.xybox = (xybox[0]*ws, xybox[1]*hs)
        # make annotation box visible
        ab.set_visible(True)
        # place it at the position of the hovered scatter point
        ab.xy =(x[ind], y[ind])
        # set the image corresponding to that point
        im.set_data(arr[ind,:,:])
    else:
        #if the mouse is not over a scatter point
        ab.set_visible(False)
    fig.canvas.draw_idle()

# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)           
plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • 7
    In a jupyter notebook it's of course best to enable the `%matplotlib notebook` backend. Otherwise no interaction is possible. – ImportanceOfBeingErnest Mar 20 '17 at 18:32
  • is it possible to do this same thing but with different data groups. Example, say there are 3 categories, you need to do a for loop ie here https://matplotlib.org/3.1.0/gallery/lines_bars_and_markers/scatter_with_legend.html. Can each x,y data point still display a representative image? Thanks – Robin White Jun 20 '20 at 19:00
  • Ok sorry, I hacked this by making these data points transparent with alpha 0 and overlaying the colored plot. Please let me know if there is a more correct method – Robin White Jun 20 '20 at 19:20
  • 1
    This works fine as a standalone script, but does not work on Colab (with either `%matplotlib inline` or `%matplotlib notebook`): No images appear – sh37211 Mar 27 '21 at 02:20
  • If like me your points are cluttered and you often get warnings because the mouse goes over two points at the same time, fix it by using `ind = line.contains(event)[1]["ind"][0]` instead of `ind, = line.contains(event)[1]["ind"]` – P. Camilleri Feb 01 '23 at 16:05
3

If you want your images to be displayed in RGB, you have to slightly adjust the code. For this example the images need to be at your disk. Instead of using an 3darray for the images where the first dimension only represents the index you need to read the image with the plt.imread and then set_data to the corresponding position in your array containing the image names.

import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)
import os

os.chdir('Path/to/your/images')

# Generate data x, y for scatter and an array of images.
x = np.arange(3)
y = np.random.rand(len(x))
jpg_name_np = np.array(['904646.jpg', '903825.jpg', '905722.jpg']).astype('<U12') # names of your images files

cmap = plt.cm.RdYlGn

# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
#line, = ax.plot(x,y, ls="", marker="o")
line = plt.scatter(x,y,c=heat, s=10, cmap=cmap)
image_path = np.asarray(jpg_name_np)

# create the annotations box
image = plt.imread(image_path[0])
im = OffsetImage(image, zoom=0.1)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
        boxcoords="offset points",  pad=0.3,  arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)

def hover(event):
    # if the mouse is over the scatter points
    if line.contains(event)[0]:
        # find out the index within the array from the event
        ind, = line.contains(event)[1]["ind"]
        # get the figure size
        w,h = fig.get_size_inches()*fig.dpi
        ws = (event.x > w/2.)*-1 + (event.x <= w/2.) 
        hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
        # if event occurs in the top or right quadrant of the figure,
        # change the annotation box position relative to mouse.
        ab.xybox = (xybox[0]*ws, xybox[1]*hs)
        # make annotation box visible
        ab.set_visible(True)
        # place it at the position of the hovered scatter point
        ab.xy =(x[ind], y[ind])
        # set the image corresponding to that point
        im.set_data(plt.imread(image_path[ind]))
    else:
        #if the mouse is not over a scatter point
        ab.set_visible(False)
    fig.canvas.draw_idle()

# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)  

fig = plt.gcf()
fig.set_size_inches(10.5, 9.5)

plt.show()

Plot when hovering over scatterpoint

lschmidt90
  • 358
  • 3
  • 6
  • 1
    this doesn't work on Colab for me. (even after changing the path and pointing it toward images) – sh37211 Mar 27 '21 at 02:13