Coupling Sliders in GLMakie

I would like to be able to “couple” two sliders of a Makie.SliderGrid so that their value always adds to 1. That is, if I move the first slider I would like to Makie.set_close_to!(slider2, 1-slider1.value) and vice versa. However, I cannot figure out a way how to make this work. All of the ways that I can think of would introduce circular dependencies of Observables.

Edit: I should also say that I am looking for a solution that generalizes to n > 2 sliders.

Here’s a sub of an example which has everything except for the coupling of the sliders

using GLMakie: GLMakie
using Makie: Makie

figure = Makie.Figure()

# create two sliders
slider1, slider2 =
    Makie.SliderGrid(
        figure[2, 1],
        (; label = "Slider 1", range = -1:0.01:1),
        (; label = "Slider 2", range = -1:0.01:1),
    ).sliders

# create a slimple scatter point based on the slider (just a dummy for the purpose of this example)
data = Makie.@lift [Makie.Point2f($(slider1.value), $(slider2.value))]
Makie.scatter(figure[1, 1], data)

display(figure)
1 Like

Mathematically, with some equations. How would you solve it for n=3?

I think you need to make an observable with a tuple of the n values. Then for each slider value observable, make it so changing the value computes the n new values for the tuple and set that tuple. When that observable triggers, set all the sliders to the new values. This would be cyclical normally. But you can set it up so that the slider observable functions only change the tuple value when a boolean state variable is false. So when the tuple observable function triggers, set this bool or these bools to true first. The sliders will be updated but the tuple observable won’t fire again.

The values you need downstream you’ll take from the single tuple observable, not each slider.

1 Like

Essentially, you have the solution, and the only thing left is to break the circularity. This can be done by triggering a change only when slider state is out of the ‘good’ set of states. And to make sure the changes bring you back into the ‘good’ set of states (otherwise a “doom loop” can occur, like you suggested).

So for the example in the post:

using Observables

obs_func = on(slider1.value) do val
    abs(val+slider2.value[])>0.01 && Makie.set_close_to!(slider2, -val)
end

obs_func2 = on(slider2.value) do val
    abs(slider1.value[]+val)>0.01 && Makie.set_close_to!(slider1, -val)
end

The code makes sure slider1 and slider2 sum to zero.
When I ran it, it worked quite smoothly.

2 Likes

Nice. The last solution proposed by @Dan seems to be exactly what I want. This also avoids adding an extra boolean state for breaking the circular dependency (though that would also be fine in my case).

That is a good point. I have to think about what kind of “projection logic” I would want to have in that case. In worst case, I can always solve an LP but I’ll figure that out offline.

Thank you everyone!