Rotating Plots in Makie

I’m interested in understanding how one can rotate a whole axis/plot in Makie, in order to produce visualizations like the one below.

This solution was posted by Simon Danisch on Slack:

using GLMakie

f, ax, pl = barplot(rand(5), bar_labels=:y, figure=(resolution=(500, 500),))
hideydecorations!(ax)
hidespines!(ax, :t, :r, :l)
hidexdecorations!(ax, ticks=false)
tightlimits!(ax)
f
img = Makie.colorbuffer(ax.scene)

f, ax, pl = scatter(rand(Point2f, 100), axis=(aspect = DataAspect(),))
impl = image!(ax, 0..1, 0..1, rotr90(img))
display(f)

rotate!(impl, -0.25pi)
translate!(impl, 0.5, 1.2, 0)
xlims!(ax, 0, 2)
ylims!(ax, 0, 2)

Although it’s a great solution, I’v got some problems with it. First of all, it does not seem to work with CairoMakie. Secondly, I’d like for the visualization to be fully svg, which I’m guessing would not be the case here, since img = Makie.colorbuffer(ax.scene) would rasterize the original scene.

Following the Julia Data Science book by @lazarusA , I tried the following:

function add_box_inset(fig; left=100, right=250, bottom=200, top=300,
bgcolor=:grey90)
 inset_box = Axis(fig, bbox=BBox(left, right, bottom, top),
 xticklabelsize=12, yticklabelsize=12,  backgroundcolor=bgcolor)

 # bring content upfront

 translate!(inset_box.scene, 0, 0, 10)
 qx = qrotation(Vec(1, 0, 0), pi / 4)
 rotate!(inset_box.scene,qr)

 elements = keys(inset_box.elements)
 filtered = filter(ele -> ele != :xaxis && ele != :yaxis, elements)
 foreach(ele -> translate!(inset_box.elements[ele], 0, 0, 9), filtered)
 return inset_box
end

But with no success. Any ideas?

2 Likes

The reason why just rotating ax.scene doesn’t work is because Axis has a lot of screen space components that are in fig.scene. If you look at ax.scene.plots you’ll see that the only plot you didn’t add yourself is a mesh plot. This is the (by default white) background. Everything else is in fig.scene.plots - the box outline, ticks, text, labels, etc. The problem is that “everything else” includes every other Axis as well.
I played around with Axis a bit and it turns out you can call it with a scene as well. So what you can do is add a pixel-space proxy scene and base your rotated Axis off of that. Then you can rotate and translate the proxy scene and ax.scene to get your rotated inset axis:

fig = Figure()
b = Axis(fig[1, 1])
scene = Scene(fig.scene)
campixel!(scene)
ax = Axis(scene, bbox = MakieLayout.BBox(-100, 100, -100, 100))

for s in (scene, ax.scene)
    Makie.rotate!(s, Vec3f(0,0,1), -pi/4)
    translate!(s, Vec3f(300, 300, 100))
end
fig

This works for me in GLMakie but CairoMakie still has two problems - text gets transformed differently and disappears, and the background pane of the rotated axis doesn’t draw over the normal axis. I think these are things that need to fixed in CairoMakie though.

1 Like

Thanks a lot. Very nice solution. I just wish CairoMakie worked :cry:

@ffreyer , I’ve tried adding a scatter plot to the rotated axis, but it did not work with your example. The plot is not translated and not rotated. Any idea on what might be going on?

fig = Figure()
b = Axis(fig[1, 1])
scene = Scene(fig.scene)
campixel!(scene)
ax = Axis(scene, bbox = MakieLayout.BBox(-100, 100, -100, 100))
scatter!(ax, rand(10,2))

for s in (scene, ax.scene)
    Makie.rotate!(s, Vec3f(0,0,1), -pi/4)
    translate!(s, Vec3f(300, 300, 100))
end
fig

Alright, I’ve tried a bit more. Some issues that the previous solution had were:

  1. Applying transformation to a scene doesn’t actually transform the scene - it transforms the plots inside the scene. With the Axis call we create one in the bottom left corner and it’s still there after the transformations are applied.
  2. Transformations are passed down to child scenes and plots and apply to whatever space that scene or plot is in. So translating scene by 200 pixels also means translating ax.scene by 200 data units.

The first one can be solved by explicitly replacing ax.scene.px_area (if we only adjust it we mess with some other Axis internals) and the second by transforming plots individually. My (fragile) solution is now:


fig = Figure()
b = Axis(fig[1, 1])
scene = Scene(fig.scene)
campixel!(scene)
bbox = MakieLayout.BBox(-100, 100, -100, 100)
ax = Axis(scene, bbox = bbox)
p = scatter!(ax, 1:5, [0, 1, 2, 1, 0])

# Make axis scene visible:
#scene.plots[1].visible[] = false # axis background off
#ax.scene.clear = true # clear to background color
#ax.scene.backgroundcolor[] = RGBAf(0.9, 0.9, 0.5, 1);

# Transform decorations
ϕ = -pi/4
v = Vec3f(300, 300, 0)
for p in scene.plots
    Makie.rotate!(p, Vec3f(0,0,1), ϕ)
    translate!(p, v)
end
translate!(scene, 0, 0, 100)

# Adjust ax.scene position
scale = sqrt(2) # for a 45° rotation
center = 0.5f0* (minimum(bbox) + maximum(bbox))
ws = widths(bbox)
ax.scene.px_area = Observable(Rect2i(
    floor.(Int, center + v[Vec(1, 2)]) - ceil.(Int, 0.5 * scale .* ws), 
    2 * ceil.(Int, 0.5 * scale .* ws)
))
iscale = width(ax.scene.px_area[]) ./ ws

# rotate plots in ax.scene
model = map(ax.finallimits, ax.scene.px_area) do rect, px_area 
    bb_width = widths(ax.layoutobservables.computedbbox[])
    iscale = width(px_area) ./ bb_width
    ws = widths(rect)
    center = minimum(rect) .+ 0.5ws
    Makie.translationmatrix(Vec3f(center..., 100)) *
    Makie.scalematrix(Vec3f((ws ./ iscale)..., 1)) *
    Makie.rotationmatrix_z(Float32(ϕ)) *
    Makie.scalematrix(Vec3f((1.0 ./ ws)..., 1)) *
    Makie.translationmatrix(Vec3f(-center..., 0))
end
# p.model = model
ax.scene.transformation.model[] = model[]
p.markersize[] = 9 / iscale[1]

fig

This breaks when the limits of the rotated axis change, when that axis is not square or when it’s rotated by some other angle (both require different transformations).