1

Good day. The minimal code provided below allows the user to click on a legend element to hide/show the associated data set. For some reason, it only works on one of the axes despite the code being highly "regular" and not treating the last ax in a different way. The first ax does not seem to pick pick_events. How to fix that?

Click the bubbles to test:

enter image description here

import numpy as np
import matplotlib.pyplot as plt

# Create dummy data.
fig = plt.gcf()
ax1 = plt.gca()
ax2 = ax1.twinx()
X = np.arange(-5, +5.01, 0.5)
Y1 = -X**2
Y2 = -0.5*X**2
ax1.scatter(X, Y1, color="red", label="1")
ax2.scatter(X, Y2, color="blue", label="2")
ax1.legend(loc="upper left")
ax2.legend(loc="upper right")
ax1.set_ybound(+5, -30)
ax2.set_ybound(+5, -30)

# Enable the pickable legend elements.
for ax in (ax1, ax2):
    for legend_item in ax.get_legend().legendHandles:
        legend_item.set_gid("1" if ax is ax1 else "2")
        legend_item.set_picker(10)

# Connect the pick event to a function.
def hide_or_show_data(event):
    """Upon clicking on a legend element, hide/show the associated data."""

    artist = event.artist
    gid = artist.get_gid()

    if gid == "1":
        scatter = ax1.collections[0]
    elif gid == "2":
        scatter = ax2.collections[0]

    scatter.set_visible(not scatter.get_visible())
    plt.draw()        

fig.canvas.mpl_connect("pick_event", hide_or_show_data)

My gut feeling is that ax1 ignores events because it's "below" ax2 if that makes any sense.

Guimoute
  • 4,407
  • 3
  • 12
  • 28

2 Answers2

3

You can create the legend for the lower axes in the upper axes. Then pick-events will only be fired in the upper axes.

import numpy as np
import matplotlib.pyplot as plt

# Create dummy data.
fig = plt.gcf()
ax1 = plt.gca()
ax2 = ax1.twinx()
X = np.arange(-5, +5.01, 0.5)
Y1 = -X**2
Y2 = -0.5*X**2
ax1.scatter(X, Y1, color="red", label="1")
ax2.scatter(X, Y2, color="blue", label="2")
ax1.set_ybound(+5, -30)
ax2.set_ybound(+5, -30)


h,l=ax1.get_legend_handles_labels()
leg1 = ax2.legend(h,l,loc="upper left")
leg2 = ax2.legend(loc="upper right")
ax2.add_artist(leg1)


# Enable the pickable legend elements.
for leg in [leg1, leg2]:
    for legend_item in leg.legendHandles:
        legend_item.set_gid("1" if leg is leg1 else "2")
        legend_item.set_picker(10)

# Connect the pick event to a function.
def hide_or_show_data(event):
    """Upon clicking on a legend element, hide/show the associated data."""

    artist = event.artist
    gid = artist.get_gid()

    if gid == "1":
        scatter = ax1.collections[0]
    elif gid == "2":
        scatter = ax2.collections[0]

    scatter.set_visible(not scatter.get_visible())
    plt.draw()        

fig.canvas.mpl_connect("pick_event", hide_or_show_data)

plt.show()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thank you for your solution! I had no idea we could have several legend objects per `ax`. – Guimoute Feb 11 '20 at 17:04
  • 2
    Once you remove it and add it (again, or to another axes), it's not retreivable via `ax.get_legend()` any more. Else, you can have as many artists as you want in an axes. – ImportanceOfBeingErnest Feb 11 '20 at 17:11
  • Yes, I guess it makes sense when you see `legend`s as regular artists. Also, if someone stumbles upon this answer later and needs it for a Qt5 application like me, add a flag to make sure that the work on legends is only done once. – Guimoute Feb 11 '20 at 17:25
  • Can we connect the pick_event to a function right after the figure is created and define pickers later without problem? I tested it and it works, but I'd like to know if this is the "prefered" way. – Guimoute Feb 12 '20 at 10:10
  • Yes, but you need to make sure the objects you work with are defined once the callback is called. – ImportanceOfBeingErnest Feb 12 '20 at 11:07
  • Hello again. As shown in this [follow-up question](https://stackoverflow.com/q/61617163/9282844), that technique fails for every drawing function except `scatter`. – Guimoute May 06 '20 at 14:03
  • 2
    Ok, so the idea of moving the entire legend is probably a bad one. I edited the answer to create the legend directly in the upper axes. This way, the pick event should always fire. – ImportanceOfBeingErnest May 07 '20 at 13:11
  • 1
    That is perfect. I tested it with 5 different drawing methods (`scatter, plot, bar, barbs, quiver`) and it works swimmingly. Thank you again! – Guimoute May 07 '20 at 13:50
  • @ImportanceOfBeingErnest Do you know why the original approach only worked for the scatter plot? Is it accidental, or should it in principle also work for other methods but doesn't for some reason? As you can see in the other post, we looked into a number of different things, and the behaviour is pretty strange. – mapf May 07 '20 at 13:55
  • 1
    @mapf Yes, I think it's accidental. Usually, an artist should only reside in the axes it was created in. – ImportanceOfBeingErnest May 07 '20 at 13:58
0

Before a real answer comes, let me share a workaround based on this question. We can merge both legends and put them in ax2 to enable picking. If you put them in ax1 it won't work.

# Create dummy data.
...

# Build a legend manually.
scatters = ax1.collections + ax2.collections # This is great if you can't save the return value of `ax.scatter(...)` because they are made somewhere else.
labels = [s.get_label() for s in scatters]
ax2.legend(scatters, labels, loc="upper left") # MUST use ax2.

# Enable the pickable legend elements.
 for n, legend_item in enumerate(ax2.get_legend().legendHandles, start=1):
        legend_item.set_gid(str(n))
        legend_item.set_picker(10) 

# Connect the pick event to a function.
...

enter image description here

Guimoute
  • 4,407
  • 3
  • 12
  • 28