Arrows on Axis in Makie

First, I love Makie and am currently switching to it as my daily plotting library!
I often need arrow tips on the axis ends. Unfortunately, I could not find anything about it in the docs, so I used the following workaround:

using CairoMakie

function axisarrows!(ax::Axis=current_axis(); kwargs...)
    points = [
        ax.elements[:xaxis].attributes.endpoints.val[2],
        ax.elements[:yaxis].attributes.endpoints.val[2]
    ]
    directions = [Vec2f(1, 0), Vec2f(0, 1)]
    arrows!(ax.parent.scene, points, directions; kwargs...)
end

f, ax, p = lines(1:10, rand(10))
axisarrows!(arrowsize=15)
hidespines!(ax, :t, :r)
f

One downside is that the axisarrows! function always needs to be run last because it plots arrows over the whole scene. Any modification to the ticks would lead to a wrong position of the axis arrows.

So Is there a better method or a way to modify my workaround to make it more robust and flexible? Maybe if I use Observables so that the axis arrow position gets updated automatically? I`ve seen similar things in this thread:

Is it possible that Makie will natively support axis arrows in the future?

3 Likes

Yes exactly, you need to use observables so the endpoints auto update whenever the axis shifts. endpoints is already an observable, just with two points. You extract the value out of it statically but need to do it such that you make a new observable with one point. The short syntax would be @lift($(ax.elements[:xaxis].attributes.endpoints)[1]).

1 Like

Thank you very much, @jules, for your fast and helpful reply!
I changed the function so that the arrow tips always stay on the axis endpoints.

function axisarrows!(ax::Axis=current_axis(); kwargs...)
    points = lift(
            @lift($(ax.elements[:xaxis].attributes.endpoints)[2]),
            @lift($(ax.elements[:yaxis].attributes.endpoints)[2]),
        ) do px, py
        [px, py]
    end
    directions = [Vec2f(1, 0), Vec2f(0, 1)]
    arrows!(ax.parent.scene, points, directions; kwargs...)
end

Example:

f, ax, p = lines(1:10, rand(10))
hidespines!(ax, :t, :r)
axisarrows!(arrowsize=15)
f

Updating the axis labels, arrows change position:

ax.title = "title"
ax.xlabel = "x"
ax.ylabel = "y"
f

After adding a new Axis:

ax2 = Axis(f[1,2], title="title 2", xlabel="x", ylabel="y")
scatter!(ax2, 1:10, rand(10))
hidespines!(ax2, :t, :r)
axisarrows!(ax2, arrowsize=15)
f

1 Like

Nice! Note that you can do one less layer of lifting, instead of this:

points = lift(
            @lift($(ax.elements[:xaxis].attributes.endpoints)[2]),
            @lift($(ax.elements[:yaxis].attributes.endpoints)[2]),
        ) do px, py
        [px, py]
    end

I think this should work (didn’t test):

points = @lift [
            $(ax.elements[:xaxis].attributes.endpoints)[2],
            $(ax.elements[:yaxis].attributes.endpoints)[2],
        ]
3 Likes

Thanks for this tip! I tested it - it works fine and is even shorter!

1 Like

Where did you find such attributes: ax.elements[:xaxis]? I can not find xaxis in my figure.

Those are Axis internals

It seems like the endpoints can now be found with

ax.xaxis.attributes[:endpoints]