6.7 GLMakie.jl

CairoMakie.jl supplies all our needs for static 2D images. But sometimes we want interactivity, especially when we are dealing with 3D images. Visualizing data in 3D is also a common practice to gain insight from your data. This is where GLMakie.jl might be helpful, since it uses OpenGL as a backend that adds interactivity and responsiveness to plots. Like before, a simple plot includes, of course, lines and points. So, we will start with those and since we already know how layouts work, we will put that into practice.

6.7.1 Scatters and Lines

For scatter plots we have two options, the first one is scatter(x, y, z) and the second one is meshscatter(x, y, z). In the first one markers don’t scale in the axis directions, but in the later they do because they are actual geometries in 3D space. See the next example:

using GLMakie
GLMakie.activate!()
function scatters_in_3D()
    Random.seed!(123)
    xyz = randn(10, 3)
    x, y, z = xyz[:,1], xyz[:,2], xyz[:,3]
    fig = Figure(resolution = (1800, 600), fontsize = 26)
    ax1 = Axis3(fig; aspect = (1, 1, 1), perspectiveness = 0.5)
    ax2 = Axis3(fig; aspect= (1,1,1), perspectiveness = 0.5,)
    ax3 = Axis3(fig; aspect= :data, perspectiveness = 0.5,)

    scatter!(ax1, x, y, z; markersize = 50)
    meshscatter!(ax2, x, y, z; markersize = 0.25)
    hm = meshscatter!(ax3, x, y, z; markersize = 0.25,
        marker = FRect3D(Vec3f0(0), Vec3f0(1)), color = 1:size(xyz)[2],
        colormap = :plasma, transparency = false)
    cbar = Colorbar(fig, hm, label = "values", height = Relative(0.5))
    fig[1,1] = ax1
    fig[1,2] = ax2
    fig[1,3] = ax3
    fig[1,4] = cbar
    fig
end
scatters_in_3D()
Figure 36: Scatters in 3D.

Note also, that a different geometry can be passed as markers, i.e., a square/rectangle and we can assign a colormap for them as well. In the middle panel one could get perfect spheres by doing aspect = :data as in the right panel.

And doing lines or scatterlines is also straightforward:

function lines_in_3D()
    Random.seed!(123)
    xyz = randn(10, 3)
    x, y, z = xyz[:,1], xyz[:,2], xyz[:,3]
    fig = Figure(resolution = (1800,600), fontsize = 26)
    ax1 = Axis3(fig; aspect= (1,1,1), perspectiveness = 0.5,)
    ax2 = Axis3(fig; aspect= (1,1,1), perspectiveness = 0.5,)
    ax3 = Axis3(fig; aspect= :data, perspectiveness = 0.5,)

    lines!(ax1, x, y, z; color = 1:size(xyz)[2], linewidth=3)
    scatterlines!(ax2, x, y, z; markersize = 50)
    hm = meshscatter!(ax3, x, y, z; markersize = 0.2,
        color = 1:size(xyz)[2])
    lines!(ax3, x, y, z; color = 1:size(xyz)[2])
    cbar = Colorbar(fig, hm; label = "values", height = 15, vertical = false,
     flipaxis = false, ticksize=15, tickalign = 1, width = Relative(3.55/4))
    fig[1,1] = ax1
    fig[1,2] = ax2
    fig[1,3] = ax3
    fig[2,1] = cbar
    fig
end
lines_in_3D()
Figure 37: Lines in 3D.

Plotting a surface is also easy to do as well as a wireframe and contour lines in 3D.

6.7.2 Surfaces, wireframe, contour, contourf and contour3d

To show these cases we’ll use the following peaks function:

function peaks(; n = 49)
    x = LinRange(-3, 3, n)
    y = LinRange(-3, 3, n)
    a = 3 * (1 .- x').^2 .* exp.(-(x'.^2) .- (y .+ 1).^2)
    b = 10 * (x' / 5 .- x'.^3 .- y.^5) .* exp.(-x'.^2 .- y.^2)
    c = 1 / 3 * exp.(-(x' .+ 1).^2 .- y.^2)
    return (x, y, a .- b .- c)
end

The output for the different plotting functions is

function plot_peaks_function()
    x, y, z = peaks()
    x2, y2, z2 = peaks(;n = 15)

    fig = Figure(resolution = (1800,600), fontsize = 26)
    ax1 = Axis3(fig[1,1]; aspect= (1,1,1))
    ax2 = Axis3(fig[1,2]; aspect= (1,1,1))
    ax3 = Axis3(fig[1,3]; aspect= (1,1,1))

    hm = surface!(ax1, x, y, z)
    wireframe!(ax2, x2, y2, z2)
    contour3d!(ax3, x, y, z; levels = 20)
    Colorbar(fig[1,4], hm, height = Relative(0.5))
    fig
end
plot_peaks_function()
Figure 38: Plot peaks function.

But, it can also be plotted with a heatmap(x, y, z), contour(x, y, z) or contourf(x, y, z):

function heatmap_contour_and_contourf()
    x, y, z = peaks()

    fig = Figure(resolution = (1800,600), fontsize = 26)
    ax1 = Axis(fig[1,1]; aspect = DataAspect())
    ax2 = Axis(fig[1,2]; aspect = DataAspect())
    ax3 = Axis(fig[1,3]; aspect = DataAspect())

    hm = heatmap!(ax1, x, y, z)
    contour!(ax2, x, y, z; levels = 20)
    contourf!(ax3, x, y, z)
    Colorbar(fig[1,4], hm, height = Relative(0.5))
    fig
end
heatmap_contour_and_contourf()
Figure 39: Heatmap contour and contourf.

Additionally, by changing Axis to an Axis3, these plots will be automatically be in the x-y plane:

function heatmap_contour_and_contourf_in_a_3d_plane()
    x, y, z = peaks()

    fig = Figure(resolution = (1800,600), fontsize = 26)
    ax1 = Axis3(fig[1,1])
    ax2 = Axis3(fig[1,2])
    ax3 = Axis3(fig[1,3])

    hm = heatmap!(ax1, x, y, z)
    contour!(ax2, x, y, z; levels = 20)
    contourf!(ax3, x, y, z)
    Colorbar(fig[1,4], hm, height = Relative(0.5))
    fig
end
heatmap_contour_and_contourf_in_a_3d_plane()
Figure 40: Heatmap contour and contourf in a 3d plane.

Something else that is easy to do is to mix all these plotting functions into just one plot, namely:

using TestImages
function mixing_surface_contour3d_contour_and_contourf()
    img = testimage("coffee.png")
    x, y, z = peaks()
    cmap = :Spectral_11

    fig = Figure(resolution = (1400,800), fontsize = 26)
    ax1 = Axis3(fig[1,1]; aspect = (1,1,1), elevation = pi/6,
        perspectiveness = 0.5, xzpanelcolor= (:black,0.75),
        yzpanelcolor= :black, zgridcolor = :grey70,
        ygridcolor = :grey70, xgridcolor = :grey70)
    ax2 = Axis3(fig[1,3]; aspect = (1,1,1), elevation = pi/6,
        perspectiveness = 0.5)

    hm = surface!(ax1, x, y, z; colormap = (cmap, 0.95), shading = true)
    contour3d!(ax1, x, y, z .+ 0.02; colormap =cmap,
        levels = 20, linewidth = 2 )
    xmin, ymin, zmin = minimum(ax1.finallimits[])
    xmax, ymax, zmax = maximum(ax1.finallimits[])
    contour!(ax1, x, y, z; colormap = cmap, levels = 20,
        transformation = (:xy, zmax))
    contourf!(ax1, x, y, z; colormap = cmap,
        transformation = (:xy, zmin))
    Colorbar(fig[1,2], hm, width = 15, ticksize=15, tickalign = 1,
        height = Relative(0.35))
    # transformations into planes
    heatmap!(ax2, x, y, z; colormap = :viridis,
        transformation = (:yz, 3.5))
    contourf!(ax2, x, y, z; colormap = :CMRmap,
        transformation = (:xy, -3.5))
    contourf!(ax2, x, y, z; colormap = :bone_1,
        transformation = (:xz, 3.5))
    image!(ax2, -3..3, -3..2, rotr90(img);
        transformation = (:xy, 3.8))
    xlims!(ax2, -3.8,3.8)
    ylims!(ax2, -3.8,3.8)
    zlims!(ax2, -3.8,3.8)
    fig
end
mixing_surface_contour3d_contour_and_contourf()
Figure 41: Mixing surface contour3d contour and contourf.

Not bad, right? From there is clear that any heatmap’s, contour’s, contourf’s or image can be plotted into any plane.

6.7.3 Arrows and Streamplots

arrows and streamplot are plots that might be useful when we want to know the directions that a given variable will follow. See a demonstration below:

using LinearAlgebra
function arrows_and_streamplot_in_3d()
    ps = [Point3f0(x, y, z) for x in -3:1:3 for y in -3:1:3 for z in -3:1:3]
    ns = map(p -> 0.1*rand() * Vec3f0(p[2], p[3], p[1]), ps)
    lengths = norm.(ns)
    flowField(x,y,z) = Point(-y + x*(-1+x^2+y^2)^2, x + y*(-1+x^2+y^2)^2,
        z + x*(y-z^2))

    fig = Figure(resolution = (1400,800), fontsize = 26)
    ax1 = Axis3(fig[1,1]; aspect = (1,1,1),  perspectiveness = 0.5)
    ax2 = Axis3(fig[1,2]; aspect = (1,1,1),  perspectiveness = 0.5)
    arrows!(ax1, ps, ns, color=lengths, linewidth = 0.1,
        arrowsize = Vec3f0(0.2, 0.2, 0.3), align = :center)
    streamplot!(ax2, flowField, -4..4, -4..4, -4..4, colormap = :plasma,
        gridsize= (7,7), arrow_size = 0.25,linewidth=1)
    fig

end
arrows_and_streamplot_in_3d()
Figure 42: Arrows and streamplot in 3d.

Other interesting examples are a mesh(obj), a volume(x, y, z, vals), and a contour(x, y, z, vals).

6.7.4 Meshes and Volumes

Drawing Meshes comes in handy when you want to plot geometries, like a Sphere or a Rectangle, i. e. FRect3D. Another approach to visualize points in 3D space is by calling the functions volume and contour, which implements ray tracing to simulate a wide variety of optical effects. See the next examples:

using GeometryBasics
function mesh_volume_contour()
    # mesh objects
    rectMesh = FRect3D(Vec3f0(-0.5), Vec3f0(1))
    recmesh = GeometryBasics.mesh(rectMesh)
    sphere = Sphere(Point3f0(0), 1)
    # https://juliageometry.github.io/GeometryBasics.jl/stable/primitives/
    spheremesh = GeometryBasics.mesh(Tesselation(sphere, 64))
    # uses 64 for tesselation, a smoother sphere
    colors = [rand() for v in recmesh.position]
    # cloud points for volume
    x = 1:10
    y = 1:10
    z = 1:10
    vals = randn(10,10,10)
    # figure
    fig = Figure(resolution = (1800,600), fontsize = 26)
    ax1 = Axis3(fig[1,1]; aspect = (1,1,1),  perspectiveness = 0.5)
    ax2 = Axis3(fig[1,2]; aspect = (1,1,1),  perspectiveness = 0.5)
    ax3 = Axis3(fig[1,3]; aspect = (1,1,1),  perspectiveness = 0.5)

    mesh!(ax1, recmesh; color= colors, colormap = :rainbow, shading = false)
    mesh!(ax1, spheremesh; color = (:white,0.25), transparency = true)
    volume!(ax2, x, y, z, vals; colormap = Reverse(:plasma))
    contour!(ax3, x, y, z, vals; colormap = Reverse(:plasma))
    fig
end
mesh_volume_contour()
Figure 43: Mesh volume contour.

Note that here we are plotting two meshes in the same axis, one transparent sphere and a cube. So far, we have covered most of the 3D use-cases. Another example is ?linesegments.

Taking as reference the previous example one can do the following custom plot with spheres and rectangles:

using GeometryBasics, Colors
function grid_spheres_and_rectangle_as_plate()
    Random.seed!(123)
    rectMesh = FRect3D(Vec3f0(-1,-1,2.1), Vec3f0(22,11,0.5))
    recmesh = GeometryBasics.mesh(rectMesh)
    colors = [RGBA(rand(4)...) for v in recmesh.position]
    fig = with_theme(theme_dark()) do
        fig = Figure(resolution = (1600,800), fontsize = 26)
        ax1 = Axis3(fig[1,1]; aspect = (1,1,1),  perspectiveness = 0.5,
            azimuth= 0.7223275083269882)
        ax2 = Axis3(fig[1,2], aspect=:data, perspectiveness = 0.5,)

        for i in 1:2:10, j in 1:2:10, k in 1:2:10
            sphere = Sphere(Point3f0(i,j,k), 1)
            spheremesh = GeometryBasics.mesh(Tesselation(sphere, 32))
            mesh!(ax1, spheremesh; color = RGBA(i*0.1,j*0.1,k*0.1, 0.75),
                transparency = false, shading = false)
        end
        cbarPal = :plasma
        cmap = get(colorschemes[cbarPal], LinRange(0,1,50))
        for i in 1:2.5:20, j in 1:2.5:10, k in 1:2.5:4
            sphere = Sphere(Point3f0(i,j,k), 1)
            spheremesh = GeometryBasics.mesh(Tesselation(sphere, 32))
            mesh!(ax2, spheremesh; color = cmap[rand(1:50)],
            lightposition = Vec3f0(10, 5, 2),
            ambient = Vec3f0(0.95, 0.95, 0.95), backlight = 1f0,
            transparency = false, shading = true)
        end
        mesh!(recmesh; color= colors, colormap = :rainbow, shading = false,
            transparency = false)
        #hidedecorations!(ax2)
        fig
    end
    fig
end
grid_spheres_and_rectangle_as_plate()
Figure 44: Grid spheres and rectangle as plate.

Here, the rectangle is semi-transparent due to the alpha channel added to the RGB color. The rectangle function is quite versatile, for instance 3D boxes are easy do implement which in turn could be used for plotting a 3D histogram. See our next example:

function histogram_or_bars_in_3d()
    x, y, z = peaks(;n = 15)
    δx = (x[2] - x[1])/2
    δy = (y[2] - y[1])/2

    cbarPal = :Spectral_11
    ztmp = (z .- minimum(z))./(maximum(z .- minimum(z)))
    cmap = get(colorschemes[cbarPal], ztmp)
    cmap2 = reshape(cmap, size(z))
    ztmp2 = abs.(z) ./ maximum(abs.(z)) .+ 0.15

    fig = Figure(resolution = (1400,800), fontsize = 26)
    ax1 = Axis3(fig[1,1]; aspect = (1,1,1), elevation = π/6, perspectiveness = 0.5)
    ax2 = Axis3(fig[1,2]; aspect = (1,1,1), perspectiveness = 0.5)

    for (idx, i) in enumerate(x), (idy,j) in enumerate(y)
        rectMesh = FRect3D(Vec3f0(i - δx, j - δy, 0), Vec3f0(2δx, 2δy, z[idx, idy]))
        recmesh = GeometryBasics.mesh(rectMesh)
        mesh!(ax1, recmesh; color= cmap2[idx,idy], shading = false)
    end
    for (idx, i) in enumerate(x), (idy,j) in enumerate(y)
        rectMesh = FRect3D(Vec3f0(i - δx, j - δy, 0), Vec3f0(2δx, 2δy, z[idx, idy]))
        recmesh = GeometryBasics.mesh(rectMesh)
        lines!(ax2, recmesh; color= (cmap2[idx,idy], ztmp2[idx, idy]))
        mesh!(ax2, recmesh; color= (cmap2[idx,idy], 0.25),
            shading = false, transparency = true)
    end
    fig
end
histogram_or_bars_in_3d()
Figure 45: Histogram or bars in 3d.

Note, that you can also call lines or wireframe over a mesh object.

6.7.5 Filled Line and Band

For our last example we will show how to do a filled curve in 3D with band and some linesegments:

function filled_line_and_linesegments_in_3D()
    xs = LinRange(-3, 3, 10)
    lower = [Point3f0(i,-i,0) for i in LinRange(0,3,100)]
    upper = [Point3f0(i,-i, sin(i) * exp(-(i+i)) ) for i in range(0,3, length=100)]

    fig = Figure(resolution = (1400,800), fontsize = 26)
    ax1 = Axis3(fig[1,1]; elevation = pi/6, perspectiveness = 0.5)
    ax2 = Axis3(fig[1,2]; elevation = pi/6, perspectiveness = 0.5)

    band!(ax1, lower, upper, color = repeat(norm.(upper), outer=2), colormap = :CMRmap)
    lines!(ax1, upper, color = :black)
    linesegments!(ax2, cos.(xs), xs, sin.(xs), linewidth = 5,
        color = 1:length(xs), colormap = :plasma)
    fig
end
filled_line_and_linesegments_in_3D()
Figure 46: Filled line and linesegments in 3D.

Finally, our journey doing 3D plots has come to an end. You can combine everything we exposed here to create amazing 3D images! Now, it’s time to dig into the basic rules to create animations.



CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer and Lazaro Alonso