I also started on an implementation, but from the transformation end. This code prototype only runs on Scenes for now, but it shouldn’t be hard at all to change it to run on an LScene or similar type. It’s a bit easier on the interaction end since inverse_transform
is also defined, and the transformation doesn’t change - only the limits of the Scene determine the aspect ratio.
# First, define the polar transformation as a Makie.PointTrans
# of the form (r, theta) -> (x, y)
trans = Makie.PointTrans{2}() do point
y, x = point[1] .* sincos(point[2])
return Point2f(x, y)
end
# Define its inverse (for interactivity)
Makie.inverse_transform(::typeof(trans)) = Makie.PointTrans{2}() do point
Point2f(hypot(point[1], point[2]), atan(point[2], point[1]))
end
# Define a method to transform boxes from input space to transformed space
function Makie.apply_transform(f::typeof(trans), r::Rect2{T}) where {T}
# TODO: once Proj4.jl is updated to PROJ 8.2, we can use
# proj_trans_bounds (https://proj.org/development/reference/functions.html#c.proj_trans_bounds)
N = 21
umin = vmin = T(Inf)
umax = vmax = T(-Inf)
xmin, ymin = minimum(r)
xmax, ymax = maximum(r)
# If ymax is 2π away from ymin, then the limits
# are a circle, meaning that we only need the max radius
# which is already known.
if (ymax - ymin) ≈ 2π
@assert xmin ≥ 0
return Rect2f(
Makie.apply_transform(f, Vec2(xmin, ymin)),
Makie.apply_transform(f, Vec2f(xmax - xmin, prevfloat(2f0π)))
)
end
for x in range(xmin, xmax; length = N)
for y in range(ymin, ymax; length = N)
u, v = Makie.apply_transform(f, Point(x, y))
umin = min(umin, u)
umax = max(umax, u)
vmin = min(vmin, v)
vmax = max(vmax, v)
end
end
return Rect(Vec2(umin, vmin), Vec2(umax-umin, vmax-vmin))
end
# Define a method which will perform the correct limit adjustments
# such that our aspect ratio is 1
# This is somewhat configurable.
function MakieLayout.adjustlimits!(scene::Scene)
asp = 1
# We transform our limits to transformed space, since we can
# operate linearly there
target = Makie.apply_transform((scene.transformation.transform_func[]), Rect2f(Makie.boundingbox(scene)))
area = scene.px_area[]
# in the simplest case, just update the final limits with the target limits
#if isnothing(asp) || width(area) == 0 || height(area) == 0
# la.finallimits[] = target
# return
#end
xlims = (left(target), right(target))
ylims = (bottom(target), top(target))
size_aspect = width(area) / height(area)
data_aspect = (xlims[2] - xlims[1]) / (ylims[2] - ylims[1])
aspect_ratio = data_aspect / size_aspect
correction_factor = asp / aspect_ratio
if correction_factor > 1
# need to go wider
# TODO: find appropriate autolimitmargins
marginsum = 0#sum(la.xautolimitmargin[])
ratios = if marginsum == 0
(0.5, 0.5)
else
(la.xautolimitmargin[] ./ marginsum)
end
xlims = Makie.MakieLayout.expandlimits(xlims, ((correction_factor - 1) .* ratios)..., identity) # don't use scale here?
elseif correction_factor < 1
# need to go taller
marginsum = 0#sum(la.yautolimitmargin[])
ratios = if marginsum == 0
(0.5, 0.5)
else
(la.yautolimitmargin[] ./ marginsum)
end
ylims = Makie.MakieLayout.expandlimits(ylims, (((1 / correction_factor) - 1) .* ratios)..., identity) # don't use scale here?
end
bbox = BBox(xlims[1], xlims[2], ylims[1], ylims[2])
Makie.update_cam!(scene, bbox)
return
end
sc = Scene(show_axis=false, camera = cam2d!)
sc.transformation.transform_func[] = trans
# draw "axis"
for r in 0:0.1:1
lp = lines!(sc, fill(r, 100), LinRange(0, 2π, 100))
translate!(lp, 0, 0, 100)
end
Makie.MakieLayout.adjustlimits!(sc)
# draw plot
rs, θs = LinRange(0, 1, 100), LinRange(0, 2π, 100)
field = [exp(r) + cos(θ) for r in rs, θ in θs]
surface!(rs, θs, field; shading = false)
which yields a nice looking picture which Discourse isn’t letting me upload.
The advantage here is that we can use the normal tick finding algorithms, and possibly once nonlinear clip is implemented we can use that to create polar axes which don’t span all of (0, 2pi).
@jules, do you mind if I use your code as a base and integrate this into it?