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.