How to make a series optional in AlgebraOfGraphics.jl

I have a “wide” DataFrame where one column is a shared vector of x-values, and two different columns are separate vectors of y-values. For example,

df = let
    t = range(0, 9, length = 1000)
    mass_lab_frame = @. cos(t)^2
    mass_col_frame = @. sin(t)^2
    (; t, mass_lab_frame, mass_col_frame)
end

And I want to plot the two mass series on the same graph, with an option to turn off the plot for mass_col_frame. With Makie, it’s easily achieved

using CairoMakie
fig = Figure()
ax = Axis(fig[1,1], xlabel = "time", ylabel = "observed mass")

lines!(ax, df.t, df.mass_lab_frame, label = "Lab frame")
if should_plot_col_frame
    lines!(ax, df.t, df.mass_col_frame, label = "Collider frame")
end

axislegend(ax)

fig

where should_plot_col_frame is some boolean value set elsewhere.

How would I achieve the same effect in AlgebraOfGraphics? My current attempt is as follows:

using AlgebraOfGraphics
map_layer = mapping(
    :t => "time",
    [:mass_lab_frame, :mass_col_frame] .=> "observed mass",
    color = dims(1) => renamer(["Lab frame", "Collider frame"])
)
spec = data(df) * map_layer * visual(Lines)

draw(spec)

This achieves (almost) the same effect as directly using Makie, but I’m not sure how to make the plot of :mass_col_frame toggle-able within the framework of AoG.

I did consider making them separate layers, as

map_layer = mapping(:t => "time", :mass_lab_frame => "observed mass") +
            mapping(:t => "time", :mass_col_frame => "observed mass") * should_plot_col_frame

and defining a type-pirated version of multiplication:

Base.:*(b::Bool, l::Layer) = b ? l : zerolayer()
Base.:*(l::Layer, b::Bool) = b * l # commutative

But that has both the series with the same color, and I cannot distinguish them with labels either. (For more details on this, also see How to label multiple histograms in AlgebraOfGraphics)

With a wide dataframe and vectors you’d have to conditionally place or not place elements in these vectors to “toggle” them. I find that Julia doesn’t make this particularly pretty, you can either push! or vcat conditionally or you can splat empty vectors like ["A", (condition ? ["B"], [])...].

But you can do the two-layer approach you were trying, to make the color correct you just cannot use dims in that case. Instead, each layer needs its own categorical color value. But the direct helper makes this easier because you don’t have to add columns just for those categorical values. The code then becomes:

df = let
    t = range(0, 9, length = 1000)
    mass_lab_frame = @. cos(t)^2
    mass_col_frame = @. sin(t)^2
    (; t, mass_lab_frame, mass_col_frame)
end

mappings = mapping(:t, :mass_lab_frame => "observed mass", color = direct("Lab frame"))
if true
    mappings += mapping(:t, :mass_col_frame => "observed mass", color = direct("Collider frame"))
end

data(df) * mappings * visual(Lines) |> draw

And with if false:

The repetition that remains is :t and => "observed mass" but I don’t see a quick way around that right now, other than storing "observed mass" in a variable and pairing that.

If you wanted to stay with a wide mapping I’d probably do it like this, to keep the columns and labels together for clarity

cols_labels = [:mass_lab_frame => "Lab frame"]

if true
    push!(cols_labels, :mass_col_frame => "Collider frame")
end

map_layer = mapping(
    :t => "time",
    first.(cols_labels) .=> "observed mass",
    color = dims(1) => renamer(last.(cols_labels))
)
spec = data(df) * map_layer * visual(Lines)

draw(spec)