Search code examples
pythonmatplotlibmatplotlib-widget

Link two matplotlib sliders together


I'm writing a script using matplotlib in which I have two sliders that can move the plot left and right. I want to make it so that if I move one slider, the other slider is also updated. I thought I could use the Slider.set_val(val) method for this, but as this gets me stuck in an endless loop. The function of the sliders is to stretch or compress the graphed line along the x-axis. If you run the code you will see that the one slider is more 'rough' than the other, it stretches the graph more, and the other allows the user to fine-tune. I will eventually need for people to be able to easily read off the absolute amount of stretching, which is why I want the values to be linked. I currently have the following code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import sys

fig, ax = plt.subplots()
plt.subplots_adjust(left=0.25, bottom=0.25)
t = np.arange(0.0, 1.0, 0.001)
a0 = 5
f0 = 3
s = a0*np.sin(2*np.pi*f0*t)
l, = plt.plot(t,s, lw=2, color='red')
plt.axis([0, 1, -10, 10])

axcolor = 'lightgoldenrodyellow'

d0      = 0.0
c       = 300000                
z0      = d0/c

vmin    = -300.0
vmax    = 3000.0

zmin    = -0.01
zmax    = 2

axfreq  = plt.axes([0.25, 0.1, 0.65, 0.03], axisbg=axcolor)
axz     = plt.axes([0.25, 0.15, 0.65, 0.03], axisbg=axcolor)

svlsr   = Slider(axfreq, 'VLSR', vmin, vmax, valinit=d0, valfmt=u'%1.1f')
sreds   = Slider(axz, 'z', zmin, zmax, valinit=z0, valfmt=u'%1.4f')

def update(val):
    global d0, z0
    delt = svlsr.val/c
    z = sreds.val

    if z!=0.0:
        if z != z0:
            delt = z
            svlsr.set_val(z*c) #set_val causes infinite loop??

    d0 = delt
    z0    = z
    fac = 1.0 + delt
    l.set_xdata(t*fac)
    fig.canvas.draw_idle()

svlsr.on_changed(update)
sreds.on_changed(update)

plt.show()

Solution

  • You get the infinite recursion because when you call svlsr.set_val in the update() function this notifies any observer registered on svlsr. For anyone interested the code that does that is here.

    The observer is the function you specified in the call to svlsr.on_changed and is ... update() again. So, update() will be called again, which will call set_val() again, then update() again and so on...

    Solution for simple case

    Based on the code you have, the top slider (sreds) changes the value on the bottom one (svlsr) but not vice versa. If this is the case, then the solution is relatively easy. You can have one function to deal with sreds (e.g. updatesreds()) which could be exactly the same as your current update() and a different one (e.g. updatesvlsr()) doing whatever you want when the bottom slider updates. This will work unless you want a change in svlsr to call set_val() on sreds, in which case you are back in the same situation.

    The code would look something like this (replacing line 33 onwards in your example):

    def updatereds(val):
        global d0, z0
        delt = svlsr.val/c
        z = sreds.val
    
        if z!=0.0:
            if z != z0:
                delt = z
                svlsr.set_val(z*c) #set_val causes infinite loop??
    
        d0 = delt
        z0    = z
        fac = 1.0 + delt
        l.set_xdata(t*fac)
        fig.canvas.draw_idle()
        
    def updatesvlsr(val):
       # put any code you need to execute on direct update to svlsr here
       # the only thing you can't do is set_val on sreds, otherwise again
       # you will infinitely recurse
       pass
    
    svlsr.on_changed(updatesvlsr)
    sreds.on_changed(updatereds)