How to Add Hovering Annotations to a Plot

How to add hovering annotations to a plot

It seems none of the other answers here actually answer the question. So 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()

Sample Image

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 pltimport 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()

How to add hovering annotations with multiple curves

You could use mplcursors. Each curve can have a unique label, which is shown by default.

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

force = np.random.randn(70, 100).cumsum(axis=1)
force -= force.mean(axis=1, keepdims=True)
plt.figure(figsize=(12, 5))
for i in range(len(force)):
plt.plot(force[i], alpha=0.2, label=f'force[{i}]')
plt.margins(x=0.01)
cursor = mplcursors.cursor(hover=True)
plt.show()

mplcursors showing 70 labeled curves

If you're working with a Jupyter notebook, you might need %matplotlib nbagg or %matplotlib qt instead of %matplotlib inline to enable interactivity.

Annotate lines of a plot with matplotlib when hover with mouse

You cannot blindly copy the code used for scatter() and use it for plot()as the returned artists are completly different.

In addition, and the root cause for the error you are seeing is that plot() returns a list of artists (even when plotting a single line). I've modified you code to give something that should be close to what you wanted like so:

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
y = np.random.rand(4, 15)
x = [np.arange(15) for i in range(len(y))]
names = np.array(list("ABCD"))
fig, ax = plt.subplots()
lines = []
for i in range(len(names)):
l, = ax.plot(x[i], y[i], label=names[i])
lines.append(l)

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(line, idx):
posx, posy = [line.get_xdata()[idx], line.get_ydata()[idx]]
annot.xy = (posx, posy)
text = f'{line.get_label()}: {posx:.2f}-{posy:.2f}'
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:
for line in lines:
cont, ind = line.contains(event)
if cont:
update_annot(line, ind['ind'][0])
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()

Python and Matplotlib and Annotations with Mouse Hover

Take a look at this question and demo :

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()

How to create hover annotations on a subplot using matplotlib for BrokenBarHCollection objects?

It's a bit unclear how you create your plot and how it looks like. The approach below assigns a label to each of the little bars. A broken_barh can only have one label for the whole set, so instead of drawing ax.broken_barh(), individual rectangles are created, each with their own label. An additional possibility is to also assign individual colors to each rectangle (the example below supposes a maximum of 20 colors).

In that case, mplcursors default generates an annotation with label, x and y position of the cursor. You can change the default annotation, for example only using the label.

Note that mplcursors.cursor(..., hover=True) shows an annotation while hovering. The default only shows the annotation when clicking.

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

gene_dict = {'YFL067': 2074.5, 'YFL041': 49352.5, 'YPT1': 56193.5, 'PAU5': 99435.0, 'YFL019': 100497.0, 'SMX2': 103801.5, 'YFL015': 106649.5, 'HSP12': 107304.5, 'YFL012': 110789.5, 'AUA1': 114958.0, 'WWM1': 115252.0, 'YPI1': 152424.0, 'MIC19': 166370.5, 'YFR012': 168579.0, 'RPL29': 222135.5, 'CDC26': 225896.0, 'YMR31': 247177.5}
color_dict = { key:color for key, color in zip(gene_dict.keys(), plt.cm.tab20.colors )}

fig, ax = plt.subplots()

for y in range(10, 51, 10):
keys = np.random.choice(list(gene_dict.keys()), 5, replace=False)
width = 3000
for key in keys:
ax.add_patch(plt.Rectangle((gene_dict[key] - width / 2, y), width, 9,
color=color_dict[key], label=key))
ax.relim() # needed to update the xlim and ylim when using add_patch
ax.autoscale()

mplcursors.cursor(hover=True).connect("add", lambda sel: sel.annotation.set_text(sel.artist.get_label()))
plt.show()

hover annotations for broken_barh

Some explanation about mplcursors.cursor(...).connect("add", lambda sel: ...). lambda is a way to quickly write a short function (without name) so it can be used as a parameter to another function.
The code is equivalent to:

def update_annotation(sel):
""" update the annotation belonging to the current selected item (sel) """
# get the label of the graphical element that is selected
label = sel.artist.get_label()
# change the text of the annotation
sel.annotation.set_text(label)

# create an mplcursor object that shows an annotation while hovering
cursor = mplcursors.cursor(hover=True)
# call the function "update_annotation" each time a new element gets hovered over
cursor.connect("add", update_annotation)

Matplotlib Hover Text

The mplcursors library can be used to create custom annotations while hovering. Here is an example with a tree map:

import matplotlib.pyplot as plt
import matplotlib as mpl
import squarify
import mplcursors

sizes = [5, 20, 30, 25, 10, 12]
sumsizes = sum(sizes)
labels = ['A', 'B', 'C', 'D', 'E', 'F']

cmap = plt.cm.get_cmap('Greens')
norm = plt.Normalize(vmin=min(sizes), vmax=max(sizes))
colors = [cmap(norm(s)) for s in sizes]
squarify.plot(sizes=sizes, label=labels, color=colors)
plt.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm))

cursor = mplcursors.cursor(hover=True)
cursor.connect("add", lambda sel: sel.annotation.set_text(
f"ID:{sel.target.index} '{labels[sel.target.index]}'\nSize:{sizes[sel.target.index]} ({sizes[sel.target.index] * 100.0 / sumsizes:.1f} %)"))

plt.show()

resulting plot

How to add a hovering annotation on a bar plot with mplcursors

You could employ mplcursors as follows:

import matplotlib.pyplot as plt
import mplcursors
import numpy as np
import pandas as pd
import seaborn as sns

df = pd.DataFrame({"date": pd.date_range('20210101', periods=10),
"no_of_dogs": np.random.randint(10, 30, 10)})
fig, ax = plt.subplots(figsize=(15, 5))
sns.barplot(x="date", y="no_of_dogs", data=df, palette="husl", ax=ax)

x_dates = df['date'].dt.strftime('%Y-%m-%d')
ax.set_xticklabels(labels=x_dates)

cursor = mplcursors.cursor(hover=True)
@cursor.connect("add")
def on_add(sel):
x, y, width, height = sel.artist[sel.target.index].get_bbox().bounds
sel.annotation.set(text=f"{x_dates[round(x)]}\n{height:.0f}",
position=(0, 20), anncoords="offset points")
sel.annotation.xy = (x + width / 2, y + height)

plt.show()

example plot

Making labels appear while hovering over plot for graphs *with high y-axis values*, multiple lines and multiple axes

Essentially, your problem is that you created your annotation as belonging to the axes y1_axis. When you were hovering over a point, you were setting the position of the annotation in the data coordinate of y1_axis, regardless of whether the line was in that axes or another.

The solution is to update not only the coordinates of the annotation, but also its transform to correctly map the point to the correct coordinates in pixels.

The same is true for the background of the annotation. Since you were creating it on the bottom-most axes, the annotation was above the line in these axes, but below the lines in the other axes. The solution here is to create the annotation in the top-most axes.

(...)
# annotation should be on the top axis to avoid zorder problems
annot = fig.axes[-1].annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
bbox=dict(boxstyle="round", facecolor="#FFFFFF"),
arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

(...)
def update_annot(line, annot, ind):
posx, posy = [line.get_xdata()[ind], line.get_ydata()[ind]]
annot.xycoords = line.axes.transData # set the correct transform for that line
annot.xy = (posx, posy)
text = f'{line.get_label()}: ({posx:.2f},{posy:.2f})'
annot.set_text(text)
annot.get_bbox_patch().set_alpha(1)
(...)

Full code:

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

x_data = list(range(0,30))
y1_data_a = np.sort(np.random.rand(30))
y1_data_b = np.sort(np.random.rand(30))
y1_data_c = [0.4 for point in x_data]
y2_data_a = [point**2 for point in x_data]
y2_data_b = [point*0.5 for point in y2_data_a]
y3_data = [(10/(point+1)) for point in x_data]

# #The code works fine with this data
# x_data = list(range(0,30))
# y1_data_a = np.sort(np.random.rand(30))
# y1_data_b = np.sort(np.random.rand(30))
# y1_data_c = [0.4 for point in x_data]
# y2_data_a = np.random.rand(30)
# y2_data_b = np.sort(np.random.rand(30))
# y3_data = np.sort(np.random.rand(30))[::-1]

fig, y1_axis = plt.subplots()
fig.subplots_adjust(right=0.75)

y2_axis = y1_axis.twinx()
y3_axis = y1_axis.twinx()

def make_patch_spines_invisible(ax):
ax.set_frame_on(True)
ax.patch.set_visible(False)
for sp in ax.spines.values():
sp.set_visible(False)

y3_axis.spines["right"].set_position(("axes", 1.2))
make_patch_spines_invisible(y3_axis)
y3_axis.spines["right"].set_visible(True)

plot1, = y1_axis.plot(x_data, y1_data_a, color="#000CFF", label="Temp1 (°C)")
plot2, = y1_axis.plot(x_data, y1_data_b, color="#FF5100", label="Temp2 (°C)")
plot3, = y1_axis.plot(x_data, y1_data_c, "r--", label="Critical Temp (°C)")

plot4, = y2_axis.plot(x_data, y2_data_a, color="#000000", label="Pressure1 (atm)")
plot5, = y2_axis.plot(x_data, y2_data_b, color="#17E111", label="Pressure2 (atm)")

plot6, = y3_axis.plot(x_data, y3_data, color="#D418DE", label="Volume (m3)")

y1_axis.set_xlabel("Time (hrs)")
y1_axis.set_ylabel("Temperature (°C)")
y2_axis.set_ylabel("Pressure (atm)")
y3_axis.set_ylabel("Volume (m3)")

y3_axis.yaxis.label.set_color(plot6.get_color())

tkw = dict(size=4, width=1.5)
y1_axis.tick_params(axis='y', **tkw)
y2_axis.tick_params(axis='y', **tkw)
y3_axis.tick_params(axis='y', colors=plot6.get_color(), **tkw)
y1_axis.tick_params(axis='x', **tkw)

lines = [plot1, plot2, plot4, plot5, plot6]

plt.title("Labeling data points for plots with Multiple Axes and Lines", fontdict=None, loc='center')

# annotation should be on the top axis to avoid zorder problems
annot = fig.axes[-1].annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
bbox=dict(boxstyle="round", facecolor="#FFFFFF"),
arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)


def update_annot(line, annot, ind):
posx, posy = [line.get_xdata()[ind], line.get_ydata()[ind]]
annot.xycoords = line.axes.transData
annot.xy = (posx, posy)
text = f'{line.get_label()}: ({posx:.2f},{posy:.2f})'
annot.set_text(text)
annot.get_bbox_patch().set_alpha(1)


def hover(event):
vis = annot.get_visible()
if event.inaxes in [y1_axis, y2_axis, y3_axis]:
for line in lines:
cont, ind = line.contains(event)
if cont:
update_annot(line, annot, ind['ind'][0])
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()


Related Topics



Leave a reply



Submit