2

Good day. This question is a follow-up of Why does legend-picking only works for `ax.twinx()` and not `ax`?.

The minimal code provided below plots two curves respectively on ax1 and ax2 = ax1.twinx(), their legend boxes are created and the bottom legend is moved to the top ax so that picker events can be used. Clicking on a legend item will hide/show the associated curve.

If ax.scatter(...) is used that works fine. If ax.plot(...) is used instead, legend picking suddenly breaks. Why? Nothing else is changed so that's quite confusing. I have tested several other plotting methods and none of them work as expected.

Here is a video of it in action: https://imgur.com/qsPYHKc.mp4

import matplotlib.pyplot as plt
import numpy as np

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

X = np.linspace(0, 2*np.pi, 100)
Y1 = X**0.5 * np.sin(X)
Y2 = -np.cos(X)

# This is a quick way to change the plotting function, simply modify n.
n = 0
function, container = [("scatter", "collections"),
                       ("plot",    "lines"),
                       ("bar",     "patches"),
                       ("barbs",   "collections"),
                       ("quiver",  "collections")][n]
getattr(ax1, function)(X, Y1, color="green", label="$Y_1$")
getattr(ax2, function)(X, Y2, color="red",   label="$Y_2$")

# Put both legends on ax2 so that pick events also work for ax1's legend.
legend1 = ax1.legend(loc="upper left")
legend2 = ax2.legend(loc="upper right")
legend1.remove()
ax2.add_artist(legend1)

for n, legend in enumerate((legend1, legend2)):
    legend_item = legend.legendHandles[0]
    legend_item.set_gid(n+1)
    legend_item.set_picker(10)

# When a legend element is picked, hide/show the associated curve.   
def on_graph_pick_event(event):

    gid = event.artist.get_gid()
    print(f"Picked Y{gid}'s legend.")

    ax = {1: ax1, 2: ax2}[gid]
    for artist in getattr(ax, container):
        artist.set_visible(not artist.get_visible())
    plt.draw()

fig.canvas.mpl_connect("pick_event", on_graph_pick_event)
Guimoute
  • 4,407
  • 3
  • 12
  • 28

1 Answers1

1

Ok, so I know this is not the answer, but the comments don't allow me to do this kind of brainstorming. I tried a couple of things, and noticed the following. When you print the axes of the legendHandles artists in your for loop, it returns None for both legends in the case of the scatter plot / PathCollection artists. However, in the case of the 'normal' plot / Line2D artists, it returns axes objects! And even more than that; even though in the terminal their representations seem to be the same (AxesSubplot(0.125,0.11;0.775x0.77)), if you check if they are == ax2, for the legendHandles artist of legend1 it returns False, while for the one of legend2, it returns True. What is happening here?

So I tried to not only remove legend1 from ax1 and add it again to ax2 but to also do the same with the legendHandles object. But it doesn't allow me to do that:

NotImplementedError: cannot remove artist

To me it looks like you found a bug, or at least inconsistent behaviour. Here is the code of what I tried so far, in case anybody else would like to play around with it further.

import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Qt5Agg')
import numpy as np

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

X = np.linspace(0, 2*np.pi, 100)
Y1 = X**0.5 * np.sin(X)
Y2 = -np.cos(X)

USE_LINES = True  # <--- set this to True or False to test both cases.
if USE_LINES:
    ax1.plot(X, Y1, color="green", label="$Y_1$")
    ax2.plot(X, Y2, color="red",   label="$Y_2$")
else:
    ax1.scatter(X, Y1, color="green", label="$Y_1$")
    ax2.scatter(X, Y2, color="red",   label="$Y_2$")

# Put both legends on ax2 so that pick events also work for ax1's legend.
legend1 = ax1.legend(loc="upper left")
legend2 = ax2.legend(loc="upper right")
legend1.remove()
ax2.add_artist(legend1)
# legend1.legendHandles[0].remove()
# ax2.add_artist(legend1.legendHandles[0])

for n, legend in enumerate((legend1, legend2)):
    legend_item = legend.legendHandles[0]
    legend_item.set_gid(n+1)
    legend_item.set_picker(10)
    print(
        f'USE_LINES = {USE_LINES}', f'legend{n+1}',
        legend_item.axes.__repr__() == legend.axes.__repr__(),
        legend_item.axes == legend.axes,
        legend_item.axes.__repr__() == ax2.__repr__(),
        legend_item.axes == ax2, type(legend_item),
    )

# When a legend element is picked, hide/show the associated curve.
def on_graph_pick_event(event):

    gid = event.artist.get_gid()
    print(f"Picked Y{gid}'s legend.")

    ax = {1: ax1, 2: ax2}[gid]
    artist = ax.lines[0] if USE_LINES else ax.collections[0]
    artist.set_visible(not artist.get_visible())
    plt.draw()

fig.canvas.mpl_connect("pick_event", on_graph_pick_event)
plt.show()
mapf
  • 1,906
  • 1
  • 14
  • 40
  • 1
    Thank you for trying! Another inconsistency if you check `legend.axes` and `legend_item.axes`: they both return `AxesSubplot(...)` when using scatters but return respectively `AxesSubplot(...)` and `None` when using plots. – Guimoute May 05 '20 at 16:39
  • 1
    Yeah, I find this super weird. I extended the print statement a little bit. Maybe this should me made into an issue for the mpl github. – mapf May 05 '20 at 17:51
  • Yep, I will have to do that eventually. The original post has been edited now to allow more functions to be tested quickly, and none of them works except `scatter`. – Guimoute May 05 '20 at 19:00