Search code examples
pythonmatplotlibmplcursors

Print legend in popup for a stackplot


I'm trying to plot a big amount of curves in a stackplot with matplotlib, using python. To read the graph, I need to show legends, but if I show it with the legend method, my graph is unreadable (because of the number of legends, and their size).


I have found that mplcursors could help me to do that with a popup in the graph itself. It works with "simple" plots, but not with a stackplot.

Here is the warning message with stackplots:

/usr/lib/python3.7/site-packages/mplcursors/_pick_info.py:141: UserWarning: Pick support for PolyCollection is missing.
  warnings.warn(f"Pick support for {type(artist).__name__} is missing.")

And here is the code related to this error (it's only a proof of concept):

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


data = np.outer(range(10), range(1, 5))

timestamp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

tmp = list()
tmp.append(data[:, 0])
tmp.append(data[:, 1])
tmp.append(data[:, 2])
tmp.append(data[:, 3])
print(data)
print(tmp)

fig, ax = plt.subplots()
ax.stackplot(timestamp, tmp, labels=('curve1', 'line2', 'curvefever', 'whatever'))
ax.legend()

mplcursors.cursor()

cursor = mplcursors.cursor(hover=True)


@cursor.connect("add")
def on_add(sel):
    print(sel)
    label = sel.artist.get_label()
    sel.annotation.set(text=label)


plt.show()

Do you have an idea of how to fix that, or do you know another way to do something like that ?


Solution

  • It is not clear why mplcursors doesn't accept a stackplot. But you can replicate the behavior with more primitive matplotlib functionality:

    import matplotlib.pyplot as plt
    import numpy as np
    
    def update_annot(label, x, y):
        annot.xy = (x, y)
        annot.set_text(label)
    
    def on_hover(event):
        visible = annot.get_visible()
        is_outside_of_stackplot = True
        if event.inaxes == ax:
            for coll, label in zip(stckplt, labels):
                contained, _ = coll.contains(event)
                if contained:
                    update_annot(label, event.x, event.y)
                    annot.set_visible(True)
                    is_outside_of_stackplot = False
        if is_outside_of_stackplot and visible:
            annot.set_visible(False)
        fig.canvas.draw_idle()
    
    data = np.random.randint(1, 5, size=(4, 40))
    
    fig, ax = plt.subplots()
    labels = ('curve1', 'line2', 'curvefever', 'whatever')
    stckplt = ax.stackplot(range(data.shape[1]), data, labels=labels)
    ax.autoscale(enable=True, axis='x', tight=True)
    # ax.legend()
    
    annot = ax.annotate("", xy=(0, 0), xycoords="figure pixels",
                        xytext=(20, 20), textcoords="offset points",
                        bbox=dict(boxstyle="round", fc="yellow", alpha=0.6),
                        arrowprops=dict(arrowstyle="->"))
    annot.set_visible(False)
    plt.connect('motion_notify_event', on_hover)
    plt.show()
    

    example plot