'How can I force two jupyter sliders to interact with one another (non-trivially)? Is "tag" available for handler?

I want to create two ipywidget sliders, say one with value x, the other with value 1-x. When I change one slider, the other one should be automatically changed acccordingly. I am trying to use observe for callback. I see that I might use owner and description to identify which slider was modified. But I don't think description supposed to be used for this purpose. After all, description should not need to be unique in the first place. I wonder if I am missing something here.

from ipywidgets import widgets

x=0.5
a=widgets.FloatSlider(min=0,max=1,description='a',value=x)
b=widgets.FloatSlider(min=0,max=1,description='b',value=1-x)

display(a,b)
def on_value_change(change):
    if str(change['owner']).split("'")[1]=='a':
        exec('b.value='+str(1-change['new']))
    else:
        exec('a.value='+str(1-change['new']))

a.observe(on_value_change,names='value')
b.observe(on_value_change,names='value')


Solution 1:[1]

Beginner with widgets, but I ran into the same question earlier and couldn't find a solution. I pieced together several sources and came up with something that seems to work.

Here's a model example of two sliders maintaining proportionality according to '100 = a + b', with two sliders representing a and b. :

caption = widgets.Label(value='If 100 = a + b :')
a, b = widgets.FloatSlider(description='a (=100-b)'),\
                 widgets.FloatSlider(description='b (= 100-a)')
def my_func1(a):
    # b as function of a
    return (100 - a)
def my_func2(b):
    # a as function of b
    return (100 - b)

l1 = widgets.dlink((a, 'value'), (b, 'value'),transform=my_func1)
l2 = widgets.dlink((b, 'value'), (a, 'value'),transform=my_func2)

display(caption, a, b)

To explain, as best as I understand... the key was to set up a directional link going each direction between the two sliders, and to provide a transform function for the math each direction across the sliders. i.e.,:

l1 = widgets.dlink((a, 'value'), (b, 'value'),transform=my_func1)

What that is saying is this: .dlink((a, 'value'), (b, 'value'),transform=my_func1) is like "the value of a is a variable (input) used to determine the value of b (output)" and that "the function describing b, as a function of a, is my_func1". With the links described, you just need to define the aforementioned functions. The function pertaining to direct link l1 is:

def my_func1(a):          # defining b as function of a
    return (100 - a)

Likewise (but in reverse), l2 is the 'vice versa' to l1, and my_func2 the 'vice versa' to my_func1.

I found this to work better for learning purposes, compared to the fairly common approach of utilizing a listener (a.observe or b.observe) to log details (e.g. values) about the states of the sliders into a dictionary-type parameter (change) which can be passed into the transform functions and indexing for variable assignments.

Good luck, hope that helps! More info at https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#Linking-Widgets

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mitchell Miller