Reputation: 17
My question is: how to annotate a Matplotlib Polycollection bar chart when hovering over the bars using "motion_notify_event"? I have spent the day trying to make this work but I just cant get my head round a polycollection object and how to index it. Hopefully someone can help solve this, I have spent so much time on it I would hate to give up now!
A great solution to scatter, line and bar annotations using "motion_notify_event" is found in the link below: Possible to make labels appear when hovering over a point in matplotlib?
However, the above solutions do not deal with a 'matplotlib.collections.polycollection' object. I am struggling to find ways of accessing the vertices and arrays held in this object using the code in the above solution. Is it possible to index the polycollection; use the index to access a string value in the original data; then annotate the position whilst hovering over it?
I have used a modified example of a Polycollection bar timeline from the below link which I have fused with the hover/annotate examples in the above link. How to individually label bars in Matplotlib plot?
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.collections import PolyCollection
data = [(dt.datetime(2018, 7, 17, 0, 15), dt.datetime(2018, 7, 17, 0, 30), 'sleep','nap_1'),
(dt.datetime(2018, 7, 17, 0, 30), dt.datetime(2018, 7, 17, 0, 45), 'eat', 'snack_1'),
(dt.datetime(2018, 7, 17, 0, 45), dt.datetime(2018, 7, 17, 1, 0), 'work', 'meeting_1'),
(dt.datetime(2018, 7, 17, 1, 0), dt.datetime(2018, 7, 17, 1, 30), 'sleep','nap_2'),
(dt.datetime(2018, 7, 17, 1, 15), dt.datetime(2018, 7, 17, 1, 30), 'eat', 'snack_2'),
(dt.datetime(2018, 7, 17, 1, 30), dt.datetime(2018, 7, 17, 1, 45), 'work', 'project_2')]
cats = {"sleep" : 1, "eat" : 2, "work" : 3}
colormapping = {"sleep" : "C0", "eat" : "C1", "work" : "C2"}
verts = []
colors = []
for d in data:
v = [(mdates.date2num(d[0]), cats[d[2]]-.4),
(mdates.date2num(d[0]), cats[d[2]]+.4),
(mdates.date2num(d[1]), cats[d[2]]+.4),
(mdates.date2num(d[1]), cats[d[2]]-.4),
(mdates.date2num(d[0]), cats[d[2]]-.4)]
verts.append(v)
colors.append(colormapping[d[2]])
bars = PolyCollection(verts, facecolors=colors)
fig, ax = plt.subplots()
ax.add_collection(bars)
ax.autoscale()
loc = mdates.MinuteLocator(byminute=[0,15,30,45])
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(mdates.AutoDateFormatter(loc))
ax.set_yticks([1,2,3])
ax.set_yticklabels(["sleep", "eat", "work"])
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):
# how to index the polycollection? get_offsets causes an error
pos = bars.get_offsets()[ind["ind"][0]]
# causes 'IndexError: index 5 is out of bounds for axis 0 with size 1'
annot.xy = pos
# If we can get an index, can we use it to access the original 'data' to get the
# string value at d[3] from data ('nap_1') and use it to annotate?
text = ""
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 = bars.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()
Upvotes: 0
Views: 417
Reputation: 80509
The way to get to the desired data is a bit complicated, and can be found out by digging into the debugger:
bars.get_paths()
gives a list of Path
objects (the polygons).bars.get_paths()[ind].get_extents()
gives the bounding box of the barbars.get_paths()[ind].get_extents().get_points()
gives the lower left and the upper right point of the bounding box.import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.collections import PolyCollection
data = [(dt.datetime(2018, 7, 17, 0, 15), dt.datetime(2018, 7, 17, 0, 30), 'sleep', 'nap_1'),
(dt.datetime(2018, 7, 17, 0, 30), dt.datetime(2018, 7, 17, 0, 45), 'eat', 'snack_1'),
(dt.datetime(2018, 7, 17, 0, 45), dt.datetime(2018, 7, 17, 1, 0), 'work', 'meeting_1'),
(dt.datetime(2018, 7, 17, 1, 0), dt.datetime(2018, 7, 17, 1, 30), 'sleep', 'nap_2'),
(dt.datetime(2018, 7, 17, 1, 15), dt.datetime(2018, 7, 17, 1, 30), 'eat', 'snack_2'),
(dt.datetime(2018, 7, 17, 1, 30), dt.datetime(2018, 7, 17, 1, 45), 'work', 'project_2')]
cats = {"sleep": 1, "eat": 2, "work": 3}
colormapping = {"sleep": "C0", "eat": "C1", "work": "C2"}
verts = []
colors = []
for d in data:
v = [(mdates.date2num(d[0]), cats[d[2]] - .4),
(mdates.date2num(d[0]), cats[d[2]] + .4),
(mdates.date2num(d[1]), cats[d[2]] + .4),
(mdates.date2num(d[1]), cats[d[2]] - .4),
(mdates.date2num(d[0]), cats[d[2]] - .4)]
verts.append(v)
colors.append(colormapping[d[2]])
bars = PolyCollection(verts, facecolors=colors)
fig, ax = plt.subplots()
ax.add_collection(bars)
ax.autoscale()
loc = mdates.MinuteLocator(byminute=[0, 15, 30, 45])
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(mdates.AutoDateFormatter(loc))
ax.set_yticks([1, 2, 3])
ax.set_yticklabels(["sleep", "eat", "work"])
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 = bars.get_paths()[ind].get_extents().get_points()[1]
annot.xy = pos
text = f"{data[ind][2]}: {data[ind][3]}"
annot.set_text(text)
# annot.get_bbox_patch().set_facecolor(colormapping[data[ind][2]])
annot.get_bbox_patch().set_facecolor(bars.get_facecolors()[ind])
annot.get_bbox_patch().set_alpha(0.4)
def hover(event):
vis = annot.get_visible()
if event.inaxes == ax:
cont, ind = bars.contains(event)
if cont:
update_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()
Upvotes: 1