5.7 GLMakie.jl

CairoMakie.jl 满足了所有关于静态 2D 图的需求。 但除此之外,有时候还需要交互性,特别是在处理 3D 图的时候。 使用 3D 图可视化数据是 洞察 数据的常见做法。 这就是 GLMakie.jl 的用武之地,它使用 OpenGL 作为添加交互和响应功能的绘图后端。 与之前一样,一幅简单的图只包括线和点。因此,接下来将从简单图开始。因为已经知道布局如何使用,所以将在例子中应用一些布局。

5.7.1 散点图和折线图

散点图有两种绘制选项,第一种是 scatter(x, y, z),另一种是 meshscatter(x, y, z)。 若使用第一种,标记则不会沿着坐标轴缩放,但在使用第二种时标记会缩放, 这是因为此时它们是三维空间的几何实体。 例子如下:

  1. using GLMakie
  2. GLMakie.activate!()
  1. function scatters_in_3D()
  2. seed!(123)
  3. xyz = randn(10, 3)
  4. x, y, z = xyz[:, 1], xyz[:, 2], xyz[:, 3]
  5. fig = Figure(resolution=(1600, 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=50)
  10. meshscatter!(ax2, x, y, z; markersize=0.25)
  11. hm = meshscatter!(ax3, x, y, z; markersize=0.25,
  12. marker=FRect3D(Vec3f(0), Vec3f(1)), color=1:size(xyz)[2],
  13. colormap=:plasma, transparency=false)
  14. Colorbar(fig[1, 4], hm, label="values", height=Relative(0.5))
  15. fig
  16. end
  17. scatters_in_3D()

Figure 36: Scatters in 3D.

Figure 36: Scatters in 3D.

另请注意,标记可以是不同的几何实体,比如正方形或矩形。另外,也可以为标记设置 colormap。 对于上面位于中间的 3D 图,如果想得到获得完美的球体,那么只需如右侧图那样添加 aspect = :data 参数。 绘制 linesscatterlines 也很简单:

  1. function lines_in_3D()
  2. seed!(123)
  3. xyz = randn(10, 3)
  4. x, y, z = xyz[:, 1], xyz[:, 2], xyz[:, 3]
  5. fig = Figure(resolution=(1600, 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. lines!(ax1, x, y, z; color=1:size(xyz)[2], linewidth=3)
  10. scatterlines!(ax2, x, y, z; markersize=50)
  11. hm = meshscatter!(ax3, x, y, z; markersize=0.2, color=1:size(xyz)[2])
  12. lines!(ax3, x, y, z; color=1:size(xyz)[2])
  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. lines_in_3D()

Figure 37: Lines in 3D.

Figure 37: Lines in 3D.

在 3D 图中绘制 surfacewireframecontour 是一项容易的工作。

5.7.2 表面,wireframecontourcontourfcontour3d

将使用如下的 peaks 函数展示这些例子:

  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

不同绘图函数的输出如下:

  1. function plot_peaks_function()
  2. x, y, z = peaks()
  3. x2, y2, z2 = peaks(; n=15)
  4. fig = Figure(resolution=(1600, 400), fontsize=26)
  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. plot_peaks_function()

Figure 38: Plot peaks function.

Figure 38: Plot peaks function.

但是也可以使用 heatmap(x, y, z)contour(x, y, z)contourf(x, y, z) 绘图:

  1. function heatmap_contour_and_contourf()
  2. x, y, z = peaks()
  3. fig = Figure(resolution=(1600, 400), fontsize=26)
  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. heatmap_contour_and_contourf()

Figure 39: Heatmap contour and contourf.

Figure 39: Heatmap contour and contourf.

另外,只要将Axis 更改为 Axis3,这些图就会自动位于 x-y 平面:

  1. function heatmap_contour_and_contourf_in_a_3d_plane()
  2. x, y, z = peaks()
  3. fig = Figure(resolution=(1600, 400), fontsize=26)
  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. 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.

将这些绘图函数混合在一起也是非常简单的,如下所示:

  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. mixing_surface_contour3d_contour_and_contourf()

Figure 41: Mixing surface, contour3d, contour and contourf.

Figure 41: Mixing surface, contour3d, contour and contourf.

还不错,对吧?从这里也可以看出,任何的 heatmapcontourcontourfimage 都可以绘制在任何平面上。

5.7.3 arrowsstreamplot

当想要知道给定变量的方向时,arrowsstreamplot 会变得非常有用。 参见如下的示例18

  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=Vec3f0(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. arrows_and_streamplot_in_3d()

Figure 42: Arrows and streamplot in 3d.

Figure 42: Arrows and streamplot in 3d.

另外一些有趣的例子是 mesh(obj)volume(x, y, z, vals)contour(x, y, z, vals)

5.7.4 meshvolume

绘制网格在想要画出几何实体时很有用,例如 Sphere 或矩形这样的几何实体,即 FRect3D。 另一种在 3D 空间中可视化的方法是调用 volumecontour 函数,它们通过实现 光线追踪) 来模拟各种光学效果。 例子如下:

  1. using GeometryBasics
  1. function mesh_volume_contour()
  2. # mesh objects
  3. rectMesh = FRect3D(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=(1600, 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. mesh_volume_contour()

Figure 43: Mesh volume contour.

Figure 43: Mesh volume contour.

注意到透明球和立方体绘制在同一个坐标系中。 截至目前,我们已经包含了 3D 绘图的大多数用例。 另一个例子是 ?linesegments

参考之前的例子,可以使用球体和矩形平面创建一些自定义图:

  1. using GeometryBasics, Colors

首先为球体定义一个矩形网格,而且给每个球定义不同的颜色。 另外,可以将球体和平面混合在一张图里。下面的代码定义了所有必要的数据。

  1. seed!(123)
  2. spheresGrid = [Point3f(i,j,k) for i in 1:2:10 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:10 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 = FRect3D(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]

然后可使用如下方式简单地绘图:

  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. grid_spheres_and_rectangle_as_plate()

Figure 44: Grid spheres and rectangle as plate.

Figure 44: Grid spheres and rectangle as plate.

注意,右侧图中的矩形平面是半透明的,这是因为颜色函数 RGBA() 中定义了 alpha 参数。 矩形函数是通用的,因此很容易用来实现 3D 方块,而它又能用于绘制 3D 直方图。 参见如下的例子,我们将再次使用 peaks 函数并增加一些定义:

  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

其中方块的尺寸由 \(\delta x, \delta y\) 指定。 cmap2 用于指定每个方块的颜色而 ztmp2 用于指定每个方块的透明度。如下图所示。

  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 = FRect3D(Vec3f0(-0.5, -0.5, 0), Vec3f0(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 = FRect3D(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. histogram_or_bars_in_3d()

Figure 45: Histogram or bars in 3d.

Figure 45: Histogram or bars in 3d.

应注意到,也可以在 mesh 对象上调用 lineswireframe

5.7.5 填充的线和带

在最终的例子中, 我们将展示如何使用 band和一些 linesegments 填充 3D 图中的曲线:

  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. filled_line_and_linesegments_in_3D()

Figure 46: Filled line and linesegments in 3D.

Figure 46: Filled line and linesegments in 3D.

最后,我们的3D绘图之旅到此结束。 你可以将我们这里展示的一切结合起来,去创造令人惊叹的 3D 图!

CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso, 刘贵欣 (中文翻译), 田俊 (中文审校)