Reputation: 7344
I'm working with a stacked area plot created by pandas. The screenshots shows one such typical plot (labels are deliberatly not shown):
The relevant code producing this plot is
fig, axes = plt.subplots(nrows=2, ncols=1)
coredata = nonzero.loc[:, nonzero.columns != 'Busy'].plot.area(figsize=(9, 8), ax=axes[0], colormap='jet')
where nonzero
is a larger dataframe. The issue is that there are too many columns leading to a crowded legend. Instead of moving the legend out of the picture I'd like to use matplotlib's events to tell me which element of the chart I'm hovering over.
def on_move(event):
if event.inaxes == coredata:
# help please
fig.canvas.mpl_connect("motion_notify_event", on_move)
The event fires exactly as desired but I have trouble extracting the area I'm hovering over (respectively its label). coredata.artists
is empty, coredata.lines
is a matplotlib.lines.Line2D element (supposedly too low level). How can I access the current area under the cursor in order to display its label?
Edit: following is a minimal example:
from pandas import DataFrame, Series
from matplotlib import pyplot as plt
# mock data
d = {'one' : Series([1., 2., 3.], index=['a', 'b', 'c']),
'two' : Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd']),
'three': Series([0.5, 0.2, 0.3, 0.1], index=['a', 'b', 'c', 'd']),
'four': Series([3., 2., 1., 0.3], index=['a', 'b', 'c', 'd']),
}
df = DataFrame(d)
fig, axes = plt.subplots()
chart = df.plot.area(ax=axes)
# create and initially hide annotation
annot = axes.annotate("", xy=(0,0), xytext=(-20,20),textcoords="offset points",
bbox=dict(boxstyle="round", fc="w"))
annot.set_visible(False)
def on_move(event):
if event.inaxes == chart:
pass # help plz: how do I best check I currently hover over one, two, three or four?
print(event.xdata, event.ydata)
fig.canvas.mpl_connect("motion_notify_event", on_move)
plt.show()
Upvotes: 1
Views: 1950
Reputation: 339310
Just as when hovering a scatter, see e.g. here or here you need to check if any of the collections contains the mouseevent. To this end you would look over the collections of interest, do the check and if successful may add an identifier to a list.
which = []
for i,c in enumerate(axes.collections):
if c.contains(event)[0]:
which.append(i)
You may then use this list to draw a new legend with only the collections identified in that list. Since redrawing the canvas is expensive and may slow down the application one would try to do it as seldom as possible. While moving the mouse, the exact same result would be expected a lot of times, so we may store it and only create a new legend in case it needs to be changed.
from pandas import DataFrame, Series
from matplotlib import pyplot as plt
# mock data
d = {'one' : Series([1., 2., 3.], index=['a', 'b', 'c']),
'two' : Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd']),
'three': Series([0.5, 0.2, 0.3, 0.1], index=['a', 'b', 'c', 'd']),
'four': Series([3., 2., 1., 0.3], index=['a', 'b', 'c', 'd']),
}
df = DataFrame(d)
fig, axes = plt.subplots()
df.plot.area(ax=axes, legend=False)
# create and initially hide annotation
annot = axes.annotate("", xy=(0,0), xytext=(-20,20),textcoords="offset points",
bbox=dict(boxstyle="round", fc="w"))
annot.set_visible(False)
last = [None]
def on_move(event):
if event.inaxes == axes:
which = []
for i,c in enumerate(axes.collections):
if c.contains(event)[0]:
which.append(i)
if which != last[0]:
last[0] = which
axes.legend([axes.collections[i] for i in which],
[df.columns[i] for i in which])
fig.canvas.draw_idle()
fig.canvas.mpl_connect("motion_notify_event", on_move)
plt.show()
Upvotes: 1