1

I have several matplolib.pyplot figures. Each has a legend, and what I want is that clicking the line on the legend hides the line in the figure. The click event handling was found here: https://matplotlib.org/examples/event_handling/legend_picking.html

This works fine when there is only one figure, but when there are more than one, it only works with the last figure. When I click on the legend of another figure, I get no exception or warning, but nothing happens.

Here is an example code which features this issue:

import matplotlib.pyplot as plt
import numpy as np

a = np.arange(0,10,1)
    b = np.arange(0,20,2)
    c = np.arange(0,5,.5)
    d = np.arange(-1,9,1)

    lined = {}

    for var1, var2 in [(a,b), (c,d)]:
        fig, ax = plt.subplots()
        line1, = ax.plot(var1, label="l1")
        line2, = ax.plot(var2, label="l2")
        leg = fig.legend([line1, line2], ["l1", "l2"])
        legl1, legl2 = leg.get_lines()
        legl1.set_picker(5)
        lined[legl1] = line1
        legl2.set_picker(5)
        lined[legl2] = line2

        def onpick(event, figu):
            legl = event.artist
            origl = lined[legl]
            vis = not origl.get_visible()
            origl.set_visible(vis)
            if vis:
                legl.set_alpha(1.0)
            else:
                legl.set_alpha(0.2)
            figu.canvas.draw()

        fig.canvas.mpl_connect('pick_event', lambda ev: onpick(ev, fig))
    plt.show()

How can I make the click event work on the first figure too?

4 Answers 4

1

The reason is given by https://docs.python-guide.org/writing/gotchas/#late-binding-closures. I must admit I do not understand it entirely myself, but it gives the trick to solve it: use a default argument.

import matplotlib.pyplot as plt
import numpy as np

a = np.arange(0,10,1)
b = np.arange(0,20,2)
c = np.arange(0,5,.5)
d = np.arange(-1,9,1)

lined = {}

for var1, var2 in [(a,b), (c,d)]:
    fig, ax = plt.subplots()
    line1, = ax.plot(var1, label="l1")
    line2, = ax.plot(var2, label="l2")
    leg = fig.legend([line1, line2], ["l1", "l2"])
    legl1, legl2 = leg.get_lines()
    legl1.set_picker(5)
    lined[legl1] = line1
    legl2.set_picker(5)
    lined[legl2] = line2

    def onpick(event, figu=fig):
        legl = event.artist
        origl = lined[legl]
        vis = not origl.get_visible()
        origl.set_visible(vis)
        if vis:
            legl.set_alpha(1.0)
        else:
            legl.set_alpha(0.2)
        figu.canvas.draw()

    fig.canvas.mpl_connect('pick_event', onpick)  # no need for a lambda
plt.show()

As mentioned, that solution is kind of hacky. Compare the seemingly equivalent

# works
    def onpick(event, figu=fig):
        (...)
        figu.canvas.draw()  # using a default arg equal to fig
    fig.canvas.mpl_connect('pick_event', onpick)

vs.

# fails as described
    def onpick(event):
        (...)
        fig.canvas.draw()  # using fig from main loop directly
    fig.canvas.mpl_connect('pick_event', onpick)
Sign up to request clarification or add additional context in comments.

1 Comment

I've added the missing explanation
1

@Leporello's answer is complete in terms of diagnosis and solution. Here is the full explanation:

The line lambda ev: onpick(ev, fig) creates a function object that references the names onpick and fig. The function gets called after the loop ends, after plt.show() runs.

The names onpick and fig are non-local, so it searches in the module namespace. By the time the listener is invoked, onpick refers to the last function created, and fig refers to the figure created in the last iteration of the loop.

Leporello's suggestion of default arguments is probably the most elegant way of doing this. It works because the entire def statement is evaluated at once. A def creates a function object with the code block inside it, and assigns references to the defaults right then and there. That means that you end up setting the callback to the correct function object, and whatever fig was inside the loop, will be what figu points to.

Any operation that binds the names onpick and fig to something in the local namesapace of the callback, or any other namespace created in the loop, will fix your problem.

Comments

0

This is a perfect application where using a class may be beneficial. It would allow to store the respective figure in an instance variable and use it in the methods of the class.

import matplotlib.pyplot as plt
import numpy as np

class MyPlot():
    def __init__(self, var1, var2):
        self.lined = {}
        self.fig, ax = plt.subplots()
        line1, = ax.plot(var1, label="l1")
        line2, = ax.plot(var2, label="l2")
        leg = self.fig.legend([line1, line2], ["l1", "l2"])
        legl1, legl2 = leg.get_lines()
        legl1.set_picker(5)
        self.lined[legl1] = line1
        legl2.set_picker(5)
        self.lined[legl2] = line2
        self.cid = self.fig.canvas.mpl_connect('pick_event', self.onpick)

    def onpick(self, event):
        legl = event.artist
        if legl in self.lined:
            origl = self.lined[legl]
            vis = not origl.get_visible()
            origl.set_visible(vis)
            if vis:
                legl.set_alpha(1.0)
            else:
                legl.set_alpha(0.2)
            self.fig.canvas.draw()


a = np.arange(0,10,1)
b = np.arange(0,20,2)
c = np.arange(0,5,.5)
d = np.arange(-1,9,1)

plots = [MyPlot(*var) for var in [(a,b), (c,d)]]

plt.show()

Comments

0

Actually, I found a solution that is a bit simpler than ImportanceOfBeingErnest's and makes more sense than Leporello's because it only defines the function onpick once, and is the same for every figure.

Since the reference to the figure is only needed for the canvas, and the canvas is found in the event, the following code works perfectly:

import matplotlib.pyplot as plt
import numpy as np

a = np.arange(0,10,1)
    b = np.arange(0,20,2)
    c = np.arange(0,5,.5)
    d = np.arange(-1,9,1)

    lined = {}
    def onpick(event):
            legl = event.artist
            origl = lined[legl]
            vis = not origl.get_visible()
            origl.set_visible(vis)
            if vis:
                legl.set_alpha(1.0)
            else:
                legl.set_alpha(0.2)
            event.canvas.draw()

    for var1, var2 in [(a,b), (c,d)]:
        fig, ax = plt.subplots()
        line1, = ax.plot(var1, label="l1")
        line2, = ax.plot(var2, label="l2")
        leg = fig.legend([line1, line2], ["l1", "l2"])
        legl1, legl2 = leg.get_lines()
        legl1.set_picker(5)
        lined[legl1] = line1
        legl2.set_picker(5)
        lined[legl2] = line2

        fig.canvas.mpl_connect('pick_event', onpick)
    plt.show()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.