Errors using Makie plots with Observable metadata Vectors of variable length

I would like to generate a Makie animation that has a variable number of points and polygons per frame, and am running into some odd behavior. I am using scatter() and poly() with Observables holding the data points, and updating the Observables in a loop. The polygons should each have custom transparency, which is held in an Observable of Vector{RGBA}, and the scatter points should each have custom markersize, held in an Observable of Vector{Float}. Because the length of the data and metadata vectors must always be consistent, I am manually updating each Observable Vector, and then only notifying them together after the new data has been loaded.

I ran into a bug where the poly() update fails if the number of objects in the list changes:

ERROR: LoadError: Metadata array needs to have same length as data.
                    Found 6 data items, and 4 metadata items

I had been working on Makie v0.17.3, and discovered that this bug seems to be corrected in v0.18.1, for which the poly plot works as expected. However, the scatter plot which had worked fine in v0.17.3 is now failing with a similar error in v0.18.1.

Is this an odd confluence of related Makie bugs? Am I setting this plot up incorrectly? Or is varying the length of metadata vectors simply not supported? Any suggested workaround or alternate approaches to accomplishing this?

Reproducible example:

using GLMakie, Makie
using Colors

polys = Observable([rand(Point2f,3) for nn=1:2])
colors = Observable(rand(RGBA,2))
pts = Observable(rand(Point2f,2))
sz = Observable(rand(2))
update = Observable(true)

on(update) do _
    NN = rand(1:10)      #only get an error if number of objects is changed...
    
    #update plot data, but dont notify Plots until all Observables are updated
    polys.val = [rand(Point2f,3) for nn=1:NN]
    colors.val = rand(RGBA,NN)
    pts.val = rand(Point2f,NN)
    sz.val = 10 .* rand(NN)
    
    notify(colors)
    notify(polys)
    notify(pts)
    notify(sz)
end

f = Figure();
ax = Axis(f[1,1])
xlims!(ax,0,1)
ylims!(ax,0,1)

# scatter(), variable number of points, custom markersize per point
# Works on v0.17.3, breaks on v0.18.1
Makie.scatter!(ax, pts, markersize=sz) 

# poly(), variable number of polys, custom RGBA per poly
# Works on v0.18.1, breaks on v0.17.3
Makie.poly!(ax, polys, color=colors)

display(f)

for mm=1:10
    notify(update)
    sleep(.1)
end

I thought I decent workaround to this might be to delete the old plot object and generate a new one for each frame. I first tried this per the documentation:

delete!(ax, sp)
sp = scatter!(ax, pts, markersize=sz) 

But this appears to corrupt the axis itself, e.g. all axis tick labels become grey boxes:

(Edit: this rendering issue seems to be specific to GLMakie, and does not occur with CairoMakie)

I also tried manually deleting the plot from the scene, e.g.

deleteat!(ax.scene.plots, findfirst(ax.scene.plots .== sp))

But this does not actually remove the old plot until an explicit redraw (e.g. display(figure)) which is rather slow.

I’m facing a similar problem when using scatter() (on GLMakie 0.9.2) with markersize set to a Vector. Namely, if I add a new element to be plotted and a size for it and then I notify() both Observables, then I get the following error:

DimensionMismatch: arrays could not be broadcast to a common size; got a dimension with lengths 8 and 9

However, if I print the two Observables they both have 9 elements.

This is addressed in the documentation here.

Can you please clarify. The documentation says to mutate the content of one observable withoug truggering it’s listeners, but
that’s what my code does anyway. Please see the reproducer below and let me know if I’m misusing something.

using GLMakie

function my_plot()

    pnts = [Point2f(x,x*x) for x in 1:8]
    sz = [10.0+x for x in 1:8]

    positions = Observable(pnts)
    sizes = Observable(sz) 

    fig, ax, p = scatter(positions, markersize=sizes)

    on(events(ax.scene).mousebutton, priority = 2) do event
        if event.button == Mouse.left && event.action == Mouse.press
            if Keyboard.a in events(fig).keyboardstate
                # Add marker
                push!(positions[], mouseposition(ax))
                push!(sizes[], 12.345)
                show(positions)
                println()
                show(sizes)
                println()
#                When notify(sizes) is commented out the plot does not update.
#                When not in comment, the program crashes with "DimensionMismatch", even if notify(positions) is commented out.
#                notify(sizes)
                notify(positions)
                return Consume(true)
            end
        end
        return Consume(false)
    end         
                
    fig         
end    

    wait(display(my_plot()))

These two lines are already notifying since the use getindex. Try

push!(positions.val, mouseposition(ax))
push!(sizes.val, 12.345)

This does not fix it in my environment. The problem seems to be with “sizes”. The following code still crashes with DimensionMismatch:

using GLMakie

function my_plot()

    pnts = [Point2f(x,x*x) for x in 1:8]
    sz = [10.0+x for x in 1:8]

    positions = Observable(pnts)
    sizes = Observable(sz)

    fig, ax, p = scatter(positions, markersize=sizes)

    on(events(ax.scene).mousebutton, priority = 2) do event
        if event.button == Mouse.left && event.action == Mouse.press
            if Keyboard.a in events(fig).keyboardstate
                push!(positions.val, mouseposition(ax))
                push!(sizes.val, 12.345)
                notify(sizes)
                return Consume(true)
            end
        end
        return Consume(false)
    end

    fig
end

    wait(display(my_plot()))

Oh, this is actually a problem in the bezierpath code path… This works:
scatter(positions, markersize=sizes, marker=Circle)

using GLMakie

function my_plot()

    pnts = [Point2f(x, x * x) for x in 1:8]
    sz = [10.0 + x for x in 1:8]

    positions = Observable(pnts)
    sizes = Observable(sz)

    fig, ax, p = scatter(positions, markersize=sizes, marker=Circle)

    on(events(ax.scene).mousebutton, priority=2) do event
        if event.button == Mouse.left && event.action == Mouse.press
            if Keyboard.a in events(fig).keyboardstate
                mp = mouseposition(ax)
                @show mp
                push!(positions[], mp)
                push!(sizes[], 12.345)
                notify(positions)
                notify(sizes)
                return Consume(true)
            end
        end
        return Consume(false)
    end

    fig
end

wait(display(my_plot()))

Background, Circle is a primitive we render directly on the GPU, while bezierpaths circles are a bit more complicated. The default is a bezier circle.

Thank you @sdanisch. I can confirm that adding “marker=Circle” to scatter() fixes the problem in my environment.