Interactive subplots inspection with Makie

Hi,

I am using Makie for exploring the data of particle trajectories. Currently, my output looks like this:

The trajectories can be long, and I may wish to zoom-in to a specific time interval for careful inspection. Now in GLMakie, since all the axis for the line plots are linked, I can select a range on the screen and zoom-in. However, the 3D plots are not updated in synchronization. Are there any ways to improve the visualization?

I looked at IntervalSlider and Slider, but I am not sure if they are what I am looking for. Any suggestions?

Here is a link to the demo data output_1.jld2.

Source code:

using JLD2
using GLMakie
#using CairoMakie

filename = "output_1.jld2"

k = [300., 800., 3000., 5000*pi]

d = jldopen(filename)
t = d["trange"] ./ 2π
x, y, z = d["x"], d["y"], d["z"]
vx, vy, vz = d["vx"], d["vy"], d["vz"]
bx, by, bz = d["bx"], d["by"], d["bz"]
bmag = @. sqrt(bx^2 + by^2 + bz^2)
bmag_perp = @. sqrt(bx^2 + by^2)
rL = d["rL"]
jx, jy, jz = d["jx"], d["jy"], d["jz"]
jmag = @. sqrt(jx^2 + jy^2 + jz^2)

# pitch angle
μ = [
   (bx[i] * vx[i] + by[i] * vy[i] + bz[i] * vz[i]) /
   (sqrt(bx[i]^2 + by[i]^2 + bz[i]^2) *
    sqrt(vx[i]^2 + vy[i]^2 + vz[i]^2)) for i in eachindex(x)
]

invariant1st = similar(x)

for i in eachindex(bx)
   local bmag = hypot(bx[i], by[i], bz[i])
   vpar = (vx[i] * bx[i] + vy[i] * by[i] + vz[i] * bz[i]) / bmag
   vperp2 = vx[i]^2 + vy[i]^2 + vz[i]^2 - vpar^2
   invariant1st[i] = vperp2 / bmag
end

f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98),
    size = (2000, 1200), fontsize=18)

colsize!(f.layout, 1, Relative(3/5))

ga = f[1,1] = GridLayout()
gcd = f[1, 2] = GridLayout()
gc = gcd[1, 1] = GridLayout()
gd = gcd[2, 1] = GridLayout()

nrow = 7

axs = [Axis(ga[row, col]) for row in 1:nrow, col in 1:1]

ind_ = findfirst("_", filename)[1] + 1
id = parse(Int, filename[ind_:end-5])
ik_ = (id - 1) % length(k) + 1

axs[1].title = "Particle ID $id, k = $(round(k[ik_], digits=0))"

lines!(axs[1], t, μ)
lines!(axs[2], t, invariant1st)
lines!(axs[3], t, z)
lines!(axs[4], t, bx, label = L"B_x")
lines!(axs[4], t, by, label = L"B_y")
lines!(axs[4], t, bz, label = L"B_z")
lines!(axs[5], t, bmag, label = L"B_\text{total}")
lines!(axs[5], t, bmag_perp, label = L"B_\text{perp}")
lines!(axs[6], t, rL)
lines!(axs[7], t, jx, label = L"J_x")
lines!(axs[7], t, jy, label = L"J_y")
lines!(axs[7], t, jz, label = L"J_z")
lines!(axs[7], t, jmag, linestyle=:dash, color=:black, label = L"J_\text{total}")

linkxaxes!(axs...)
@views hidexdecorations!.(axs[1:end-1], grid=false, ticks=false)
xlims!(axs[end], low=0, high=round(t[end]))

Legend(ga[4,2], axs[4], nbanks=1)
Legend(ga[5,2], axs[5], nbanks=1)
Legend(ga[7,2], axs[7], nbanks=1)

ylabels = (L"\mu", L"M", "Z", L"B", L"B_\text{total}", L"r_L", L"J")

for (i, ax) in enumerate(axs)
   ax.ylabel = ylabels[i]
end
axs[end].xlabel = L"t\,\Omega_0/2\pi"

## Trajectory with B field magnitude
ax3d = Axis3(gc[1, 1], title = "Trajectory")
scatter!(ax3d, x[1], y[1], z[1], color=:tomato, markersize=10)
scatter!(ax3d, x[end], y[end], z[end], color=:purple, markersize=10)
m = lines!(ax3d, x, y, z,  linewidth=2,color = bmag, colormap=:berlin)

Colorbar(gc[1, 2], m, label = "|B|")

## Trajectory with time
ax3d = Axis3(gd[1, 1], title = "Trajectory")
scatter!(ax3d, x[1], y[1], z[1], color=:tomato, markersize=10)
scatter!(ax3d, x[end], y[end], z[end], color=:purple, markersize=10)
m = lines!(ax3d, x, y, z, color = t, colormap=:solar)

Colorbar(gd[1, 2], m, label = "time")

f

You can grab the observable ax.finallimits from any of the 2d axes and restrict the 3d axes based on that, there’s currently no inbuilt axis linking for Axis3

Oh that would be nice! Could you elaborate a bit how to modify my code? I now have these following lines added:

time_span = axs[1].finallimits

t1_ = findfirst(x -> x >= time_span[].origin[1], t)
t2_ = findlast(x -> x <= time_span[].widths[1], t)
xs = @views Observable(x[t1_:t2_])
ys = @views Observable(y[t1_:t2_])
zs = @views Observable(z[t1_:t2_])

and I can see that by selecting a range in the line plots, xs, ys, zs are updated. However, I missed the part to update the 3D plots. Should I use on() or lift() to achieve that?

I thought you just wanted something like

on(ax.finallimits) do lims
    limits!(ax3,  something_derived_from_lims)
end

Thanks for the tip! I think maybe a better way for me is to have a slider or something that I can drag to select the time range, and update all the 2D and 3D plots accordingly. I don’t understand the Observable system very well, so my current naive approach is to manually select a time range, get the new input variables and redo the plot.

Still WIP.

When I tried with IntervalSliders, I quickly went into the issue of synchronous update. This seems more difficult than I expected.