Makie workflow to access and change attributes of objects

Hi,

This post is about plotting with Makie. I quite often set out making a figure and then later add further plots or labels. My workflow below works but seems a bit cumbersome to me and I would like to have your input on what a more Julia-like workflow would be like.

Many thanks,

Michael

# Step 1: first plot
f, ax, l1 = lines(1..10, sin; linestyle=:dashdot, color=:tomato, label="sin" )

# Set labels and title
ax.title = "my title"
ax.subtitle = "my subtitle"
ax.xlabel="x-label"
ax.ylabel="y-label"
f  # display

Question 1: I didn’t find a way to directly pass the title and label to the lines function (e.g. as keywords). Is this indeed not possible and the above code is the way to do it?

# Step 2: add an additional plot
lines!(ax, 1..10, cos; color=:blue, label="cos")

# I now would like to change attributes of the lines but I forgot to 
# catch the  plotobject.  
# I can access it via the Axis object.

ax.scene.plots                   # all plot objects in the scene of the axis
@assert ax.scene.plots[2] == l1  # this is the first plot added

l2 = ax.scene.plots[3];
l2.attributes  # list attributes
l2.color = :green  # change it from blue to green
l2.label = "cos fun"  # update label
f

Question 2: Do I need to go via ax.scene.plots? It seems complicated and I wonder whether there is a simpler way? I tried ax.content, similar to f.content (where f is a figure object) but this didn’t work.

# Step 3: add xticklabels
ax.xticks = [0, π, 2π, 3π]  # set ticks
f
ax.xtickformat = x->["0", "Ď€", "2Ď€", "3Ď€"];  # set tick labels
f

Question 3: Is this the simplest way to set custom tick labels? I was looking for an option like ax.xticklabels = ["0", "π", "2π", "3π"] but didn’t find it.

# Step 4: add legend
axislegend("Legend title", position = :rb, orientation = :horizontal)
f

# not quite what I wanted. Let's add it on the outside
Legend(f[2,1], ax, "Trig functions")

# Oh, no. I again forgot to bind the Legend objects.
# I can access them via the figure object
#
leg2 = f.content[3]
leg1 = f.content[2]
@assert ax == f.content[1]

# fix spacing
f
leg2.tellwidth = false
leg2.tellheight = true
leg2.orientation = :horizontal
f

# remove first legend
delete!(leg1)
f

Question 4 For figures, I can quite intuitively access its objects, the axis and the two legends via f.content. But for the axis object ax, I have to access
the plotting objects in a slightly convoluted way via ax.scene.plot. What is the rationale here?

Question 5 Removing the first legend via delete!(leg1) works in the sense that the legend is not displayed in the figure. But julia throws the error

ERROR: MethodError: no method matching remove_from_gridlayout!(::Nothing)
Closest candidates are:
  remove_from_gridlayout!(::GridLayoutBase.GridContent) at ~/.julia/packages/GridLayoutBase/lYdxT/src/gridlayout.jl:290
Stacktrace:
 [1] delete!(block::Legend)
   @ Makie ~/.julia/packages/Makie/Ppzqh/src/makielayout/blocks.jl:503
 [2] top-level scope
@ ... 

and f.content still lists two legend objects.

julia> f.content
3-element Vector{Any}:
 Axis (3 plots)
 Legend()
 Legend()

How do I correctly delete the first legend object?

Question 1

You can supply the axis attributes to the lines call by using the axis keyword with a NamedTuple:

f, ax, l1 = lines(1..10, sin; linestyle=:dashdot, color=:tomato, label="sin",
                    axis=(title="my title", subtitle="my subtitle", xlabel="x-label", ylabel="y-label")

Question 2

When adding another plot to the axis via lines!, you can catch that simply by using a variable assignment. l2 = lines!(ax, ...)

Question 3

In makie the ticklabels are part of the xticks axis attribute when using a tuple like (xticks, xticklabels).

Example:

f = Figure()
ax = Axis(f[1, 1], xticks=([1, 2, 3, 4, 5], ["one", "two", "three", "four", "five"]))

Cheers,

Nils

1 Like

Hi Nils,

Thank you for the quick response and answers.

For Question 2, yes, that is the simplest way to catch the object. My question was rather what to do if we forgot to do it in the first place. In other words, how to access the plot object via ax or f if I didn’t bind the lines object when plotting.

For Question 3, thank you pointing out that we can specify the labels when constructing the Axis object. That’s very helpful. My question was, however, more about how to change the labels after construction. Quite often, after seeing a plot, I would like to quickly change the labels without having to redraw everything. The command ax.xtickformat = x->["0", "π", "2π", "3π"]; allows me to do that but feels too complicated and I wonder whether there is a simpler solution.

Best

Michael

Axis wraps Scene so the way to access plots is ax.scene.plots as you said, I didn’t mirror that vector over to Axis just for convenience. Figure has .content because there’s no other place the Block objects would be stored, Scenes don’t hold Blocks, but their child scenes and plot objects.

You shouldn’t do ax.xtickformat = x->["0", "π", "2π", "3π"], this means “replace the labels of whatever tick values are currently shown with those four strings” and it will not work if the number of auto-ticks changes to something other than four, or the values change and then you’ll have incorrect ticks.

To specify tick values and tick labels at the same time do ax.ticks = (values, labels). There is no convenience function to specify only new labels for the current ticks and hardcode those tick values at the same time. You could write one if you want?

Question 5 looks like a bug to me, deleting objects has been neglected functionality so far (understandably so because people care more about creating stuff at first). Maybe open an issue on github?

Disclaimer: This relies on Makie, GridLayoutBase internals and does not work for any plot elements (like lines, scatter, ...). Tested on Makie v0.18.0. (I am sorry @jules, but I could not resist :slight_smile: )

using Makie
using GLMakie
GLMakie.activate!()

function delete!(f::Figure, blk::Makie.Block)
    # remove from scene
    idx = findfirst(s -> s === blk.blockscene, f.scene.children)
    isnothing(idx) && error("could not find block in figure's scenes")
    deleteat!(f.scene.children, idx)
    layout = blk.layoutobservables.gridcontent[]
    # remove from layout
    l_idx = findfirst(l -> l === layout, f.layout.content)
    isnothing(l_idx) && error("could not find block in figure's layout")
    prev_parent = layout.parent
    Makie.GridLayoutBase.remove_from_gridlayout!(f.layout.content[l_idx])
    layout.parent = prev_parent
    Makie.GridLayoutBase.trim!(f.layout)
    return f # trigger a redraw to remove the blockscene's drawings
end

f = Figure()
ax = Axis(f[1,1])
lines!(ax, 1:3, 1:3, label="line1")
leg = Legend(f[1,2], ax)

display(f) # layout =^= [ ax, leg ]
delete!(f, leg) # layout =^= [ ax ]

EDIT: I think the above does not work properly if you try to remove a block that is not located at the boundary of a layout. E.g. if you have a layout like (only 1 row) [ ax1, ax2, ax3 ] and you call delete!(f, ax2) then the gap between ax1 and ax3 will not be removed… Nevermind, it seems to work.