[Makie] Observables, Animations, & an annoying plot workflow

Hey folks,
I’m slowly getting used to Makie and the animations workflow. I am currently trying to make an animation of a 2D surface plot (quite similar to the use case here. I learned that my workflow was slow because I was constantly making new scenes and should use Observables. Now, I have a question on how to properly incorporate Observables into my particular workflow.

For this particular plotting workflow, I have to plot sections ( maybe 1/4 of the whole plot at a time, for example) due to how the data are arranged. For an individual plot, I may do something like this:

  ax = Axis( fig )
  for i in 1:N
    surface!( x[i,1,:,:], y[i,1,:,:], q[i,1,:,:], shading=false, 
                 colormap = cmap, colorrange = (qmin, qmax) )
  end
  Colorbar( fig[1,2], ... )

where x, y, and the quantity q are 4 dimensional, with the last two containing the interesting stuff and the first dimension being a discretization index. (in practice I can do away with index 2 if it makes the solution easier). Then I would, in theory, update the data in the surface scene to make an animation.

My question becomes: How do I adapt this workflow to use Observables when I am slicing & iterating on the data? I assume that there is some solution with @lift?

I guess in this case, the easiest way would be to have a loop like this:

# Setup
fig = Figure()
ax = Axis(fig[1, 1])
t = 1 # assuming dim 2 is time or whatever ;) 
surfaces = map(1:N) do i
    return surface!(ax, x[i,t,:,:], y[i,t,:,:], q[i,t,:,:])
end
# update with new data:
t = 2
for (i, surf) in enumerate(surfaces)
    # via setindex, one can update the nth argument in a plot
    # a bit awkward to update all 3, since `plot[1:3] = (x, y, z)`` isn't overloaded
    setindex!.(p, (x[i,t,:,:], y[i,t,:,:], q[i,t,:,:]), 1:3)
end
yield() # don't forget to yield if the updating happens in a loop, so that the render task can draw a new image.
1 Like

Thanks for the response. Which part of this would happen within the record block? And what is p in setindex!? Also we can ignore the second index (it is for higher dimensional data not used here, I should remove it). To get the “new data” I have to load the next file, update, and then plot (it is a lot of data in practice, spread across many files). Loading all of the data or plotting in situ would be nice but is infeasible unfortunately.

Edit: Also, should x, y, and q be Observables in this case? An error is thrown when I try to index them (e.g., q[i,1,:,:] gives a MethodError: no method matching getindex(::Observable{Array{Float64, 4}}, ::Int64, ::Int64, ::Colon, ::Colon). Should they stay plain arrays?

Here’s how I would do it I think, this worked for me with CairoMakie, didn’t test GLMakie now.

x = Observable(range(0, 1, length = 10))
y = Observable(range(0, 1, length = 15))
z = Observable(randn(10, 15))

f = Figure()
ax = Axis(f[1, 1])
surface!(ax, x, y, z, shading = false)

record(f, "test.mp4", 1:10) do i
    x.val = range(0, i, length = 10)
    y.val = range(0, i, length = 15)
    z[] = randn(10, 15) # you could load data from a file here as well
    reset_limits!(ax) # because I'm increasing x and y
end

The .val updates are currently a necessary evil in Makie if you need to update multiple related Observables that are used in the same plot object. Otherwise you’d potentially trigger the redraw with an invalid configuration (if you change the number of elements for example).

So .val mutates the first two Observables without triggering an update and z[] = ... then triggers the update for all three (because they’re all three arguments of surface).

1 Like

Thanks for the reply. So far I’ve gotten a solution working using @sdanisch 's suggestions:

fig :: Figure = Figure( )
ax  :: Axis = Axis( fig[1,1] )

surfaces = map(1:N)  do i
  return surface!( x[i,1,:,:], y[i,1,:,:], q[i,1,:,:] )
end

record( fig,  savename, 2:NumFiles ) do i
  # reload data stuff here
  for (j, surf) in enumerate( surfaces )
    setindex!.( surf, (x[j,1,:,:], y[j,1,:,:], q[j,1,:,:]), 1:3 )
  end
  yield()
end

And this seems decently performant (faster than Python, in any case). Just for my own benefit, I wanted to try to get your method to work, @jules. The issue I run into again is that I cannot do e.g.,
surface!(ax, x, y, z, shading = false)
because the plotting requires iterating over the observable (I need N plot commands plotting e.g., q[i,1,:,:] for i in 1:N). Is there a way to adjust this method to accommodate that constraint?

Ah yes, forgot to change p → surf…
I wouldn’t bother with observables for this use case, since it’s easier to just modify the plot object.
If you want to get @jules example working, you’d do something like this:

surface_observables = map(1:N)  do i
  obs = Observable.(( x[i,1,:,:], y[i,1,:,:], q[i,1,:,:]))
  surface!(obs...)
  return obs
end

And then update the observables in record accordingly via setindex :wink:

Thanks y’all! One last question, which is perhaps better suited for elsewhere. The reason for needing many plot commands is that the data is domain decomposed (e.g., from a parallel simulation) and each index is a chunk of the whole domain. When plotting like above with CairoMakie everything is fine, but with GLMakie there are apparent gaps between these chunks (below). Any reason or fix for this? It’s not super important, but GLMakie is somewhat faster.

Again, not super important. Thanks for y’all’s help!