Makie on(Observable) combo triggers multiple times

I made a data visualization tool with Makie where observables watch my axis.finallimits selection to update the selected indices, which then triggers the plotting. This involves two on() blocks.

The problem is that it triggers multiple times, going back and forth between the on() blocks. I have so far mitigated the issue using a trick using Dates.now() to require more than a few hundred milliseconds since the last time it entered the block.

I now looked deeper into the cause of the issue. It happens only when a function is called in the on(finallimits) block AND a plot is made in the on(selection) block. Here is a MWE:

using GLMakie, Random, Dates

GLMakie.activate!(; focus_on_show=true, title="test")
fig = Figure(size = (900,900))
ax1 = Axis(fig[1,1])
lon = randn(100_000);
lat = randn(100_000).+41;
selection = Observable(trues(100_000))
s1 = scatter!(ax1, lon,lat)
display(fig)

function lonlatkm(reflat)  # WGS84 ellipsoid, returns distance in km of 1 deg
    y_convfactor_km = (111132.954 - 559.822 * cos(2 * reflat * π/180) + 1.175 * cos(4 * reflat * π/180))/1000
    x_convfactor_km = (π*6378137 * cos(reflat * π/180) / (180 * sqrt(1 - 0.00669437999014 * sin(reflat * π/180)*sin(reflat * π/180))))/1000
    return x_convfactor_km, y_convfactor_km
end

# The o1 block, when used instead of o2, is not triggering multiple times.

# o1 = on(ax1.finallimits) do xylims
#     println("inside on(ax1.finallimits) " * string(now()))
#     lon1 = minimum(xylims)[1]
#     lon2 = maximum(xylims)[1]
#     lat1 = minimum(xylims)[2]
#     lat2 = maximum(xylims)[2]
#     ax1.limits.val = ((lon1, lon2), (lat1, lat2))
#     selection[] = (lon1 .< lon .<= lon2) .&& (lat1 .< lat .<= lat2)
# end

# o2 uses a function to make sure any arbitrary lat-lon zoom results in a square geographic area.

o2 = on(ax1.finallimits) do xylims
    println("inside on(ax1.finallimits) " * string(now()))
    x1 = minimum(xylims)[1]
    x2 = maximum(xylims)[1]
    y1 = minimum(xylims)[2]
    y2 = maximum(xylims)[2]
    mx = 0.5 * (x1 + x2)
    my = 0.5 * (y1 + y2)
    dx = (x2 - x1) * lonlatkm(my)[1]
    dy = (y2 - y1) * lonlatkm(my)[2]
    dxy = 0.25 * (dx + dy)
    lon1 = mx - dxy / lonlatkm(my)[1]
    lon2 = mx + dxy / lonlatkm(my)[1]
    lat1 = my - dxy / lonlatkm(my)[2]
    lat2 = my + dxy / lonlatkm(my)[2]
    ax1.limits.val = ((lon1, lon2), (lat1, lat2))
    selection[] = (lon1 .< lon .<= lon2) .&& (lat1 .< lat .<= lat2)
end

s = on(selection) do sel
    println("inside on(selection) " * string(now()))
    empty!(ax1)
    scatter!(ax1, lon[sel], lat[sel])
end

# the s block is not triggering multiple times if it does not contain scatter, or with o1 block instead of o2.

Results:

julia> inside on(ax1.finallimits) 2024-06-01T03:54:20.256
inside on(selection) 2024-06-01T03:54:20.416
inside on(ax1.finallimits) 2024-06-01T03:54:20.432
inside on(selection) 2024-06-01T03:54:20.432
inside on(ax1.finallimits) 2024-06-01T03:54:20.432
inside on(selection) 2024-06-01T03:54:20.432
inside on(ax1.finallimits) 2024-06-01T03:54:20.432
inside on(selection) 2024-06-01T03:54:20.432
inside on(ax1.finallimits) 2024-06-01T03:54:20.448
inside on(selection) 2024-06-01T03:54:20.448
inside on(ax1.finallimits) 2024-06-01T03:54:20.450
inside on(selection) 2024-06-01T03:54:20.450

Perhaps it is happening because also without the on(selection) plotting routine, it will plot a zoomed map of the dots, so it may be an interference issue. It may not do that when using o1 as that the axis limits are the same. Is there a way to disable the native plot update following a rectangle zoom in the axis? (without disabling the rectangle selection itself)

Perhaps not a direct fix, but perhaps adding a Observable.throttle might help?

scatter! causes a limit update so you have a loop in the logic. You could make the limits update function conditional on some boolean you set to true while you are adding the scatter. You currently cannot disable the update itself.

2 Likes

That sounds like what I was doing, executing on(ax1.finallimits) block only when some minimum amount of time passed since the last time. I tried if I could throttle the observables after creating the first plot, which could be a more elegant solution:

using Observables
throttle(0.5,ax1.finallimits)
throttle(0.5,ax1.limits)

But, I see no effect on the multiple triggering.

You are not using it correctly. throttle does not modify its argument. It returns a signal to is throttled.

on(throttle(0.5, selection)) do s
   # ...
end

In my opinion throttling is definitely less elegant than prohibiting the clear loop you have there from happening at the precise location where it happens :slight_smile: but your mileage may vary