Proper Makie.jl recipe usage

This is the first time I am writing a recipe and I am a bit lost in all the options. Basically I want to have a few convenient plotting functions for a package I am working on. It calculates solar positions, which are just tuples of (datetime, zenith, azimuth). Fairly straightforward.

My current attempt is this:

module SolarPositionMakieExt

using Dates, Tables, Makie
using SolarPosition
import SolarPosition: sunpathplot, sunpathplot!

_elevation_from_zenith(ze) = 90 .- ze

function _normalize_input(data, t_col)
    if Tables.istable(data)
        colnames = Tables.columnnames(data)
        t = Tables.getcolumn(data, t_col)
        az = Tables.getcolumn(data, :azimuth)

        if :zenith in colnames
            ze = Tables.getcolumn(data, :zenith)
            el = _elevation_from_zenith(ze)
        elseif :elevation in colnames
            el = Tables.getcolumn(data, :elevation)
            ze = 90 .- el
        else
            error("Need either :zenith or :elevation in table input.")
        end
    elseif data isa Tuple && length(data) == 3
        t, ze, az = data
        el = _elevation_from_zenith(ze)
    else
        error("Data must be a Tables.jl source or (time, zenith, azimuth) tuple.")
    end
    return t, ze, el, az
end

@recipe(SunpathPlot) do scene
    Theme(
        coords = :polar,      # :polar or :cartesian
        colormap = :twilight,
        markersize = 3,
        t_col = :datetime,
    )
end

function sunpathplot(data; coords = :polar, kwargs...)
    if coords === :polar
        fig = Figure()
        ax = PolarAxis(fig[1, 1])
    else
        fig = Figure()
        ax = Axis(fig[1, 1])
    end
    sunpathplot!(ax, data; coords = coords, kwargs...)
    return fig
end

function sunpathplot!(ax, data; coords = :polar, t_col = :datetime, kwargs...)
    t, ze, el, az = _normalize_input(data, t_col)
    vals = dayofyear.(t)

    if coords === :polar
        if !(ax isa PolarAxis)
            error("Axis must be a PolarAxis for polar coordinates")
        end

        # configure polar axis for solar paths
        ax.direction = -1
        ax.theta_0 = -Ď€ / 2
        ax.rlimits = (0, 90)
        x = deg2rad.(az)
        y = ze
    else
        if !(ax isa Axis)
            error("Axis must be a regular Axis for cartesian coordinates")
        end
        x = az
        y = el
    end

    scatter!(
        ax,
        x,
        y;
        color = vals,
        colormap = get(kwargs, :colormap, :twilight),
        markersize = get(kwargs, :markersize, 3),
    )
end

# fallback method for when no axis is provided
function sunpathplot!(data; coords = :polar, kwargs...)
    ax = current_axis()
    sunpathplot!(ax, data; coords = coords, kwargs...)
end

function Makie.plot!(sp::SunpathPlot)
    t, _, el, az = _normalize_input(sp[1][], sp.theme.t_col[])
    vals = dayofyear.(t)

    # only handle cartesian coordinates in the recipe
    scatter!(
        sp,
        az,
        el;
        color = vals,
        colormap = sp.colormap[],
        markersize = sp.markersize[],
    )

    return sp
end

end # module

Which I then use like this:

"""Plot solar positions using SolarPosition.jl."""

using Dates
using DataFrames
using GLMakie
using SolarPosition

# define observer location (latitude, longitude, altitude in meters)
obs = Observer(28.6, 77.2, 0.0)

# a whole year of hourly timestamps
times = DateTime(2023):Hour(1):DateTime(2024)

# compute solar positions
positions = solar_position(obs, times)

# plot positions from NamedTuple
sunpathplot(positions, coords = :polar)

The resulting plot looks okay:

But I don’t think this is very ideal.

According to the docs, I think I should be using convert_arguments.

I also need to be able to select the right axis type (PolarAxis or regular Axis) and configure it properly in each case. There isn’t really much the user would be able to configure in this type of plot. It would be nice if I could just dispatch :polar or :cartesian for the plot type and on NamedTuple and Tables for the type of data input.

1 Like

I wonder if you really need a Makie recipe? From the first glance, a regular Julia function sunpathplot! + sunpathplot should be fine. That’s basically what you have anyway…

I guess not. But I also wanted to follow the format to learn how to use it.

convert_arguments literally converts plot input arguments to another set of arguments: basically, “if scatter is called with a::A, b::B, c::C I want it to mean exactly the same as if scatter was called with x, y, z = f(a, b, c)”. It can also choose stuff like colormap (although be careful to allow overrides), but doesn’t affect axis properties (like direction and theta_0 in your example) nor does it allow choosing the axis type.

Okay got it thanks. How would you structure this?

I think I managed to do it.

Code can be found here: SolarPosition.jl/ext/SolarPositionMakieExt.jl at main · JuliaSolarPV/SolarPosition.jl · GitHub

Just mentioning that the way you did it with current_axis() etc can easily break if you plot into different axes, it’s just not how recipes are meant to work. We are still looking for ways to make plots with axis settings and colorbars etc. possible in some form of recipe. Currently recipes are just about combining plot primitives and for everything with axes and figures you need to use normal functions.

1 Like

thanks. Thought I was missing something with the recipe implementation but that makes more sense. I’ll revisit this when it starts breaking and then I’ll implement your suggestion :smiley: