Updating Makie plot based on button action

I’m currently discovering interactions with on in GLMakie.

I’ve tried to make an example where

  • I can start an animation by clicking on a button
  • The animation performed as a loop
    however, it seems intermediate frames are not displayed and only the last frame shows directly.

Here is the example.

function plot_random_with_button()

    x = Observable(rand(10))
    y = Observable(rand(10))

    f = Figure()
    ax = Axis(f[1,1])
    p = plot!(x, y)

    but = Button(f[2, :], label="Animate", tellwidth=false)

    on(but.clicks, priority=1) do _
        but.label[] = "Animating"
        for _ in 1:10
            x[] = rand(10)
            y[] = rand(10)
            sleep(0.1)
        end
        but.label[] = "Animate"
        Consume(false)
    end
    return f
end

Is it due to some Consume or priority between events ?

Try @async ing the for loop so that it can be interleaved with the rendering

2 Likes

Ah yes, that seems to do the trick. That works well for a limited for-loop. Would you know how to properly kill it assuming the for-loop is replaced by a while flag-loop ? I found Stop/terminate a (sub)task started with @async - #16 by attdona but I could not scope things properly.

Make the flag something you can change from the outside so that you can break the loop whenever you want. It’s also good to use Base.errormonitor on the async task because otherwise it’ll swallow error messages

A easy way to control the lifecycle of a long running task may be to use Visor.jl.

If you don’t mind to give a try let me know if the Visor docs (should suffice the README in your case) is clear enough to get your case done.

1 Like

So my latest attempt is the following. I use two flags, one to trigger start an the other to trigger stop. Is that the kind of logic you had in mind ?
I also limited the number of runnable tasks by watching the animate button click count.

function plot_random_with_button()

    x = Observable(rand(10))
    y = Observable(rand(10))

    f = Figure()
    ax = Axis(f[1, 1])
    p = plot!(x, y)

    buta = Button(f[2, :], label="Animate", tellwidth=false)
    buts = Button(f[3, :], label="Stop", tellwidth=false)

    onflag = Observable(false)
    offflag = Observable(true)

    on(buta.clicks, priority=1) do c
        if c == 1
            onflag.val = true
            @async while onflag.val
                x[] = rand(10)
                y[] = rand(10)
                sleep(0.1)
                offflag.val == true && break
            end
            offflag.val = false
        end
        Consume(true)
    end

    on(buts.clicks, priority=2) do _
        offflag.val = true
        buta.clicks = 0
    end
    return f
end

@attdona Thanks for the tip about Visor! I think it’s a bit much for what I’m working on right now, but I have another project that could really benefit from this.

EDIT I’ve tried coupling the former solution with a slider and set_close_to! but there is something weird. Two clicks on the Stop button are required while from the code, I would have expected only one to suffice. Do you have a recommendation about this behavior ?

function plot_random_with_button()
    f = Figure()
    ax = Axis(f[1, 1])
    
    buta = Button(f[2, :], label="Animate", tellwidth=false)
    buts = Button(f[3, :], label="Stop", tellwidth=false)
    sl = Slider(f[4, :], range=range(0, 2 * pi, 50))
    
    
    x = Observable(range(0, 2 * pi, 100))
    y = lift(x, sl.value) do x, p
        cos.(x .+ p)
    end
    p = plot!(x, y)

    onflag = Observable(false)
    offflag = Observable(true)


    on(buta.clicks, priority=1) do c
        if c == 1
            onflag.val = true
            @async for val in Iterators.cycle(sl.range[])
                sleep(1 / 30)
                set_close_to!(sl, val)
                offflag.val == true && break
            end
            offflag.val = false
        end
        Consume(true)
    end

    on(buts.clicks, priority=2) do _
        offflag.val = true
        set_close_to!(sl, 0)
        buta.clicks = 0
        Consume(true)
    end
    f
end

After one click

image

After two clicks

image

You have a race condition because you set your offflag to true and immediately readjust the slider, but the async task only gets to the flag after setting the slider again itself. This logic here should be a bit more robust, async code is always more complicated to reason about than sync code:

function plot_random_with_button()
    f = Figure()
    ax = Axis(f[1, 1])
    
    buta = Button(f[2, :], label="Animate", tellwidth=false)
    buts = Button(f[3, :], label="Stop", tellwidth=false)
    sl = Slider(f[4, :], range=range(0, 2 * pi, 50))
    
    
    x = Observable(range(0, 2 * pi, 100))
    y = lift(x, sl.value) do x, p
        cos.(x .+ p)
    end
    p = plot!(x, y)

    
    taskref = Ref{Union{Nothing,Task}}(nothing)
    should_close = Ref(false)

    on(buta.clicks) do _
        if taskref[] === nothing
            taskref[] = @async begin
                for val in Iterators.cycle(sl.range[])
                    sleep(1 / 30)
                    set_close_to!(sl, val)
                    should_close[] && break
                end
                should_close[] = false
            end
        end
        Consume(true)
    end

    on(buts.clicks) do _
        if taskref[] !== nothing && !should_close[]
            should_close[] = true
            wait(taskref[])
            taskref[] = nothing
            set_close_to!(sl, 0)
        end
        Consume(true)
    end
    f
end

plot_random_with_button()