Dynamic labels in matplotlib stacked area chart

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

Answers (1)

ImportanceOfBeingErnest
ImportanceOfBeingErnest

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

enter image description here

Upvotes: 1

Related Questions