2

I am writing a Python tool that needs several figures open at the same time, each one with its own widgets (sliders, for the most part). I don't need any interactions across the figures here. Each figure is independent of the other ones, with its own plot and its own sliders affecting only itself.

I can get Matplotlib sliders working fine on a single figure, but I can't get them to work on multiple figures concurrently. Only the sliders of the LAST figure to open are working. The other ones are unresponsive.

I recreated my problem with the simple code below, starting from the example in the Matplotlib.Slider doc. If I run it as-is, only the sliders for the second figure (amplitude) works. The other doesn't. If I invert the two function calls at the bottom, it's the other way around.

I've had no luck googling solutions or pointers. Any help would be much appreciated.

I'm on Python 3.9.12, btw. I can upload a requirements file if someone tries and cannot reproduce the issue. Thank you!

import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Slider
    
    
    # The parametrized function to be plotted
    def f(time, amplitude, frequency):
        return amplitude * np.sin(2 * np.pi * frequency * time)
    
    
    # Define initial parameters
    init_amplitude = 5
    init_frequency = 3
    t = np.linspace(0, 1, 1000)
    
    
    def create_first_fig():
    
        # Create the figure and the line that we will manipulate
        fig1, ax1 = plt.subplots()
        line1, = ax1.plot(t, f(t, init_amplitude, init_frequency), lw=2, color='b')
        ax1.title.set_text('First plot - interactive frequency')
        ax1.set_xlabel('Time [s]')

        # adjust the main plot to make room for the sliders
        fig1.subplots_adjust(left=0.25, bottom=0.25)

        # Make a horizontal slider to control the frequency.
        axfreq = fig1.add_axes([0.25, 0.1, 0.65, 0.03])
        freq_slider = Slider(
            ax=axfreq,
            label='Frequency [Hz]',
            valmin=0,
            valmax=30,
            valinit=init_frequency,
        )
    
        # register the update function with each slider
        freq_slider.on_changed(lambda val: update_first_fig(val, fig1, line1))
    
        plt.draw()
        plt.pause(0.1)
    
        return fig1
    
    
    # The function to be called anytime a slider's value changes
    def update_first_fig(val, fig, line):
        line.set_ydata(f(t, init_amplitude, val))
        fig.canvas.draw_idle()
        plt.pause(0.1)
    
    
    def create_second_fig():
    
        # Create the figure and the line that we will manipulate
        fig2, ax2 = plt.subplots()
        line2, = ax2.plot(t, f(t, init_amplitude, init_frequency), lw=2, color='r')
        ax2.title.set_text('Second plot - interactive amplitude')
        ax2.set_xlabel('Time [s]')
    
        # adjust the main plot to make room for the sliders
        fig2.subplots_adjust(left=0.25, bottom=0.25)
    
        # Make a vertically oriented slider to control the amplitude
        axamp = fig2.add_axes([0.1, 0.25, 0.0225, 0.63])
        amp_slider = Slider(
            ax=axamp,
            label="Amplitude",
            valmin=0,
            valmax=10,
            valinit=init_amplitude,
            orientation="vertical",
        )
    
        # register the update function with each slider
        amp_slider.on_changed(lambda val: update_second_fig(val, fig2, line2))
    
        plt.draw()
        plt.pause(0.1)
    
        return fig2
    
    
    # The function to be called anytime a slider's value changes
    def update_second_fig(val, fig, line):
        line.set_ydata(f(t, val, init_frequency))
        fig.canvas.draw_idle()
        plt.pause(0.1)

    
    figure1 = create_first_fig()
    figure2 = create_second_fig()
    
    plt.show()



I would expect the slider in both figures to work the way it does when I only open the corresponding figure. So far it's only the slider in the figure that's created last that works.

Edit in case someone else looks at this: see Yulia V's answer below. It works perfectly, including in my initial application. The site doesn't let me upvote it because I am too new on here, but it's a perfect solution to my problem. Thanks Yulia V!

fbcp
  • 33
  • 6

2 Answers2

2

You need to save the references to sliders as variables to make it work. No idea why, but this is how matplotlib works.

Specifically, in your functions, you need to have

return freq_slider, fig1
... 
return amp_slider, fig2

instead of

return fig1
... 
return fig2

and in the main script,

freq_slider, figure1 = create_first_fig()
amp_slider, figure2 = create_second_fig()

instead of

figure1 = create_first_fig()
figure2 = create_second_fig()
Yulia V
  • 3,507
  • 10
  • 31
  • 64
  • 2
    Oh wow, yes, this works! Thank you so much! I spent so much time struggling with this. Looks like it won't let me upvote your answer because I'm too new on here, but I would if I could! Thanks. – fbcp Feb 04 '23 at 21:50
  • For your information, it's not "how matplotlib works" but how Python works. Once the scope of the function is closed, everything inside it is killed because it's not referenced anywhere anymore. The reason `ax1` is not destroyed is because it's stored in `fig1`. Similarly, `line1` does not get destroyed because it's stored in `ax1.lines`. `fig1` lives because it's a variable in the main after `create_first_fig` ends. You could make the sliders an attribute of the figure if you wanted instead of returning them. – Guimoute Feb 04 '23 at 22:19
  • Thanks Guimoute. This seems to make sense. But why did it work with a single figure and with the last figure opened in my original code, then? Shouldn't the slider object of the last figure opened have been killed, according to your explanation? Thank you. – fbcp Feb 04 '23 at 22:30
  • 1
    @fbcp The sliders are not killed because they are attached to `axfreq/axamp` which are attached to `fig1/fig2`. I don't know why one of them doesn't trigger events when you have two figures but they are definitely not killed. – Guimoute Feb 04 '23 at 22:54
  • 1
    @Guimoute they are not killed, but frozen. IMHO matplotlib devs could have stored the references to the sliders inside matplotlib code so that they would not freeze - I have no idea why they didn't do this :) – Yulia V Feb 04 '23 at 23:01
  • @YuliaV - done. Now it lets me. Because I'm suddenly so senior :-). – fbcp Feb 04 '23 at 23:09
1

Just to illustrate my comment below @Yulia V's answer, it works too if we store the sliders as an attribute of the figure instead of returning them:

def create_first_fig():

    ...
    fig1._slider = freq_slider
    ...
    return fig1



def create_first_fig():

    ...
    fig2._slider = amp_slider
    ...
    return fig2

...

figure1 = create_first_fig()
figure2 = create_second_fig()
Guimoute
  • 4,407
  • 3
  • 12
  • 28
  • Yes, I can confirm that this works too, including in my original application. Thanks @Guimoute – fbcp Feb 04 '23 at 23:14