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 latter they do because they are actual geometries in 3D space. See the next example:

  1. using GLMakie
  2. GLMakie.activate!()
  1. function scatters_in_3D()
  2. seed!(123)
  3. n = 10
  4. x, y, z = randn(n), randn(n), randn(n)
  5. fig = Figure(; resolution=(1200, 400))
  6. ax1 = Axis3(fig[1, 1]; aspect=(1, 1, 1), perspectiveness=0.5)
  7. ax2 = Axis3(fig[1, 2]; aspect=(1, 1, 1), perspectiveness=0.5)
  8. ax3 = Axis3(fig[1, 3]; aspect=:data, perspectiveness=0.5)
  9. scatter!(ax1, x, y, z; markersize=15)
  10. meshscatter!(ax2, x, y, z; markersize=0.25)
  11. hm = meshscatter!(ax3, x, y, z; markersize=0.25,
  12. marker=Rect3f(Vec3f(0), Vec3f(1)), color=1:n,
  13. colormap=:plasma, transparency=false)
  14. Colorbar(fig[1, 4], hm, label="values", height=Relative(0.5))
  15. fig
  16. end
  17. JDS.scatters_in_3D()

Figure 36: 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:

  1. function lines_in_3D()
  2. seed!(123)
  3. n = 10
  4. x, y, z = randn(n), randn(n), randn(n)
  5. fig = Figure(; resolution=(1200, 500))
  6. ax1 = Axis3(fig[1, 1]; aspect=(1, 1, 1), perspectiveness=0.5)
  7. ax2 = Axis3(fig[1, 2]; aspect=(1, 1, 1), perspectiveness=0.5)
  8. ax3 = Axis3(fig[1, 3]; aspect=:data, perspectiveness=0.5)
  9. lines!(ax1, x, y, z; color=1:n, linewidth=3)
  10. scatterlines!(ax2, x, y, z; markersize=15)
  11. hm = meshscatter!(ax3, x, y, z; markersize=0.2, color=1:n)
  12. lines!(ax3, x, y, z; color=1:n)
  13. Colorbar(fig[2, 1], hm; label="values", height=15, vertical=false,
  14. flipaxis=false, ticksize=15, tickalign=1, width=Relative(3.55 / 4))
  15. fig
  16. end
  17. JDS.lines_in_3D()

Figure 37: 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:

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

The output for the different plotting functions is

  1. function plot_peaks_function()
  2. x, y, z = peaks()
  3. x2, y2, z2 = peaks(; n=15)
  4. fig = Figure(resolution=(1200, 400))
  5. axs = [Axis3(fig[1, i]; aspect=(1, 1, 1)) for i = 1:3]
  6. hm = surface!(axs[1], x, y, z)
  7. wireframe!(axs[2], x2, y2, z2)
  8. contour3d!(axs[3], x, y, z; levels=20)
  9. Colorbar(fig[1, 4], hm, height=Relative(0.5))
  10. fig
  11. end
  12. JDS.plot_peaks_function()

Figure 38: 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):

  1. function heatmap_contour_and_contourf()
  2. x, y, z = peaks()
  3. fig = Figure(resolution=(1200, 400))
  4. axs = [Axis(fig[1, i]; aspect=DataAspect()) for i = 1:3]
  5. hm = heatmap!(axs[1], x, y, z)
  6. contour!(axs[2], x, y, z; levels=20)
  7. contourf!(axs[3], x, y, z)
  8. Colorbar(fig[1, 4], hm, height=Relative(0.5))
  9. fig
  10. end
  11. JDS.heatmap_contour_and_contourf()

Figure 39: 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:

  1. function heatmap_contour_and_contourf_in_a_3d_plane()
  2. x, y, z = peaks()
  3. fig = Figure(resolution=(1200, 400))
  4. axs = [Axis3(fig[1, i]) for i = 1:3]
  5. hm = heatmap!(axs[1], x, y, z)
  6. contour!(axs[2], x, y, z; levels=20)
  7. contourf!(axs[3], x, y, z)
  8. Colorbar(fig[1, 4], hm, height=Relative(0.5))
  9. fig
  10. end
  11. JDS.heatmap_contour_and_contourf_in_a_3d_plane()

Figure 40: 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:

  1. using TestImages
  1. function mixing_surface_contour3d_contour_and_contourf()
  2. img = testimage("coffee.png")
  3. x, y, z = peaks()
  4. cmap = :Spectral_11
  5. fig = Figure(resolution=(1200, 800), fontsize=26)
  6. ax1 = Axis3(fig[1, 1]; aspect=(1, 1, 1), elevation=pi / 6, xzpanelcolor=(:black, 0.75),
  7. perspectiveness=0.5, yzpanelcolor=:black, zgridcolor=:grey70,
  8. ygridcolor=:grey70, xgridcolor=:grey70)
  9. ax2 = Axis3(fig[1, 3]; aspect=(1, 1, 1), elevation=pi / 6, perspectiveness=0.5)
  10. hm = surface!(ax1, x, y, z; colormap=(cmap, 0.95), shading=true)
  11. contour3d!(ax1, x, y, z .+ 0.02; colormap=cmap, levels=20, linewidth=2)
  12. xmin, ymin, zmin = minimum(ax1.finallimits[])
  13. xmax, ymax, zmax = maximum(ax1.finallimits[])
  14. contour!(ax1, x, y, z; colormap=cmap, levels=20, transformation=(:xy, zmax))
  15. contourf!(ax1, x, y, z; colormap=cmap, transformation=(:xy, zmin))
  16. Colorbar(fig[1, 2], hm, width=15, ticksize=15, tickalign=1, height=Relative(0.35))
  17. # transformations into planes
  18. heatmap!(ax2, x, y, z; colormap=:viridis, transformation=(:yz, 3.5))
  19. contourf!(ax2, x, y, z; colormap=:CMRmap, transformation=(:xy, -3.5))
  20. contourf!(ax2, x, y, z; colormap=:bone_1, transformation=(:xz, 3.5))
  21. image!(ax2, -3 .. 3, -3 .. 2, rotr90(img); transformation=(:xy, 3.8))
  22. xlims!(ax2, -3.8, 3.8)
  23. ylims!(ax2, -3.8, 3.8)
  24. zlims!(ax2, -3.8, 3.8)
  25. fig
  26. end
  27. JDS.mixing_surface_contour3d_contour_and_contourf()

Figure 41: 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 below18:

  1. using LinearAlgebra
  1. function arrows_and_streamplot_in_3d()
  2. ps = [Point3f(x, y, z) for x = -3:1:3 for y = -3:1:3 for z = -3:1:3]
  3. ns = map(p -> 0.1 * rand() * Vec3f(p[2], p[3], p[1]), ps)
  4. lengths = norm.(ns)
  5. flowField(x, y, z) = Point(-y + x * (-1 + x^2 + y^2)^2, x + y * (-1 + x^2 + y^2)^2,
  6. z + x * (y - z^2))
  7. fig = Figure(resolution=(1200, 800), fontsize=26)
  8. axs = [Axis3(fig[1, i]; aspect=(1, 1, 1), perspectiveness=0.5) for i = 1:2]
  9. arrows!(axs[1], ps, ns, color=lengths, arrowsize=Vec3f(0.2, 0.2, 0.3),
  10. linewidth=0.1)
  11. streamplot!(axs[2], flowField, -4 .. 4, -4 .. 4, -4 .. 4, colormap=:plasma,
  12. gridsize=(7, 7), arrow_size=0.25, linewidth=1)
  13. fig
  14. end
  15. JDS.arrows_and_streamplot_in_3d()

Figure 42: 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:

  1. using GeometryBasics
  1. function mesh_volume_contour()
  2. # mesh objects
  3. rectMesh = Rect3f(Vec3f(-0.5), Vec3f(1))
  4. recmesh = GeometryBasics.mesh(rectMesh)
  5. sphere = Sphere(Point3f(0), 1)
  6. # https://juliageometry.github.io/GeometryBasics.jl/stable/primitives/
  7. spheremesh = GeometryBasics.mesh(Tesselation(sphere, 64))
  8. # uses 64 for tesselation, a smoother sphere
  9. colors = [rand() for v in recmesh.position]
  10. # cloud points for volume
  11. x = y = z = 1:10
  12. vals = randn(10, 10, 10)
  13. fig = Figure(resolution=(1200, 400))
  14. axs = [Axis3(fig[1, i]; aspect=(1, 1, 1), perspectiveness=0.5) for i = 1:3]
  15. mesh!(axs[1], recmesh; color=colors, colormap=:rainbow, shading=false)
  16. mesh!(axs[1], spheremesh; color=(:white, 0.25), transparency=true)
  17. volume!(axs[2], x, y, z, vals; colormap=Reverse(:plasma))
  18. contour!(axs[3], x, y, z, vals; colormap=Reverse(:plasma))
  19. fig
  20. end
  21. JDS.mesh_volume_contour()

Figure 43: 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.

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

  1. using GeometryBasics, Colors

For the spheres let’s do a rectangular grid. Also, we will use a different color for each one of them. Additionally, we can mix spheres and a rectangular plane. Next, we define all the necessary data.

  1. seed!(123)
  2. spheresGrid = [Point3f(i,j,k) for i in 1:2:12 for j in 1:2:10 for k in 1:2:10]
  3. colorSphere = [RGBA(i * 0.1, j * 0.1, k * 0.1, 0.75) for i in 1:2:12 for j in 1:2:10 for k in 1:2:10]
  4. spheresPlane = [Point3f(i,j,k) for i in 1:2.5:20 for j in 1:2.5:10 for k in 1:2.5:4]
  5. cmap = get(colorschemes[:plasma], LinRange(0, 1, 50))
  6. colorsPlane = cmap[rand(1:50,50)]
  7. rectMesh = Rect3f(Vec3f(-1, -1, 2.1), Vec3f(22, 11, 0.5))
  8. recmesh = GeometryBasics.mesh(rectMesh)
  9. colors = [RGBA(rand(4)...) for v in recmesh.position]

Then, the plot is simply done with:

  1. function grid_spheres_and_rectangle_as_plate()
  2. fig = with_theme(theme_dark()) do
  3. fig = Figure(resolution=(1200, 800))
  4. ax1 = Axis3(fig[1, 1]; aspect=:data, perspectiveness=0.5, azimuth=0.72)
  5. ax2 = Axis3(fig[1, 2]; aspect=:data, perspectiveness=0.5)
  6. meshscatter!(ax1, spheresGrid; color=colorSphere, markersize=1,
  7. shading=false)
  8. meshscatter!(ax2, spheresPlane; color=colorsPlane, markersize=0.75,
  9. lightposition=Vec3f(10, 5, 2), ambient=Vec3f(0.95, 0.95, 0.95),
  10. backlight=1.0f0)
  11. mesh!(recmesh; color=colors, colormap=:rainbow, shading=false)
  12. limits!(ax1, 0, 10, 0, 10, 0, 10)
  13. fig
  14. end
  15. fig
  16. end
  17. JDS.grid_spheres_and_rectangle_as_plate()

Figure 44: 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 to implement which in turn could be used for plotting a 3D histogram. See our next example, where we are using again our peaks function and some additional definitions:

  1. x, y, z = peaks(; n=15)
  2. δx = (x[2] - x[1]) / 2
  3. δy = (y[2] - y[1]) / 2
  4. cbarPal = :Spectral_11
  5. ztmp = (z .- minimum(z)) ./ (maximum(z .- minimum(z)))
  6. cmap = get(colorschemes[cbarPal], ztmp)
  7. cmap2 = reshape(cmap, size(z))
  8. ztmp2 = abs.(z) ./ maximum(abs.(z)) .+ 0.15

here \(\delta x, \delta y\) are used to specify our boxes size. cmap2 will be the color for each box and ztmp2 will be used as a transparency parameter. See the output in the next figure.

  1. function histogram_or_bars_in_3d()
  2. fig = Figure(resolution=(1200, 800), fontsize=26)
  3. ax1 = Axis3(fig[1, 1]; aspect=(1, 1, 1), elevation / 6,
  4. perspectiveness=0.5)
  5. ax2 = Axis3(fig[1, 2]; aspect=(1, 1, 1), perspectiveness=0.5)
  6. rectMesh = Rect3f(Vec3f(-0.5, -0.5, 0), Vec3f(1, 1, 1))
  7. meshscatter!(ax1, x, y, 0 * z, marker=rectMesh, color=z[:],
  8. markersize=Vec3f.(2δx, 2δy, z[:]), colormap=:Spectral_11,
  9. shading=false)
  10. limits!(ax1, -3.5, 3.5, -3.5, 3.5, -7.45, 7.45)
  11. meshscatter!(ax2, x, y, 0 * z, marker=rectMesh, color=z[:],
  12. markersize=Vec3f.(2δx, 2δy, z[:]), colormap=(:Spectral_11, 0.25),
  13. shading=false, transparency=true)
  14. for (idx, i) in enumerate(x), (idy, j) in enumerate(y)
  15. rectMesh = Rect3f(Vec3f(i - δx, j - δy, 0), Vec3f(2δx, 2δy, z[idx, idy]))
  16. recmesh = GeometryBasics.mesh(rectMesh)
  17. lines!(ax2, recmesh; color=(cmap2[idx, idy], ztmp2[idx, idy]))
  18. end
  19. fig
  20. end
  21. JDS.histogram_or_bars_in_3d()

Figure 45: 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:

  1. function filled_line_and_linesegments_in_3D()
  2. xs = LinRange(-3, 3, 10)
  3. lower = [Point3f(i, -i, 0) for i in LinRange(0, 3, 100)]
  4. upper = [Point3f(i, -i, sin(i) * exp(-(i + i))) for i in range(0, 3, length=100)]
  5. fig = Figure(resolution=(1200, 800))
  6. axs = [Axis3(fig[1, i]; elevation=pi / 6, perspectiveness=0.5) for i = 1:2]
  7. band!(axs[1], lower, upper, color=repeat(norm.(upper), outer=2), colormap=:CMRmap)
  8. lines!(axs[1], upper, color=:black)
  9. linesegments!(axs[2], cos.(xs), xs, sin.(xs), linewidth=5, color=1:length(xs))
  10. fig
  11. end
  12. JDS.filled_line_and_linesegments_in_3D()

Figure 46: 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!

6.7 GLMakie.jl - 图12 Support this project
CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso