12 Makie包作图

这一部分介绍用Julia的Makie包作图方法。

参考:

using DataFrames, DataFramesMeta
using CategoricalArrays
using Statistics

12.1 Makie包

Makie是Julia语言的一个作图扩展包, 特点是高性能富,可扩展性强。 另外与另外常用的Plots扩展包相比, Plots包依赖于不同的后端, 不同的后端支持的功能有多有少, 所以Plots只能选择其中的最小部分。 Makie也有不同后端, 但更强调不同后端的增强功能。

参考:

12.1.1 Makie后端

Makie有如下后端:

  • CairoMakie:主要是各种二维图,非动态。 能制作出版质量图形。
  • GLMakie:利用OpenGL图形引擎功能, 性能强大,可以制作三维图形, 可以制作动态交互图形。 但不擅长制作矢量图。
  • WGLMakie:支持在网页中的交互图形。
  • RPRMakie:使用RadeonProRender引擎, 可以制作光线追踪图像。

12.1.2 安装和运行

安装任一后端的同时可以安装Makie。如:

using Pkg; Pkg.add("CairoMakie")

用某个后端作图前, 用using调用包并调用该后端的activate!()函数,如:

using CairoMakie
CairoMakie.activate!()

在最基本的REPL环境, CairoMakie后端无法提供图形窗口。 但是VSCode、Jupyter、Pluto等集成编辑环境或笔记本软件都可以支持显示其结果。

GLMakie在REPL也可以提供自己的图形窗口显示。

12.1.3 样例数据

12.1.3.1 学生身高体重年龄

设当前工作目录有class19.csv文件内容如下:

name,sex,age,height,weight
Sandy,F,11,130,23
Karen,F,12,143,35
Kathy,F,12,152,38
Alice,F,13,144,38
Becka,F,13,166,44
Tammy,F,14,160,46
Gail,F,14,163,41
Sharon,F,15,159,51
Mary,F,15,169,51
Thomas,M,11,146,39
James,M,12,146,38
John,M,12,150,45
Robert,M,12,165,58
Jeffrey,M,13,159,38
Duke,M,14,161,46
Alfred,M,14,175,51
William,M,15,169,51
Guido,M,15,170,60
Philip,M,16,183,68

读入为数据框:

using DataFrames, DataFramesMeta, CSV
using CategoricalArrays
dclass = CSV.read("class19.csv", DataFrame)
transform!(dclass,
    :sex => (s -> categorical(s)),
    renamecols = false)
transform!(dclass, :sex => (x -> levelcode.(x)) => :sexi,
    :age => categorical => :agec)

19 rows × 7 columns

name sex age height weight sexi agec
String7 Cat… Int64 Int64 Int64 Int64 Cat…
1 Sandy F 11 130 23 1 11
2 Karen F 12 143 35 1 12
3 Kathy F 12 152 38 1 12
4 Alice F 13 144 38 1 13
5 Becka F 13 166 44 1 13
6 Tammy F 14 160 46 1 14
7 Gail F 14 163 41 1 14
8 Sharon F 15 159 51 1 15
9 Mary F 15 169 51 1 15
10 Thomas M 11 146 39 2 11
11 James M 12 146 38 2 12
12 John M 12 150 45 2 12
13 Robert M 12 165 58 2 12
14 Jeffrey M 13 159 38 2 13
15 Duke M 14 161 46 2 14
16 Alfred M 14 175 51 2 14
17 William M 15 169 51 2 15
18 Guido M 15 170 60 2 15
19 Philip M 16 183 68 2 16

12.1.3.2 Cleveland心脏病数据

这是UCI网站的一个机器学习用样例数据, 关注的因变量是num,是一个取0,1,2,3,4数值的变量, 需要区分0与非0。 变量有:

  • age
  • sex,1为男,0为女
  • cp, 胸痛类型:
    • 1:典型心绞痛
    • 2:非典型心绞痛
    • 3:非心绞痛类型
    • 4:无症状
  • trestbps: 收缩压
  • chol: 胆固醇
  • fbs: 快速血糖是否超标,1为超标,否则0
  • restecg:心电图,
    • 0: 正常
    • 1: T-ST波异常
    • 2: 提示心室肥大
  • thalach: 最大心率
  • exang: 是否锻炼引发心绞痛,1有,0无
  • oldpeak: 锻炼引发的ST波降幅
  • slope: 锻炼ST波峰斜率,
    • 1: 向上
    • 2:平缓
    • 3: 向下
  • ca: 造影检查显示大血管数,取0,1,2,3
  • thal:
    • 3: 正常
    • 6:固化缺陷
    • 7: 可恢复缺陷
  • num: 0表示不诊断心脏病, 1,2,3表示血管造影诊断有心脏病, 主要血管50%以上狭窄
using Downloads
urlf = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
dht = CSV.read(Downloads.download(urlf), DataFrame,
    header=0)
rename!(dht, ["age", "sex", "cp", "trestbps", "chol", 
    "fbs", "restecg", "thalach", "exang", "oldpeak",    
    "slope", "ca", "thal", "num"])
303×14 DataFrame
 Row │ age      sex      cp       trestbps  chol     fbs      restecg  th ⋯     │ Float64  Float64  Float64  Float64   Float64  Float64  Float64  Fl ⋯─────┼─────────────────────────────────────────────────────────────────────   1 │    63.0      1.0      1.0     145.0    233.0      1.0      2.0     ⋯   2 │    67.0      1.0      4.0     160.0    286.0      0.0      2.0      
   3 │    67.0      1.0      4.0     120.0    229.0      0.0      2.0      
   4 │    37.0      1.0      3.0     130.0    250.0      0.0      0.0      
  ⋮  │    ⋮        ⋮        ⋮        ⋮         ⋮        ⋮        ⋮        ⋱ 300 │    68.0      1.0      4.0     144.0    193.0      1.0      0.0     ⋯ 301 │    57.0      1.0      4.0     130.0    131.0      0.0      0.0      
 302 │    57.0      0.0      2.0     130.0    236.0      0.0      2.0      
 303 │    38.0      1.0      3.0     138.0    175.0      0.0      0.0      
                                             7 columns and 295 rows omitted

12.2 简单一次性完成的图形

12.2.1 折线图

考虑如下的简单数据:

dline01 = DataFrame(
    x = 1:5,
    y = [11, 13, 18, 15, 14])

5 rows × 2 columns

x y
Int64 Int64
1 1 11
2 2 13
3 3 18
4 4 15
5 5 14
dline02 = DataFrame(x = [0,1,3,6,7,8], y=[15, 13, 14, 11, 10, 9])

6 rows × 2 columns

x y
Int64 Int64
1 0 15
2 1 13
3 3 14
4 6 11
5 7 10
6 8 9
lines(dline01[:, :x], dline01[:, :y])

上面的例子是假设在交互环境下运行, 所以运行lines(dline01[:, :x], dline01[:, :y]), 实际是运行了display(lines(dline01[:, :x], dline01[:, :y])), 所以图形在REPL、Jupyter、Pluto、VSC等交互环境中能够自动显示。 如果在编程环境中, 需要调用display函数。

12.2.2 散点图

scatter(dline01[:, :x], dline01[:, :y])

12.2.3 散点折线图

scatterlines(dline01[:, :x], dline01[:, :y])

12.2.4 直方图

hist(dclass[:, :height], bins=6)

hist(dht[:,:trestbps], bins=15)

12.2.5 密度估计曲线图

density(dclass[:, :height])

using CairoMakie: density
density(dht[:, :trestbps])

12.2.6 盒形图

boxplot(fill(1, nrow(dclass)), dclass[:, :height])

boxplot(levelcode.(dclass[:, :sex]), dclass[:, :height])

数据框中sex是分类变量, boxplot的横轴仅支持数值型, 所以用了CategoricalArrays.levelcode()将因子值转换成相应的编码序号整数值。

boxplot(fill(1, nrow(dht)), dht[:, :trestbps])

boxplot(dht[:, :sex], dht[:, :trestbps])

12.2.7 正态QQ图

qqnorm(dclass[:, :height], qqline=:fit)

正态QQ图用来检查输入的变量是否来自正态分布总体, 如果散点与直线比较接近, 则可以认为符合。 如果散点的走向与直线明显偏离, 则认为非正态分布。

qqnorm(dht[:, :trestbps], qqline=:fit)

12.2.8 经验分布函数图

StatsBase.ecdf可以计算经验分布函数。 ecdfplot(x)可以作经验分布函数图。 如:

ecdfplot(dht[:, :trestbps])

12.2.9 时间序列图

时间为序号的时间序列折线图:

df = DataFrame(time=1:10, y = (1:10) .^2)
lines(df[:,:time], df[:,:y])

横轴使用日期可以定制, 没有自动完成的例子。

12.2.10 曲面的等高线图

表现二元函数z = f(x, y)的图像的一种方式是等高线图。 用等间隔的x轴和y轴的若干个点组成矩阵形式的网格, 在每个网格点上计算z坐标, 输入x, y, z后可以将z值相等的点连线。

比如,如下的函数: \[\begin{aligned} f(x, y) =& \ \exp\{ -0.5 ((x + 1)^2 + 0.5 y^2) \} \cos(4x) \\ +& \ \exp\{-0.8 (2 x^2 + (y-1)^2) \} \cos(2y) . \end{aligned}\]

function surfd()
    n = 100
    xs = range(-pi, pi, n)
    ys = range(-pi, pi, n)
    z = [exp(-0.5*((x + 1)^2 + 0.5*y^2))*cos(4*x) +
        exp(-0.8*(2*x^2 + (y-1)^2))*cos(2*y)
        for x in xs, y in ys]
    return (xs, ys, z)
end
x, y, z = surfd()
contour(x, y, z, levels=20)

12.2.11 曲面的染色等高线图

x, y, z = surfd()
fig, ax, plt = contourf(x, y, z, levels=20)
Colorbar(fig[1,2], plt)
fig

12.2.12 曲面的热力图

x, y, z = surfd()
heatmap(x, y, z)

这个函数目前有BUG存在。

12.2.13 频数条形图

写一个计算变量离散取值频数的函数:

using StatsBase
function freq(x)
    y = StatsBase.countmap(x)
    kk = [k for (k, v) in y]
    vv = [v for (k, v) in y]
    return kk, vv
end

将类别变量转换为其显示字符串的函数:

cat2string = x -> levels(x)[levelcode.(x)]

dclass中性别的频数条形图:

sex, n = freq(cat2string(dclass[:, :sex]))
barplot(1:2, n,
    axis=(; width=100, height=200,
        xticks=(1:2, sex) ))

Makie不支持对分类变量自动统计频数再作条形图, 所以上述程序比较复杂。 第5节讲的AlgebraOfGarphics提供了自动统计频数作频数条形图的功能。

12.2.14 小结

上面的这些图都可以用很简单的程序完成。 但是, 许多图都只适合探索性分析使用, 不适合制作高质量出版图形, 因为缺少适当的坐标轴设置、标题、轴标题、图例等元素。 要制作更符合要求的图形, 需要进一步学习。

12.3 Makie重要概念和作图步骤

  • 绘图板(figure),相当于绘图用的纸张。
  • 坐标系统(axis),可以定位图形内容,设置大小等。
  • 绘图(plots)。具体的散点、连线、曲面、热力图等绘图内容。
  • 坐标轴、标题等标注。
  • 图例,颜色对应条。
  • 小图拼凑。

最基本的步骤是用Figure()函数新建绘图板, 用Axis()函数在此绘图板上建立一到多个绘图区域并自动生成坐标系统, 用散点、连线等作图函数作图。 也可以省略Figure()Axis()调用, 直接用作图函数做出简单图形。

12.3.1 画布

Figure()函数新建一个画布, 可以在其中放置坐标轴、散点、连线、标题、图例、颜色代码表等内容。 其中可以用backgroundcolor设置背景色, 用resolution设置大小, 如(800, 600),单位是像素。 例:

using CairoMakie
CairoMakie.activate!()

fig = Figure(backgroundcolor=:gray, 
    resolution = (800, 300))

12.3.2 添加坐标轴

Axis()命令在已建立的画布中建立坐标系。 最常用的输入是选择画布分格的左上角格子, 如fig[1,1], 没有分格子时fig[1,1]就是画布的全部空间。 这种分格称为“布局”(layout)。 如:

ax = Axis(fig[1,1])

Axis()中用title指定标题, xlabel指定x轴标签, ylabel指定y轴标签,如:

ax = Axis(fig[1,1],
    title = "这是标题",
    xlabel = "x",
    ylabel = "y")

12.3.3 添加图形内容及颜色设置

在用Figure()新建画布, 用Axis()添加并设置坐标系统以后, 可以用各种绘图函数在选定的坐标系统中添加图形内容。 如:

using CairoMakie
CairoMakie.activate!()

fig = Figure(backgroundcolor=:gray, 
    resolution = (800, 600))
ax = Axis(fig[1,1],
    title = "简单的折线图示例",
    xlabel = "x",
    ylabel = "y")
lines!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

设置折线颜色用color选项,如:

using Colors

fig = Figure()
ax = Axis(fig[1,1],
    title = "设置颜色的折线图示例",
    xlabel = "x",
    ylabel = "y")
lines!(ax, dline01[:,:x], dline01[:,:y],
    color = :green)
display(fig)

其中颜色可以用符号, 常用的:red, :blue, :black, :orange, :yellow, :green, :cyan, :purple, :pink, :brown, gray, white, Colors包中许多颜色名称, 还可以用编码表示颜色。 参见:

散点图例子:

fig = Figure()
ax = Axis(fig[1,1],
    title = "简单的散点图示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

散点图可以用color指定散点颜色, 用markersize指定大小(单位为像素), 用marker指定散点的符号, 用strokecolor指定散点轮廓线颜色, 用strokewidth指定散点轮廓线宽度。 如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图设置示例",
    xlabel = "x",
    ylabel = "y") 
scatter!(ax, dline01[:,:x], dline01[:,:y],
    color = :purple, marker = :utriangle, markersize=20)
display(fig)

lines!()scatter!()这些绘图函数输出是绘制的图形对象。 可以用坐标系统(Axis()的输出)为第一自变量, 如上面的例子; 也可以输入坐标系统, 这时选择当前坐标系统,当前坐标系统可以用current_axis()访问, 一般是最新生成的坐标系统。 如:

fig = Figure()
ax = Axis(fig[1,1])
scatter!(dline01[:,:x], dline01[:,:y])
display(fig)

12.3.4 多个图层

只要多次调用lines!(), scatter!()等函数就可以将多个图形叠加地画在同一坐标系中。 可以使用相同的数据或者不同的数据。如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图和折线图叠加示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y])
lines!(ax, dline02[:,:x], dline02[:,:y], color=:red)
display(fig)

只要在每个图层中用label参数指定一个图层的标签, 然后调用axislegend()函数就可以在指定的坐标系统中自动给出图例,如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图和折线图叠加示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y], label="Data A")
lines!(ax, dline02[:,:x], dline02[:,:y], color=:red, label="Data B")
axislegend(ax)
display(fig)

axislegend()可以用参数position设置位置, 用l, c, r表示左右位置, t, c, b表示上下位置, 如默认的:rt表示右上角。 也可以输入二元组的坐标。 可以不输入所针对的坐标系统, 这时默认选择最后生成的坐标系统。

除了使用统一的属性, 还可以为每个散点分别设置属性, 这只需要将属性选项对应到与输入数据个数等长的属性向量。 如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图分别设置示例",
    xlabel = "x",
    ylabel = "y") 
scatter!(ax, dline01[:,:x], dline01[:,:y],
    markersize=dline01[:,:x] .*2 .+ 10,
    color = range(0, 1, length=nrow(dline01)), 
    colormap = :thermal)
display(fig)

其中colormap指定一种调色盘, 可以用0到1的数值从调色盘中选择颜色, 这时color可以指定一个0到1中的范围就可以表示一系列渐变颜色。 也可以直接给color指定一个颜色值向量。

12.3.5 一次性完成作图

先调用Figure(),再调用Axis(), 然后再添加实际图形内容, 这是比较一般的步骤, 可以进行更多定制。 实际上, 直接调用绘图函数也能做出图形, 见节2的例子。 如:

lines(dline01[:,:x], dline01[:,:y])

注意lines是不带叹号的。 这样的一次性作图的函数本身就会建立画布和坐标系统, 其返回值类型为FigureAxisPlot, 这是画布(Figure)、坐标系统(Axis)、绘图对象的三个成分的元组。 这样的返回值可以直接用display()函数显示图形, 在交互运行时也可以不需要调用display()就可以直接显示。

也可以用如

fig, ax, plt = lines(dline01[:,:x], dline01[:,:y])

这样的调用格式调用, 再显示fig。 这样的好处是可以调整figaxis的属性, 还可以对fig修改布局。

12.3.6 显示和保存

在显示结果图形时, 在交互交互环境下如果最后一个表达式是Figure对象, 或者FigureAxisPlot对象, 可以自动调用display()函数显示图形。 如果在非交互情况下, 应该使用display(fig)来显示一个图形结果。

为了保存绘图板fig中的图形为PNG, 用如

save(fname, fig)

其中fname是以.png.PNG结尾的文件名,如"testsave.png"。 可以用关键字参数resolution指定一个大小的二元组, 单位可以认为是像素(某些后端有缩放功能)。

12.4 Makie作图定制

12.4.1 作图函数的一般语法规则

散点图、折线图等函数都使用类似的函数调用规则。 每种图都有不带叹号的版本和带叹号的版本, 如scatterscatter!。 以scatter为例, 不带叹号的版本可以直接生成画布、坐标系统、绘图对象:

fig, ax, plt = scatter(args...; kwargs)

在命令行、Jupyter或Pluto环境中, scatter的调用放在单独一个语句或者多个语句末尾可以自动显示图形。 也可以后续调用在fig指定的画布或ax指定的坐标系统中增添内容, 这时就需要将fig作为最后一个语句或者用draw(fig)显示最终的图形。 如果不需要fig, ax, plt在后续的修改中使用, 也可以简单地用

scatter(args...; kwargs)

作为命令行或单元格唯一命令或最后一个命令, 直接显示该命令的结果图形。

在已有画布的情况下, 也可以调用

ax, plt = scatter(gridposition, args...; kwargs)

在指定的布局小块或子布局块中作图。 如果后续不需要ax, plt的信息也可以直接写

scatter(gridposition, args...; kwargs)

指定布局位置的这种调用方法不支持自动显示结果, 还是要将涉及的画布在命令行或单元格的最后进行显示。

带叹号的版本只能在已有的画布或已有的坐标系统中增加内容, 不能单独生成画布, 返回值总是绘图对象, 不返回画布和坐标系统。 各种格式如:

scatter!(args...; kwargs...) # 使用当前默认坐标系统
scatter!(figure, args...; kwargs...) # 使用指定画布的[1,1]位置
scatter!(gridposition, args...; kwargs...) # 使用指定布局小块或子布局块
scatter!(axis, args...; kwargs...) # 使用指定坐标系统
scatter!(scene, args...; kwargs...) # 使用指定场景

需要的话可以用如

plt = scatter!(args...; kwargs...)

保存对应的图形对象。

12.4.2 在作图函数中设置画布和坐标系统属性

可以用Figure()设置与绘图板有关的属性, 包括背景色,像素大小等; 用Axis()设置与坐标系统有关的属性, 包括标题、轴标题、刻度设置等。 如:

fig = Figure(backgroundcolor = :gray80,
    resolution = (400, 300))
ax = Axis(fig[1,1], title = "测试标题")
plt = scatter!(dline01[:,:x], dline01[:,:y], label="散点图")
fig

返回画布、坐标系统和绘图对象三元组的绘图函数(如scatter(args...; kwargs)) 支持一个figure选项, 输入为一个命名元组, 其中可以使用与Figure()函数类似的关键字参数。 如:

scatter(dline01[:,:x], dline01[:,:y], 
    figure = (; backgroundcolor=:gray80, resolution=(400,300)))

返回画布、坐标系统和绘图对象三元组的绘图函数(如scatter(args...; kwargs))和返回坐标系统和绘图对象二元组的绘图函数(如scatter(gridposition, args...; kwargs)) 支持一个axis选项,可以在其中指定与坐标系统有关的属性。 属性写成命名元组的格式, 其中的元素值与Axis()命令的关键字参数相同。 许多元素本身也会写成元组或者命名元组形式。 如:

scatter(dline01[:,:x], dline01[:,:y], 
    figure = (; backgroundcolor=:gray80, resolution=(400,300)),
    axis = (; title="标题", xticks = 1:5))

另一种调整画布和坐标轴属性的方法是用“主题”的方法进行统一的调整, 见后面关于主题的说明。

12.4.3 绘图对象属性

scatter等一次性作图函数返回画布、坐标系统、绘图对象的三元组, 在指定布局位置时返回坐标系统、绘图对象的二元组, scatter!()这样的可变作图函数也返回绘图对象。 设plt为一个绘图对象, 则plt.attributes包括了绘图对象的各种属性, 比如颜色,符号,线型,字体等。 如:

fig, ax, plt = scatter(dline01[:,:x], dline01[:,:y])
plt.attributes
Attributes with 30 entries:
  color => RGBA{Float32}(0.0,0.447059,0.698039,1.0)
  colormap => viridis
  cycle => [:color]
  depth_shift => 0.0
  diffuse => Float32[0.4, 0.4, 0.4]
  distancefield => nothing
  fxaa => false
  glowcolor => (:black, 0.0)
  glowwidth => 0.0
  inspectable => true
  linewidth => 1
  marker => Circle
  marker_offset => Float32[-4.5, -4.5]
  markersize => 9
  markerspace => pixel
  model => Float32[1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]
  nan_color => RGBA{Float32}(0.0,0.0,0.0,0.0)
  overdraw => false
  rotations => Billboard{Float32}(0.0)
  shininess => 32.0
  space => data
  specular => Float32[0.2, 0.2, 0.2]
  ssao => false
  strokecolor => black
  strokewidth => 0
  transform_marker => false
  transformation => Automatic()
  transparency => false
  uv_offset_width => (0.0, 0.0, 0.0, 0.0)
  visible => true

对某个绘图函数如scatter, 可以在命令行用?scatter查询帮助, 就会显示它支持的属性。 如:

?scatter

12.4.3.1 颜色

Colors包定义了许多颜色名称, 参见:

ColorThemes包提供了许多调色盘, 其中的符号如:viridis, :heat可以用作调色盘参数colormap的值, 参见:

程序如:

let
    xs = range(-pi, pi, 100)
    ys = range(-pi, pi, 100)
    z = [exp(-0.1*(x^2 + y^2))*(cos(x) + sin(y)) 
        for x in xs, y in ys]
    fig = Figure()
    ax = Axis(fig[1,1])
    p = contourf!(ax, z, colormap = :heat)
    Colorbar(fig[1,2], p)
    display(fig)
end

又如:

let
    x = -2:0.1:2
    y = 10 .- x .^2
    lines(x, y, colormap = :Blues, color = y)
end

colormap的值可以增加一个透明度, 如colormap = (:heat, 0.5), 这里0.5是透明度, 越小越透明。

可以用cgrad函数从给定的若干个颜色生成渐变色, 用作colormap参数的值。

可以用categorical_colors(:Blues, 3)从ColorThemes包给出的用符号表示的调色盘生成指定个数的颜色,如:

colors = categorical_colors(:Blues, 3)
fig = Figure()
ax = Axis(fig[1,1])
scatter!(rand(3), rand(3), label="a", color = colors[2])
scatter!(rand(3), rand(3), label="b", color = colors[3])
axislegend()
fig

颜色也可以增加透明度参数,如color = (:red, 0.2)

12.4.4 主题

12.4.4.1 自定义

可以用set_theme!(...)函数设置一批公用的的属性,称为主题, 其中关于绘图板的属性直接作为关键字参数输入, 关于坐标系统的选项用命名元组输入到Axis参数中。 关于图例的选项用命名元组输入到Legend参数中。 最后用没有自变量的settheme!()取消这个主题的作用。

如:

set_theme!(; backgroundcolor=:gray80, resolution=(400,300),
    Axis = (; xtickgridvisible=true, ytickgridvisible=true,
        xgridstyle=:dot, ygridstyle=:dot),
    Legend = (; bgcolor = (:green, 0.2), framecolor=:yellow))
fig, ax, plt = scatter(dline01[:,:x], dline01[:,:y], label="数据A")
scatter!(dline02[:,:x], dline02[:,:y], label="数据B")
axislegend()
set_theme!()
fig

上面程序中bgcolor = (:green, 0.2)中的0.2是颜色透明度的设置, 数值越小越透明。

可以用pallette中的color设置主题所用的调色盘, 这在多个图层重复时自动循环使用。 如:

set_theme!(; palette = (; color = [:green, :cyan]) )
fig, ax, plt = scatter(dline01[:,:x], dline01[:,:y], label="数据A")
scatter!(dline02[:,:x], dline02[:,:y], label="数据B")
axislegend()
set_theme!()
fig

12.4.4.2 内置主题

有一些内置的主题可用, 如theme_ggplot2(), theme_minimal(), theme_black(), theme_light(), theme_dark()。 上图改用theme_black()的效果:

set_theme!( theme_black() )
fig, ax, plt = scatter(dline01[:,:x], dline01[:,:y], label="数据A")
scatter!(dline02[:,:x], dline02[:,:y], label="数据B")
axislegend()
set_theme!()
fig

12.4.4.3 调用主题

settheme!函数自定义或调用内置主题是使用主题的一种方法, 使用内置主题时也可以用关键字参数修改某一具体设置参数。 使用不带参数的settheme!()取消这样的主题选择。

还可以用with_theme(自定义绘图函数, 主题)的格式调用主题。

另一种方法是

with_theme(主题函数()) do
    ...
end

也可以编写自己的主题函数,从略。

12.4.4.4 设置循环(cycle)

同一坐标系统有多个散点或者折线图层时, 将自动循环使用颜色。 颜色可以用主题的palette中的color参数设置一个颜色名的向量, 还可以用categorical_colors(调色盘, n)从Colors包的某个调色盘生成n个颜色。

如:

let
    x = 1:10;  y = 2 .* x
    fig = Figure(); ax = Axis(fig[1,1])
    for off in 1:10
        lines!(ax, x, y .+ off, label = "Line $(off)")
    end
    fig
end

这使用了默认的主题和默认的调色盘, 有7个不重复的颜色。

人为指定三个颜色组成的调色盘:

let
    x = 1:10;  y = 2 .* x
    fig = Figure(palette = (; color = [:red, :blue, :green]))
    ax = Axis(fig[1,1])
    for off in 1:10
        lines!(ax, x, y .+ off, label = "Line $(off)")
    end
    fig
end

上面的程序在Figure()中定义了palette, 也可以在settheme!()函数中定义。

使用categorical_colors的例子:

let
    x = 1:10;  y = 2 .* x
    set_theme!(palette = (; color = categorical_colors(:PRGn, 10)))
    fig = Figure()
    ax = Axis(fig[1,1])
    for off in 1:10
        lines!(ax, x, y .+ off, label = "Line $(off)")
    end
    set_theme!()
    fig
end

前几个例子中, 只循环利用了颜色, 没有循环利用线型、粗细等因素。 可以在set_theme!中中定义cycle参数, 要求哪些因素参与这种循环利用。 比如,cycle = Cycle([:color, :linestyle])规定先循环利用所有的颜色, 然后再使用下一种线型。

set_theme!palette参数, 除了可以用color指定一个调色盘, 还可以用marker指定可选的散点形状, 用linestyle指定可选的线型。

循环颜色与线型的程序如:

let
    x = 1:10;  y = 2 .* x
    set_theme!(palette = (; color = categorical_colors(:Accent_3, 3)),
        Lines = (; cycle = Cycle([:color, :linestyle])))
    fig = Figure()
    ax = Axis(fig[1,1])
    for off in 1:10
        lines!(ax, x, y .+ off, label = "Line $(off)")
    end
    set_theme!()
    fig
end

对于散点图, 可重复利用的包括颜色、散点形状, 在set_theme!中写法如Scatter = (; cycle = Cycle([:color, :marker]))

这样的写法是将颜色轮流用一遍后才修改第二种元素, 还可以让这些元素并行地循环使用,这时在Cycle()中加covary=true选项, 如:

let
    x = 1:10;  y = 2 .* x
    set_theme!(palette = (; color = categorical_colors(:Accent_3, 3)),
        Lines = (; cycle = Cycle([:color, :linestyle], covary=true)))
    fig = Figure()
    ax = Axis(fig[1,1])
    for off in 1:10
        lines!(ax, x, y .+ off, label = "Line $(off)")
    end
    set_theme!()
    fig
end

12.4.5 坐标轴设置

12.4.5.1 标题

Axis()函数中可以用title设置标题, 用subtitle设置子标题(在标题下面一行), 用xlabel设置x轴标签, 用ylabel设置y轴标签。

可以用titlealign指定标题和小标题的对齐方式, 如:left, :right。 可以用titlecolor指定标题颜色, 用titlefont指定标题字体, 用titlesize指定标题字体大小(像素)。 小标题也可以使用用类似选项。 可以用titlegap指定标题与小标题之间的间隙(像素)。

如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "简单的折线图示例",
    subtitle = "使用Axis()设置",
    titlesize = 30, subtitlesize = 15,
    titlecolor=:green, subtitlecolor=:pink)
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, [0, 10])
display(fig)

12.4.5.2 坐标轴范围

可以用xlims!()设置x轴范围,如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "简单的折线图示例",
    xlabel = "x",
    ylabel = "y")
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, [0, 10])
display(fig)

也可以写成xlims!(ax, 0, 10)。 如果写成xlims!(ax, 10, 0),则x轴会逆转过来:

xlims!(ax, 10, 0)
display(fig)

对y轴可以类似地使用ylims!()调整范围。 对x、y可以同时设置limits!(ax, x1, x2, y1, y2)

如果范围下限或者上限设置为nothing则自动确定。 如xlims!(ax, 0, nothing)自动设置上限。 也可以用关键字low指定下限, 关键字high指定上限。

可以在Axis()中指定x轴和y轴的上下限如Axis(fig[1,1], limits=(;x1,x2,y1,y2)), 其中的x1, x2, y1, y2都可以取nothing表示自动获取。如:

fig = Figure()
ax = Axis(fig[1,1], limits = (nothing, 10, nothing, nothing))
lines!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

12.4.5.3 刻度

Axis()中用xticks设置x轴刻度, 用yticks设置y轴刻度。

直接设置刻度线所在数值组成的向量,如:

fig = Figure()
ax = Axis(fig[1,1], xticks = 0:2:10)
lines!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

并没有达到我们的目的。 这是因为坐标轴刻度有一些内部算法, 程序输入的要求仅作为目标。 配合xlims!

fig = Figure()
ax = Axis(fig[1,1], xticks = 0:2:10)
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, 0, 10)
display(fig)

xticks的值也可以写成[0, 2, 4, 6, 8, 10]这样的向量值。 也可以指定标签:

fig = Figure()
ax = Axis(fig[1,1], xticks = (0:2:6, ["t0", "t2", "t4", "t6"]))
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, 0, 6)
display(fig)

可以用WilkinsonTicks(n)表示有n个刻度, 如:

fig = Figure()
ax = Axis(fig[1,1], xticks = WilkinsonTicks(4))
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, 0, 6)
display(fig)

因为常用各种等间隔刻度, 所以提供了MultiplesTicks()函数用来指定特殊间隔的刻度,如:

let
    da = DataFrame(x = 0:0.1:(4*pi))
    transform!(da, :x => ByRow(sin) => :y)
    fig = Figure()
    ax = Axis(fig[1,1], xticks = MultiplesTicks(5, pi, "π"))
    lines!(ax, da[:,:x], da[:,:y])
    fig
end

可以在Axis()中用xtickformat参数指定一个函数, 该函数输入刻度值向量, 返回显示字符串向量。 ytickformat类似。 如:

fig = Figure()
ax = Axis(fig[1,1], xtickformat = (s -> string.(s) .* "E3"))
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, 0, 6)
display(fig)

xtickformat还可以指定一个格式字符串,其中可以用如{:.2f}这样的方法指定一个刻度值的输出格式, 也可以有其他原样内容。如:

fig = Figure()
ax = Axis(fig[1,1], xtickformat = "x = {:.2f}")
lines!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

可以用xticklabelrotation指定一个数值使得x轴刻度值逆时针旋转指定的弧度数。 如:

fig = Figure()
ax = Axis(fig[1,1], xtickformat = "x = {:.2f}",
    xticklabelrotation = pi/6)
lines!(ax, dline01[:,:x], dline01[:,:y])
display(fig)

12.4.5.4 网格线

二维图一般只在下方显示x轴, 左侧显示y轴。 可以在Axis()中加xticksmirrored=true使得上侧也显示刻度线, 用xgridvisible=true使得x轴刻度包括网格线, 还可以加xminorticksvisible=true显示细刻度, 加xminorgridvisible=true显示细刻度网格线。 需要细刻度时,指定xminorticks=IntervalsBetween(n)表示两个粗刻度之间用细刻度等分为n段。 y轴类似。

fig = Figure()
ax = Axis(fig[1,1],  
    xticksmirrored = true,
    yticksmirrored = true, 
    xgridvisible=true,
    ygridvisible=true)
lines!(ax, dline01[:,:x], dline01[:,:y])
xlims!(ax, 0, 6)
display(fig)

对某个坐标系统ax, 可以用hidespines!(ax)令其不显示坐标轴。 可以用hidexdecorations!(ax)令其不显示刻度线、刻度值、网格线。

12.4.5.5 对数坐标轴

Axis()中设yscale = log10可以对y轴使用对数轴。x轴类似。 可取的值包括identity(缺省值), log10, log2, log, sqrt, Makie.logit

12.4.5.6 宽高比

坐标系统属性aspect用来控制宽高比。 如果不控制宽高比, 正圆可能会变成椭圆:

let
    th = 0:0.1:(2*pi)
    x = cos.(th)
    x = [x; x[1]]
    y = sin.(th)
    y = [y; y[1]]
    da = DataFrame(x=x, y=y)
    lines(da[:,:x], da[:,:y])
end

用坐标系统选项aspect=1控制宽高比为\(1:1\)

let
    th = 0:0.1:(2*pi)
    x = cos.(th)
    x = [x; x[1]]
    y = sin.(th)
    y = [y; y[1]]
    da = DataFrame(x=x, y=y)
    lines(da[:,:x], da[:,:y],
        axis = (; aspect = 1))
end

12.4.5.7 第二纵轴

可以用xaxisposition=:top将x轴画在上方, 用yaxisposition=:right将y轴画在右侧。

可以将两个坐标系统指定在绘图板的同一个格子中, 然后隐藏第二个坐标系统的坐标轴和x轴刻度、标签,设置y轴在右侧, 在两个坐标系统中分别制作一个图层, 就可以实现左右双侧纵轴。

12.4.6 内部图例

在具体制作图形的函数(如lines())中用label选项为某一层图形指定标签, 然后用axislegend()制作某一坐标系统的图例。 如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图和折线图叠加示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y],
    label = "数据集A")
lines!(ax, dline02[:,:x], dline02[:,:y], color = :red,
    label = "数据集B")
axislegend(ax)
display(fig)

注意axislegend()的函数名没有叹号。 可以不输入所针对的坐标系统, 这时针对最新生成的一个坐标系统。

marker可以修改散点形状, 取值如:circle, :rect, :utriangle, :dtriangle, :diamond, :pentagon, :cross, :xcross等。

linestyle可以修改线型,取值如nothing(实线), :dash, :dot, :dashdot, :dashdotdot等。

如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图和折线图叠加示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y],
    marker = :utriangle, label = "数据集A")
lines!(ax, dline02[:,:x], dline02[:,:y], 
    color = :red, linestyle = :dot, label = "数据集B")
axislegend(ax)
display(fig)

axislegend()可以加merge=true, 使得关于同一组数据的点、线图例合并, 可以用nbanks指定分栏数, 用labelsize指定标签文字大小像素数。

12.4.7 简单布局

Makie支持将画布灵活地分割为多个小块, 在每一小块上分别设定坐标系统作图。 这些小块可以是等大小的,如\(1 \times 2\), \(2 \times 1\), \(2 \times 2\), 也可以利用相邻格子合并的方法使各个小块占据不同大小。 还可以用命令调整某列或某行的宽度。

Axis()中输入绘图位置时, 默认使用第[1,1]块, 可以不分割小块。 这里位置可用如[1,2], [2,1], [2, 1:2], 使用范围时表示对应该范围的小块合并使用。如:

fig = Figure()
ax1 = Axis(fig[1, 1:2],
    xlabel="height", ylabel="weight")
ax2 = Axis(fig[1, 3],
    xlabel="age", ylabel="weight")
ax3 = Axis(fig[2, 1:3],
    xlabel="height")
fig

上面的结果就是用了\(2 \times 2\)布局, 然后将第二行的左右两格合并为一个小图。 分别在三个小图中作图:

scatter!(ax1, dclass[:, :height], dclass[:, :weight])
scatter!(ax2, dclass[:,:age], dclass[:,:weight])
hist!(ax3, dclass[:, :height])
display(fig)

有多个小图时, 如果小图之间的x变量, y变量相同, 可能需要对应的坐标轴使用相同的坐标范围。 用linkxaxis!(ax1, ax2)可以使两个小图的x坐标轴联系起来, y轴类似。

12.4.8 外部图例

可以指定外部的图例, 位置设定与指定每个小图位置类似。 内容由不同图层的label参数决定。 如:

fig = Figure()
ax = Axis(fig[1,1],
    title = "散点图和折线图叠加示例",
    xlabel = "x",
    ylabel = "y")
scatter!(ax, dline01[:,:x], dline01[:,:y],
    marker = :utriangle, label = "数据集A")
lines!(ax, dline02[:,:x], dline02[:,:y], 
    color = :red, linestyle = :dot, label = "数据集B")
Legend(fig[1,2], ax)
display(fig)

12.4.9 颜色代码条

热力图之类的图形用不同颜色代表z坐标值大小, 一般应该附上一个将颜色与数值参照对应的颜色条作为说明。 使用函数Colorbar(),摆放位置也与指定小图位置类似。

如:

let
    xs = range(-pi, pi, 100)
    ys = range(-pi, pi, 100)
    z = [exp(-0.1*(x^2 + y^2))*(cos(x) + sin(y)) 
        for x in xs, y in ys]
    fig = Figure()
    ax = Axis(fig[1,1])
    p = contourf!(ax, z)
    Colorbar(fig[1,2], p)
    display(fig)
end

12.4.10 使用LaTeX公式

可以在标题、轴标签等允许使用字符串的地方使用LaTeX公式。 写法为L"公式"。 如:

let
    x = 0:0.01:4
    f = x -> exp(-0.2*x) * cos(2*pi*x)
    y = f.(x)
    fig, ax, plt = lines(x, y, 
        label = L"e^{-0.2 x} \cos(2 \pi x)")
    text!(L"f(x) = e^x; \quad  g(x) = \cos x", position = (1.5, 0.9))
    axislegend()
    fig
end

12.5 GridLayout与嵌套布局

在前面的例子中, 如果绘图板为fig, 可以直接用fig[1,1], fig[1,2], fig[2,1:2]这样的方法规定小图的分割。 这样的方法比较受限, 除了可以左右合并、上下合并的灵活性以外, 基本都是对齐的, 但有些布局不希望完全对齐。

为此, 在绘图板(用Figure()生成)和坐标系统(用Axis()生成)之间, 再额外添加一个虚拟的布局容器(用GridLayout()生成), 这就增加了自由度, 布局容器是可以嵌套的。

12.5.1 实例

生成绘图板和嵌套的摆放容器:

fig = Figure(resolution = (1000, 700))
ga = fig[1,1] = GridLayout()
gb = fig[2,1] = GridLayout()
gcd = fig[1:2, 2] = GridLayout() # 其中再嵌入方格摆放层
gc = gcd[1,1] = GridLayout()
gd = gcd[2,1] = GridLayout();

在ga给出的摆放容器内, 用基本的\(2 \times 2\)分割方法做出三个小图:

axa1 = Axis(ga[1,1])
axa2 = Axis(ga[2,1])
axa3 = Axis(ga[2,2])

let
    sex, n1 = freq(dht[:, :sex])
    barplot!(axa1, sex, n1)
    
    cp, n2 = freq(dht[:, :cp])
    barplot!(axa2, cp, n2)

    restecg, n3 = freq(dht[:, :restecg])
    barplot!(axa3, restecg, n3)
end
fig

在gb给出的摆放容器中, 作盒形图:

axb = Axis(gb[1,1])
boxplot!(axb, dht[:,:sex], dht[:,:thalach])
fig

在gc中作散点图:

axc = Axis(gc[1,1])
scatter!(axc, dht[:,:age], dht[:,:thalach])
fig

在gd中作直方图:

axd = Axis(gd[1,1])
hist!(axd, dht[:,:thalach])
fig

这样的结果还是a, b, c, d四块左右上下对齐的\(2 \times 2\)形状。 但是, 按照初始的设计,右侧的两个图实际上是合并在一起后又嵌套地分开的, 所有右侧的两个图的相对大小是可以独立于左侧的两个图调整的; 左侧两图和右侧两图的宽度也是可以调整的。

调整左右两栏的比例,调整右侧上下两块的比例:

colsize!(fig.layout, 1, Auto(0.5))
rowsize!(gcd, 1, Auto(1.5))
fig

这个例子还需要添加图例,对坐标轴、小图间隙进行调整。 Makie的GridLayout有很强的对齐调整功能。 详见Makie手册。

12.5.2 大小调整

可以指定某列小图的宽度或者某行小图的高度, 单位为像素。如:

colsize!(fig.layout, 1, Fixed(200))

rowsize!类似。

可以指定某列小图的宽度占总宽度的比例, 没有指定的列自动分配,如:

colsize!(fig.layout, 1, Relative(1/3))

如果指定colsize!(fig.layout, 1, Auto()), 则第一列小图宽度按其内容能容纳为限自动调整。 用Auto(1), Auto(2)等值指定一个建议比例, 在确定宽度时有一系列优先级的判断和计算。

还可以用colsize!(fig.layout, 1, Aspect(1, 1))表示第一列的宽度等于第一行的高度, 如果是colsize!(fig.layout, 3, Aspect(1, 2)), 则表示第三列的宽度等于第一行高度的二倍。

12.5.3 间隙

colgap!(fig.layout, 1, Relative(0.1))

可以在第一列小图与第二列小图之间增加间隙, 宽度为绘图板总宽度10%。 可以用其它的宽度单位。 rowgap!函数类似。

如果不指定那一列或那一行, 就是所有列或者所有行之间, 如:

rowgap!(fig.layout, 10)

对所有小图行之间都增加10个像素间隙。

12.5.4 使用突出部分

在坐标轴组成的方框内部是实际图形内容, 外部可以有标题、刻度线、刻度值、轴标签等内容, 外部的这些区域称为突出部分(protrusion)。 可以在突出部分添加内容, 使用方法类似于Figure(1,2)这样的布局位置设定, 但这时改为Figure(1, 2, 突出位置)这样的写法, 突出位置如Left(), Top(), TopLeft()等。 用Label()在这样的突出位置添加文字内容。 如:

fig = Figure()
ax1 = Axis(fig[1,1])
ax2 = Axis(fig[1,2])
scatter!(ax1, dht[:, :thalach], dht[:,:trestbps])
Label(fig[1,1,TopLeft()], "(a)")
Label(fig[1,1,Right()], "血压对最大心律", rotation = pi/2)
boxplot!(ax2, fill(1, nrow(dht)), dht[:,:trestbps])
Label(fig[1,2,TopLeft()], "(b)")
fig

12.5.5 对齐

每一个小图对应一个坐标系统, 上下的小图、左右的小图如何对齐可以用选项设置。 默认是按坐标轴方框对齐的。 可以在Axis()中用alignmode指定对齐方式, 缺省为Inside()。 如果用Outside(), 就会用包括刻度、标题、轴标签的无形方框对齐。 还可以用Outside(10), 其中10表示包括刻度、标题、轴标签的无形方框再向外扩充10个像素空白后才对齐。 一般情况下应该使用缺省设置。

12.6 各种图形函数参考

12.6.1 scatter

scatter主要使用colormarkermarkersize进行调整。 输入的数据可以是scatter(xs, ys), 即输入两个向量分别表示x坐标和y坐标, 也可以输入一个向量的向量或元组的向量, 其中每个元素代表一对x, y坐标。 这种做法如:

let
    xs = 0:0.1:2*pi
    ys = sin.(xs)
    points = collect(zip(xs, ys))
    scatter(points)
end

也可以用函数Point2f.(xs, ys)将两个向量转换为成对坐标的向量的形式, 此函数将一对x, y值转换成Point类型。如:

let
    xs = 0:0.1:2*pi
    ys = sin.(xs)
    points = Point2f.(xs,ys)
    scatter(points)
end

允许为每个散点指定符号、颜色、大小, 只要为marker, color, markersize输入一个向量。 指定多个颜色时, 用color输入一个颜色序号序列, 然后用colormap指定一个调色盘进行颜色映射。如:

let
    xs = 0:0.1:2*pi
    ys = sin.(xs)
    points = Point2f.(xs,ys)
    scatter(points, color=1:length(points), colormap=:thermal)
end

同一坐标系统的不同图层将自动获得不同的颜色, 可以用label参数指定图例文字, 用axislegend()绘制图例:

let
    fig = Figure()
    ax = Axis(fig[1,1])
    scatter!(ax, rand(10), rand(10), label="数据1")
    scatter!(ax, rand(10), rand(10), label="数据2")
    axislegend(ax)
    fig
end

12.6.1.1 散点符号参考

Makie的散点符号来自TeX Gyre Heros Makie字体, 可以直接使用unicode字符, 也有一些符号可以用Symbol类型访问。

许多中文输入法有输入字符的功能。 下面的例子展示了许多特殊字符:

let
    marker_str = "☆○◇□△▽◁▷♡★●◆■▲▼◀▶↖↑↗←→↙↓↘√×❤⊙⊕※▬〓§Ψ☢"
    markers = collect(marker_str)
    fig = Figure()
    ax = Axis(fig[1, 1], yreversed = true,
        xautolimitmargin = (0.15, 0.15),
        yautolimitmargin = (0.15, 0.15)
    )
    hidedecorations!(ax)

    for (i, marker) in enumerate(markers)
        p = Point2f(fldmod1(i, 6)...)

        scatter!(p, marker = marker, markersize = 20, color = :black)
    end

    fig
end

使用如:

scatter(rand(10), rand(10), marker='■', markersize=20)

注意要使用字符'■', 而不是字符串"■"

下面的例子给出了多个符号名表示的散点符号:

using CairoMakie

markers_labels = [
    (:rect, ":rect"),
    (:star5, ":star5"),
    (:diamond, ":diamond"),
    (:hexagon, ":hexagon"),
    (:cross, ":cross"),
    (:xcross, ":xcross"),
    (:utriangle, ":utriangle"),
    (:dtriangle, ":dtriangle"),
    (:ltriangle, ":ltriangle"),
    (:rtriangle, ":rtriangle"),
    (:pentagon, ":pentagon"),
    (:star4, ":star4"),
    (:star8, ":star8"),
    (:vline, ":vline"),
    (:hline, ":hline"),
    (:x, ":x"),
    (:+, ":+"),
    (:circle, ":circle"),
    ('a', "'a'"),
    ('B', "'B'"),
    ('↑', "'\\uparrow'"),
    ('😄', "'\\:smile:'"),
    ('✈', "'\\:airplane:'"),
]

f = Figure()
ax = Axis(f[1, 1], yreversed = true,
    xautolimitmargin = (0.15, 0.15),
    yautolimitmargin = (0.15, 0.15)
)
hidedecorations!(ax)

for (i, (marker, label)) in enumerate(markers_labels)
    p = Point2f(fldmod1(i, 6)...)

    scatter!(p, marker = marker, markersize = 20, color = :black)
    text!(p, text = label, color = :gray70, offset = (0, 20),
        align = (:center, :bottom))
end

f

用法如:

scatter(rand(10), rand(10), marker=:rect, markersize=20)

也可以将marker参数输入为一个向量, 给每个散点分别指定符号; 可以将markersize参数输入为一个向量, 给每个散点分别指定大小(单位是像素)。

12.6.2 lines

lines输入一对x向量和y向量, 或输入点的向量, 每个点为Point类型或者取有两个元素的数组或元组。 可以用colorlinestylelinewidth进行调整。 linewidth指定粗细, 用像素单位。 linestyle可取值为:

  • nothing: 实线,这是缺省值;
  • :dash: 短划虚线;
  • :dot: 点虚线;
  • :dashdot: 点划线;
  • :dashdotdot:两点短划虚线;
  • 一个数值向量,表示循环的短划长度比例。

12.6.3 heatmap

输入形式是向量x, 向量y, 矩阵z, 这时z[i,j] = f(x[i], y[j])

也可以输入三个向量x, y, z, 这时z[i] = f(x[i], y[i])

函数目前有BUG。

let
    x, y, z = surfd()
    heatmap(x, y, z)
end

12.7 AlgebraOfGraphics包

为了获得定制的高质量图形, 可以使用Makie的各种定制方法。 这些方法功能强大, 但是要学习的内容比较繁琐。 AlgebraOfGraphics包是与R语言的ggplot2包类似理念的作图包, 以Makie为基础, 但使用更方便, 更容易理解。 AlgebraOfGraphics利用了“图形的代数运算”的思想, 将作图分为若干个可以组合步骤, 形成若干图层。 支持直接使用数据框。

AlgebraOfGraphics的主要元素有:

  • data, 引入数据框;
  • mapping,将数据框的变量与坐标、颜色、形状等绘图的数值维度建立映射关系;
  • visual, 规定与数据无关的一些作图信息, 比如统一的颜色,透明度,大小等;
  • 分析,可以用一些简单的表达方法对数据进行变换后再用来作图。

这些元素可以用*+等代数运算组合在一起构成最后的图形结果。

参考:

using AlgebraOfGraphics;
using CairoMakie

12.7.1 简单演示

12.7.1.1 分组变量条形图

set_aog_theme!()
p1 = data(dht) * mapping(:cp) * frequency()
draw(p1, axis = (width = 200, height = 400))

变量cp为胸痛类型:

  1. 典型心绞痛
  2. 非典型心绞痛
  3. 非心绞痛类型
  4. 无症状

AlgebraOfGraphics包用*号连接互相补充的设定。

如果希望将图形输出为PNG文件,可将上述程序最后一行改成:

fig = draw(p1; axis = (width = 200, height = 400))
save("barplot-ex.png", fig, px_per_unit=3)

其中关键字参数px_per_unit用来指定一个放大倍数, 可以省略。

可以将条形按性别分段, 用不同颜色代表性别:

p1b = p1 *
    mapping(color = :sex => nonnumeric, stack = :sex => nonnumeric)
draw(p1b, axis = (width = 200, height = 400))

颜色、分组需要使用类别变量。 0表示女性,1表示男性。

并列格式的条形图:

p1c = p1 *
    mapping(color = :sex => nonnumeric, 
    dodge = :sex => nonnumeric)
draw(p1c, axis = (width = 200, height = 400))

12.7.1.2 散点图

最大心律对年龄的散点图:

p1 = data(dht) * mapping(:age, :thalach)
draw(p1)

AlgebraOfGraphics用用+使两个图层, 两个图层仅共享同一坐标系。 在散点图上面再添加一个拟合线图层:

p1b = p1 + p1 * linear()
draw(p1b)

增加一个颜色(color)维度, 可以用不同颜色区分性别:

p1c = p1 * mapping(color = :sex => nonnumeric)
draw(p1c)

在没有明确的分组(group)维度时, 颜色维度还起到了分组的作用。

按颜色分组的散点图图层和按颜色分组的拟合线图层:

p1d = p1 * mapping(color = :sex => nonnumeric) +
    p1 * linear() * mapping(color = :sex => nonnumeric)
draw(p1d, axis = (width = 400, height = 300))

男女分别在一个切片(小图)中作图:

p1e = p1 * mapping(layout = :sex => nonnumeric)
draw(p1e, axis = (width = 300, height = 300))

p1f = (p1 + p1 * linear()) * 
    mapping(layout = :sex => nonnumeric)
draw(p1f, axis = (width = 300, height = 300))

12.7.1.3 二元密度图

考虑年龄和最大心律的二元分布密度, 可以用热力图表示。

p2 = data(dht) * mapping(:age, :thalach) * 
    AlgebraOfGraphics.density()
draw(p2, axis = (width = 300, height = 300))

作成等高线图:

p2b = p2 * visual(Contour)
draw(p2b, axis = (width = 300, height = 300))

用加号增加散点图层和拟合线图层:

p2c = p1 +
    p1 * linear() +
    p2 * visual(Contour)
draw(p2c, axis = (width = 300, height = 300))

12.7.1.4 其它维度

mapping()可以用位置参数输入条形图的变量, 输入散点图的横纵坐标变量, 其它的维度用关键字参数如:

  • color 颜色
  • marker 散点图案
  • group 分组
  • layout 切片分组
  • row, col: 分组小图。

12.7.2 保存为图像文件

设某个draw()的作图结果保存为变量fig, 则与Makie保存图像文件一样, 用

save(fname, fig)

可以保存为PNG格式, 其中fname是以.png.PNG结尾的文件名, 如"testsave.png"。 可以用关键字参数px_per_unit指定一个放大倍数。

12.7.3 data函数

作图用的原始数据用data(df)指定, 其中df是数据框, 也可以是Tables.jl格式兼容的其它表格数据。 注意需要区分数值型变量和分类变量, 分类变量需要转换为分类变量(CategoricalArray)格式。

另一种常用的df类型是有名元组, 它可以比数据框更一般, 比如, 一个元素为矩阵, 另一个元素为向量。

例如:

x = collect(range(0, 2π, 100))
y = sin.(x)
p1 = data((; x=x, y=y)) * mapping(:x, :y) * visual(Lines)
draw(p1)

12.7.4 mapping函数

映射函数mapping()用来指定数据框中的变量如何与图形中表示数值的坐标、颜色、散点形状、散点大小等联系起来。 此函数有x, y, z位置参数, 映射到x轴坐标、y轴坐标和z轴坐标。 颜色等图形维度用mapping()的关键字参数输入。 这些维度有些仅允许对应到离散取值变量, 有些允许对应离散取值变量也允许连续取值变量。 如果映射中存在分类变量(如为某分类变量不同类别指定不同颜色), 就会将其它变量也按此变量分组。 也可以直接映射一个group维, 用来明确地分组处理。

在映射变量时,可以用:变量名 => "显示名"设置图形显示时的变量名,如:

p1 = data(dht) * mapping(:age => "年龄", :thalach => "血压")
draw(p1, axis = (width = 300, height = 300))

还可以用:变量名 => 变换函数 => "新变量名"的方式指定变量的变换, 映射变换后的新变量,如:

p1 = data(dclass) * mapping(:height => (x -> x / 100) => "身高", :weight => "体重")
draw(p1, axis = (width = 300, height = 300))

注意其中的t -> t / 100没有写成广播形式, 这是AlgebraOfGraphics自动进行了逐行变换。 也可以用DataFramesMeta.transform!函数预先定义新变量, 此函数在指定变换时需要用加点的广播方式表示向量运算:

dc = copy(dclass)
DataFramesMeta.transform!(dc, 
  :height => (x -> x ./ 100) => "身高(米)")
p2 = data(dc) * mapping("身高(米)", :weight => "体重")
draw(p2, axis = (width = 300, height = 300))

12.7.4.1 简化的变换

有一些比较常用的变换, AlgebraOfGraphics包提供了相应的函数。

对分类变量, renamer()变换可以在mapping()中临时指定不同的标签值, 如:

p1 = data(dclass) * frequency() * mapping(
    :sex => renamer("F" => "女", "M" => "男") => "性别")
draw(p1, axis = (width = 200, height = 400))

分类变量的类别次序可以用sorter([新次序])mapping中临时修改次序,如:

p2 = data(dclass) * frequency() * mapping(
    :sex => sorter(["M", "F"]))
draw(p2, axis = (width = 200, height = 400))

需要将数值型变量作为分类变量使用时, 可以用nonnumeric函数在mapping中临时修改其用途, 如:

p3 = data(dclass) * frequency() * 
    mapping(:age => nonnumeric)
draw(p3, axis = (width = 200, height = 400))

注意上面的nonnumeric不能写成nonnumeric()

另外, 在mappings()中可以用:变量名 => verbatim指定该变量原样使用, 不进行任何映射(变换), 可以用来在坐标系中指定坐标位置添加文本内容, 也可以输入适当的颜色名直接指定颜色。

12.7.5 visual函数

visual函数用来指定与数据无关的一些图形设定, 如统一的颜色、字体大小、透明度。

可以用visual指定要制作的图形类型,如:

  • visual(Scatter): 散点图;
  • visual(BarPlot): 条形图。
  • visual(Line): 折线图,等等。

图形类型可以使用Makie支持的图形类型, 类型名使用首字母大写的“骆驼式”命名, 比如Makie中作带有散点的折线图的函数是scatterline, 相应的类型就写成ScatterLine

一些mapping()有默认的图形类型, 可以省略用visual()指定图形类型的步骤。 比如,mapping()中仅指定一个分类变量的位置参数, 就会自动进行频数统计并作频数条形图。

因为同样的映射可以做不同的图形, 所以可以先用data()输入数据框, 用mapping()输入映射, 在实际绘图时才添加visual()设定,如:

pv1 = data(dline01) * mapping(:x, :y)
draw(pv1 * visual(Scatter, color=:blue), axis=(width=300, height=300))

draw(pv1 * visual(Lines), axis=(width=300, height=300))

draw(pv1 * visual(ScatterLines), axis=(width=300, height=300))

12.7.6 小图

可以将变量映射到col, 结果制作小图, 小图根据指定的变量的值按列摆放。

如:

pdm1 = data(dclass) * mapping(:height, :weight)
pf1 = mapping(col = :sex)
draw(pdm1 * pf1)

也可以将变量映射到row, 使得该变量每个值映射到一个小图, 按行摆放,如:

pdm1 = data(dclass) * mapping(:height, :weight)
pf1 = mapping(row = :sex)
draw(pdm1 * pf1)

可以同时使用colrow映射, 根据两个变量来区分小图。 这样的小图摆放方式称为GridLayout(格子摆放)。 为此,制作一个简单的样例数据:

dlay01 = DataFrame(f1 = categorical(rand(["a", "b"], 100)), 
    f2 = rand(["c", "d"], 100),
    x0 = randn(100), y0 = randn(100))
transform!(dlay01, [:y0, :f1] => ByRow((x, f) -> f == "a" ? x + 1 : x + 4) => :y,
    [:x0, :f2] => ByRow((x, f) -> f == "c" ? x + 10 : x + 20) => :x)

100 rows × 6 columns

f1 f2 x0 y0 y x
Cat… String Float64 Float64 Float64 Float64
1 b d -0.942355 -0.192019 3.80798 19.0576
2 b c 0.186319 -0.34768 3.65232 10.1863
3 b d -1.19326 0.338637 4.33864 18.8067
4 a c -0.327787 0.519947 1.51995 9.67221
5 b d -0.736707 -0.106367 3.89363 19.2633
6 b d -1.92288 -0.582359 3.41764 18.0771
7 a d 1.71238 0.0491358 1.04914 21.7124
8 b c -1.08963 -0.924149 3.07585 8.91037
9 b c -1.63007 -0.682275 3.31772 8.36993
10 b c 0.289208 -1.46859 2.53141 10.2892
11 a c -0.659801 -0.195474 0.804526 9.3402
12 b c 0.471143 -0.377275 3.62273 10.4711
13 a c -0.635577 -2.26178 -1.26178 9.36442
14 b d 0.204938 0.178108 4.17811 20.2049
15 b d 0.247488 -1.41551 2.58449 20.2475
16 a c 0.22891 1.27275 2.27275 10.2289
17 b d -0.0311451 0.0476805 4.04768 19.9689
18 b d 0.224916 0.703304 4.7033 20.2249
19 b d 0.264228 -0.972238 3.02776 20.2642
20 b c -1.17171 0.0829377 4.08294 8.82829
21 a c -0.452948 -0.17994 0.82006 9.54705
22 b d 0.758738 0.391855 4.39185 20.7587
23 b d -0.173525 -0.529281 3.47072 19.8265
24 b d 0.506405 0.654003 4.654 20.5064
25 a c -1.8688 -0.0429526 0.957047 8.1312
26 b d 0.910461 -1.64598 2.35402 20.9105
27 b c 0.15299 1.56182 5.56182 10.153
28 a d 0.0741696 0.424776 1.42478 20.0742
29 a d 0.875093 0.664427 1.66443 20.8751
30 b c 0.191676 1.45628 5.45628 10.1917
pdm1 = data(dlay01) * mapping(:x, :y)
dlay1 = mapping(row=:f1, col=:f2)
draw(pdm1 * dlay1)

使用格子摆放小图时, 可以在draw()中加facet(切片,或小图)参数, 参数值为有名向量, 其中用linkxaxes选择各个小图的x轴是否采用一致的坐标范围。 默认取:maximal, 不仅上下对齐的x轴采用相同的范围, 实际上所有小图中的x轴都采用相同的范围, 而且仅画最下面一层的x轴, 如上图。

如果取:minimal, 这时上下对齐的x轴的范围是相同的, 仅画最下面一层的x轴, 但左右的x轴范围不同。如:

draw(pdm1 * dlay1, facet = (; linkxaxes = :minimal))

如果取linkxaxes = :none, 则每个小图分别画x轴且不使用相同的范围。如:

draw(pdm1 * dlay1, facet = (; linkxaxes = :none))

layout映射也起到区分小图的作用,这样的摆放方式称为自动换行(wrap)方式。 如:

pdm1 = data(dclass) * mapping(:height, :weight)
pf1 = mapping(layout = :sex)
draw(pdm1 * pf1)

col, rowlayout仅能映射到字符串变量或者字符串变量转换的类别变量。 对数值型, 可以借助nonnumeric转换,如:

p1 = data(dht) * mapping(:age, :thalach, layout = :cp => nonnumeric)
draw(p1)

12.7.7 分析

某些图形类型需要进行建模分析, 比如拟合直线, 作直方图需要分组统计频数,等等。 这些分析都有相应的函数。

12.7.7.1histogram函数作直方图

为了对连续取值变量作直方图, 可以调用histogram()函数。如:

p1 = data(dht) * mapping(:thalach) * histogram()
draw(p1)

按性别分段:

p1b = data(dht) * mapping(:thalach, 
    color=:sex => nonnumeric, stack=:sex => nonnumeric) * histogram()
draw(p1b)

按性别切片:

d1c = data(dht) * mapping(:thalach, 
    layout=:sex => nonnumeric) * histogram()
draw(d1c)

可以在histogram()中指定一些参数, 如bins指定分组数或具体分点。

12.7.7.2histogram函数作二元直方图

心脏病数据集中年龄与血压的二元直方图:

p1 = data(dht) * mapping(:age, :thalach) * histogram(bins=20)
draw(p1)

12.7.7.3density函数作密度估计曲线

对dht中的血压作密度估计曲线:

p1 = data(dht) * mapping(:thalach) * AlgebraOfGraphics.density()
draw(p1)

12.7.7.4density函数作二元密度估计图

心脏病人年龄与血压的二元密度估计热力图,同于二元直方图:

p1 = data(dht) * mapping(:age, :thalach) * AlgebraOfGraphics.density(npoints = 20)
draw(p1)

可见二元密度估计图默认使用了二元直方图。 制作三维曲面图形:

p1b = p1 * visual(Surface)
draw(p1b, axis=(type=Axis3, zticks=0:0.1:0.2, 
    limits=(nothing, nothing, (0, 0.001))))

曲面图最好使用GLMakie后端, 以获得交互功能。

12.7.7.5frequency函数作频数条形图

mapping中仅有一个分类变量作为位置参数时, 默认就是作频数条形图。 用frequency()可以对任何变量要求作这种图。 比如,dht中age是一个数值型变量,下面作不同年龄的频数条形图:

p1 = data(dht) * mapping(:age) * frequency()
draw(p1)

12.7.7.6 expectation函数

输入了多个位置参数后, 固定最后一个位置参数的值对前面的位置参数作图。

12.7.7.7linear画拟合回归直线

对体重和身高拟合回归直线, 作散点图并叠加回归直线。 回归直线自动伴随有置信区间。

p1 = data(dclass) * mapping(:height, :weight) *(
    visual(Scatter) +
    linear())
draw(p1)

这个例子用了乘法到加法的分配律。

12.7.7.8smooth拟合局部多项式曲线

对dclass的体重对身高散点图, 拟合局部多项式曲线:

p1 = data(dclass) * mapping(:height, :weight) *(
    visual(Scatter) +
    smooth(span=0.75) * visual(color=:red))
draw(p1)

smooth的参数span一般取为0到1之间的小数, 取值越大,得到的曲线越光滑。

12.7.7.9 文本图

指定x, y坐标和文字变量后, 可以在指定坐标显示文字内容。 文字变量需要是字符串类型。 如:

p1 = data(dclass) * 
    mapping(:name => verbatim, (:height, :weight) => Point) *
    visual(Annotations, textsize=8)
draw(p1)

注意verbatim的使用。这个变换说明前面的变量需要保持原来的值使用, 不作任何变换, 这里用来提供要标注的文本内容。

12.7.8 代数运算

AlgebraOfGraphics是“图形的代数”, 可以对单个图层或多个图层进行乘法(*)或加法(+)运算。 加法运算满足结合律, 基本满足交换律, 但加号后面的图形会覆盖在加号前面的图形上面, 所以交换后结果不完全相同。 乘法运算满足结合律。 加法与乘法之间满足乘法的右分配律, 左分配律则有一些折扣。

乘法一般用来递进地完成一个图层, 加法一般用来叠加两种不同的图形。 如果多个图层与多个图层相乘, 结果也是所有两两组合后两两相乘的结果, 即两个图层和两个图层相乘可以得到四个图层。

12.7.9draw绘制图形

制作好的图层或多个图层用draw或者draw!函数绘制。 draw自动制作图例, 而draw!允许后续修改,不自动制作图例, 可以用colorbar!legend!后续添加颜色条图例和一般图例。

draw()axis参数输入各种绘图设置, 如绘图板的宽度、长度,标题、轴标签、轴刻度等。

12.7.9.1 在draw中设置标题

p1 = data(dclass) * mapping(:height, :weight)
draw(p1, axis = (; width=400, height=300, title="体重对身高",
        xlabel="身高", ylabel = "体重"))

注意(; width=400, height=300)这样的写法是“有名元组”的比较规范的写法。 当只有一个元素时必须这样写, 多个元素时不是必须的, 但这样写的用意比较明确。

12.7.9.2 在draw中设置坐标轴

draw()axis参数中还可以规定许多关于坐标系统和坐标轴的设置。 如axis = (; aspect = 1.0)规定宽高比:

p1 = data(dclass) * mapping(:sex) * frequency()
draw(p1, axis=(; aspect = 1/3))

axis中用xticks指定x轴刻度数字序列, yticks指定y轴刻度数字序列, 如:

p2 = data(dht) * mapping(:age, :thalach)
draw(p2, axis = (; xticks = 30:20:70))

p3 = data(dht) * mapping(:age, :thalach)
draw(p3, axis = (; xticks = (30:20:70, ["三十", "五十", "七十"])))

function cp_recode(x)
    x = string.(Int.(x))
    x = categorical(x)
    recode!(x, "1" => "典型心绞痛", "2" => "非典型心绞痛",
        "3" => "非心绞痛", "4" => "无症状")
    x
end
dht2 = transform(dht, :cp => cp_recode => :cpc)
p4 = data(dht2) * mapping(:cpc, :thalach) *
    visual(BoxPlot)
draw(p4)

上面的程序对cp变量进行了比较复杂的处理。 这是因为目前CategoricalArrays对字符串转换为分类变量支持最完善, 虽然数值也可以直接转换为字符串, 但是与AlgebraOfGraphics接口不太顺畅。 所以,写了转换函数recode_cp, 先将cp原来的浮点型转换为整型, 再转换为字符串, 再转换为因子, 并对四个水平适当命名。 因为mapping()中的变量变换都是期望逐行进行的, 而转换为因子是整列进行的, 所以额外用了transform函数生成转换后的cpc变量, 而不是在mapping()中调用recode_cp

如果x轴本来是字符型的, 需要修改标签值, 可以在mapping()中用辅助的renamer()函数进行修改。

12.7.9.3 在draw中设置绘图板

可以用draw()figure选项输入一个有名元组, 在有名元组中填入背景色、大小等设置。 如draw(fig, figure = (; resolution = (1000, 800)))

例:

p1 = data(dht) * mapping(:thalach, layout = :sex => nonnumeric) *
    histogram()
draw(p1, figure = (; backgrouncolor=:gray80, figure_padding = 10, 
        resolution=(600, 300)))

其中figure_padding是整个绘图版周围边空宽度。

12.7.9.4 在draw中设置图例

绘图函数的图例是自动生成的, 可以在draw()函数中用legend参数指定一个有名元组进行图例的设置。 如:

p1 = data(dht) * mapping(:age, :thalach) * 
    mapping(color = :sex => renamer(0 => "女", 1 => "男") => "性别")
draw(p1)

上面关于不同性别使用不同颜色的图例自动标在右侧。 在draw()中用figure()进行一些修改:

draw(p1, legend = (; position = :top, titleposition = :left, 
        framevisible = true, padding = 5))

12.7.9.5 在draw中设置调色盘

在Makie中用colormap设置连续值映射到颜色的调色盘, 对于离散值如性别, 可以用color映射自动匹配颜色, 也允许在draw()中用palettes参数指定一个包含元素color的命名元组, 在color中人为指定颜色对应关系。 如:

function recode_sex(x)
    x = string.(Int.(x))
    x = categorical(x)
    recode!(x, "0" => "女", "1" => "男")
    x
end
dht2 = transform(dht, :sex => recode_sex => :sexc)
p1 = data(dht2) * mapping(:age, :thalach) * 
    mapping(color = :sexc => "性别" )
draw(p1, palettes = (; color = ["女" => :red, "男" => :blue]))

这里为了将原数据集dht中的性别(sex)转换为字符型来源的类别变量, 单独写了转换函数, 还用了transform函数, 而不是直接在mapping()函数中转换。 mapping()函数的转换有一些限制, 而且对数值转换为类别变量兼容性不好。

当需要用的颜色比较多时, 可以人为选择某种渐变色生成指定个数的颜色。 如cgrad(:cividis, 8, categorical=true):cividis调色盘生成8个渐变色。

12.7.9.6 在draw中设置渐变颜色条

渐变颜色条(colorbar)是将连续取值的变量映射为颜色时的一个对照图例。 可以在draw()中用colorbar参数输入一个有名元组用于设置渐变颜色条, 如position = :top可以放在顶部, size = 25规定宽度。 为了修改数值到颜色的映射调色盘, 可以在visual()函数中用colormap参数指定一个连续颜色调色盘, 如colormap = :thermal

例:

p1 = data(dht) * mapping(:age, :thalach) * 
    AlgebraOfGraphics.density(npoints = 20) * 
    visual(Heatmap, colormap = :heat)
draw(p1, colorbar = (; position = :top, size = 25))

其它常用调色盘如:thermal, :viridis

12.7.10 使用LaTeX标签

标题、坐标轴标签等可以使用LaTeX公式, 格式为L"公式",如L"\int_0^1 f(x)dx"

例:

let
    x = range(-2, 2, 100)
    y = x .^ 2
    d = DataFrame(x = x, y = y)
    p1 = data(d) * mapping(:x, :y) * visual(Lines)
    draw(p1, axis = (; title = L"Graph of $y = x^2$",
            xlabel="x", ylabel = L"x^2"))
end

12.7.11 Algebra与Makie的配合使用

可以在用Makie的Figure()生成绘图板后, 用draw!()在此绘图板的指定小块内作图。 如:

fig = Figure(resolution=(200,400))
p1 = data(dht) * mapping(:sex) * frequency()
draw!(fig[1,1], p1)
display(fig)

又如:

fig = Figure()
p1 = data(dht) * mapping(:sex) * frequency()
draw!(fig[1,1], p1)
p2 = data(dht) * mapping(:age) * histogram()
draw!(fig[1,2], p2)
colsize!(fig.layout, 1, Auto(0.5))
fig

两种作图函数混用:

fig = Figure()
p1 = data(dht) * mapping(:sex) * frequency()
draw!(fig[1,1], p1)
ax2 = Axis(fig[1,2])
scatter!(ax2, dht[:,:age], dht[:,:thalach])
colsize!(fig.layout, 1, Auto(0.5))
fig

draw!()的第一参数也可以取为Axis()的输出。 如:

fig = Figure()
ax1 = Axis(fig[1,1])
p1 = data(dht) * mapping(:sex) * frequency()
draw!(ax1, p1)
ax2 = Axis(fig[1,2])
scatter!(ax2, dht[:,:age], dht[:,:thalach])
colsize!(fig.layout, 1, Auto(0.5))
fig

12.8 AlgebraOfGraphics更多范例

这一节给出AlgebraOfGraphics包的更多比较完整的应用范例。

12.8.1 散点图和折线图

输入数据、映射是统一的, 可以将这两部分合并, 而visual(图形类型)用乘法与这两部分连接。

仅散点图:

dm1 = data(dline01) * mapping(:x, :y)
lay1 = visual(Scatter)
draw(dm1 * lay1)

仅折线:

dm1 = data(dline01) * mapping(:x, :y)
lay1 = visual(Lines)
draw(dm1 * lay1)

散点和折线两个图层,两个visual用加法连接:

dm1 = data(dline01) * mapping(:x, :y)
lay1 = visual(Scatter) + visual(Lines)
draw(dm1 * lay1)

也可以使用多个数据集。如两条折线的图形:

dp1 = data(dline01) * mapping(:x, :y) * visual(Lines)
dp2 = data(dline02) * mapping(:x, :y) * visual(Lines)
draw(dp1 + dp2)

12.8.2 盒形图等

盒形图:

dp1 = data(dht) * mapping(:cp, :thalach) * visual(BoxPlot)
draw(dp1; axis=(width=200, height=400))

dp1 = data(dht) * mapping(:cp, :thalach) * 
    visual(BoxPlot, show_notch=true)
draw(dp1; axis=(width=200, height=400))

Makie的盒形图需要用x轴为分组,y轴为作图的变量。 如果没有分组,就需要预先在数据集中制作一个仅有一类的分类变量或者数值变量。

小提琴图:

dp1 = data(dht) * mapping(:cp, :thalach) * visual(Violin)
draw(dp1; axis=(width=200, height=400))

增加一个二值分组维度的背对背小提琴图:

dp1 = data(dht) * mapping(:cp, :thalach, 
    color = :sex => nonnumeric, side = :sex => nonnumeric) * visual(Violin)
draw(dp1; axis=(width=200, height=400))

正态QQ图:

dp1 = data(dclass) * mapping(:height) * visual(QQNorm, qqline=:fit)
draw(dp1)

12.8.3 对数轴

draw()函数中可以指定对数坐标轴。 考虑如下数据:

dlog01 = DataFrame(x = 1:10, 
    y = [1, 1.5, 2, 3, 6, 9, 14, 20, 28, 30])
dm1 = data(dlog01) * mapping(:x, :y)
draw(dm1)

将y轴制作成对数轴:

draw(dm1, axis = (;yscale=log))

对数轴要慎用。 散点图、折线图可以放心地使用对数轴, 但如果在散点图上拟合直线或曲线, 对数轴就会造成扭曲, 因为不同于R的ggplot2, AlgebraOfGraphics的对数轴不是对变换后的数据拟合模型, 而是对原始数据拟合模型然后用对数轴画出来。 密度估计也有这样的问题。

12.8.4 多个变量同时作图

有时数据中有多个类似的变量。 如:

dn2 = DataFrame(x = rand(100), y = rand(100), z = rand(100))

100 rows × 3 columns

x y z
Float64 Float64 Float64
1 0.709204 0.809688 0.461517
2 0.256039 0.30611 0.503989
3 0.67271 0.812424 0.256249
4 0.871437 0.803494 0.223384
5 0.710344 0.779265 0.50994
6 0.0993146 0.723984 0.00352218
7 0.872053 0.317638 0.213957
8 0.763101 0.992335 0.79795
9 0.889567 0.699942 0.647065
10 0.0536789 0.226581 0.611227
11 0.23935 0.233382 0.991148
12 0.909953 0.236486 0.0794017
13 0.220036 0.0957219 0.699915
14 0.651666 0.721985 0.439505
15 0.382512 0.442361 0.794247
16 0.815084 0.298066 0.206512
17 0.349096 0.859199 0.0492409
18 0.236388 0.680518 0.355507
19 0.475883 0.404262 0.954434
20 0.714123 0.432249 0.892232
21 0.0124185 0.206874 0.118358
22 0.913992 0.656319 0.344226
23 0.429854 0.349274 0.845334
24 0.540799 0.843093 0.928278
25 0.388292 0.309083 0.971027
26 0.174892 0.0513927 0.272157
27 0.766642 0.31153 0.258802
28 0.646467 0.857145 0.872278
29 0.185689 0.301101 0.533727
30 0.773111 0.281102 0.282939

可以将三个变量组合为一个向量的向量, 然后进行统一处理。 这时, 可以用统一的dims(1)变量来指代这三个变量的区别。 如:

p1 = data(dn2) * mapping([:x, :y, :z] .=> "三个变量") *
    AlgebraOfGraphics.density() * 
    mapping(color = dims(1) => renamer(["x", "y", "z"]))
draw(p1)

注意因为是三个变量,所以用了.=>的写法而不是=>

p2 = data(dn2) * mapping(:x, [:y, :z] .=> "yz") *
    visual(Scatter) *
    mapping(color = dims(1) => renamer(["y", "z"]))
draw(p2)

这种dims()的还适用于多个变量与多个变量之间的图形。 这时既可以用dims(1),还可以用dims(2)。 如:

n = 100
dn3 = DataFrame(
    x1 = 1 .+ randn(n),
    x2 = 5 .+ randn(n),
    y1 = 10 .+ randn(n),
    y2 = 20 .+ randn(n))
p3 = data(dn3) * visual(Scatter) *
    mapping([:x1, :x2], [:y1 :y2], col = dims(1), row = dims(2))
draw(p3)

注意散点图的横坐标写成了[:x1, :x2], 纵坐标写成了[:y1 :y2], 这样运算结果构成一个\(2 \times 2\)矩阵, [:x1, :x2]是一个列向量, 所以用来区分结果矩阵的两行, 用dims(1)标识; [:y1 :y2]是一个行向量, 所以用来区分结果矩阵的两列, 用dims(2)标识。

在如上作多格图形时, 可以在draw()中用facet参数中的linxaxes指定左右的x坐标轴是否采用统一范围, linkyaxes指定上下的y坐标轴是否采用统一范围。 缺省为:none,用:all表示要求对齐。 用:x表示仅x轴。 如:

draw(p3, facet = (;linkxaxes = :all, linkyaxes = :all))

12.9 GLMakie

GLMakie是Makie的后端绘图引擎之一, 与CairoMakie相比, GLMakie长于交互和三维图形能力。 在Jupyter Notebook界面中, GLMakie的交互能力受限, 只能做出动态图形的一个静态版本。

12.9.1 交互能力

在命令行或MS VSCode这样的界面中, 会单独打开一个GLMakie图形窗口, 此窗口中的图形有较强的交互能力:

  • 鼠标滚轮可以用来缩放图形;
  • 拖选某一矩形区域可以聚焦显示此区域;
  • 三维图形可以拖动从不同角度查看。
using GLMakie
GLMakie.activate!()

12.9.2 三维散点图、折线图

需要用Axis3生成三维图需要的坐标系统。 可以用scatter!(ax, x, y, z)作三维散点图, 这种图中散点形状并不受坐标轴伸缩的影响。 如:

fig = Figure()
ax = Axis3(fig[1, 1]; aspect=(1, 1, 1), 
    perspectiveness=0.5,
    xlabel="年龄", ylabel="最大心率", zlabel="血压")
scatter!(ax, dht[:,:age], dht[:,:thalach], dht[:,:trestbps])
fig

还可以用meshscatter!作三维散点图, 其中的符号是真正的几何形体, 会随坐标轴伸缩而变形。 如:

fig = Figure()
ax = Axis3(fig[1, 1]; aspect=(1, 1, 1), 
    perspectiveness=0.5,
    xlabel="年龄", ylabel="最大心率", zlabel="血压")
meshscatter!(ax, dht[:,:age], dht[:,:thalach], dht[:,:trestbps],
    markersize = 1)
fig

可以在Axis3()中用aspect=:data使得散点符号不变形。

可以用lines!(ax, x, y, z)制作三维折线图。 可以用scatterlines!(ax, x, y, z)制作带有散点的三维折线图。 也可以将meshscatter!lines!制作两个重叠图层。

12.9.3 三维曲面的各种图形

以前面定义的三维曲面为例。 作染色的三维曲面图, 在命令行或VSCode中可以拖动查看:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis3(fig[1,1], aspect=(1,1,1))
    plt = surface!(ax, x, y, z)
    fig
end

制作网状线图:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis3(fig[1,1], aspect=(1,1,1))
    plt = wireframe!(ax, x, y, z)
    fig
end

制作三维等高线图,在命令行和VSCode中可拖动查看:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis3(fig[1,1], aspect=(1,1,1))
    plt = contour3d!(ax, x, y, z, levels=20)
    fig
end

使用二维的热力图表现:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis(fig[1,1], aspect=DataAspect())
    plt = heatmap!(ax, x, y, z)
    Colorbar(fig[1,2], plt)
    fig
end

使用二维的等高线图:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis(fig[1,1], aspect=DataAspect())
    plt = contour!(ax, x, y, z, levels=20)
    fig
end

使用有填充色的等高线图:

let
    x, y, z = surfd()
    fig = Figure(resolution=(800, 800))
    ax = Axis(fig[1,1], aspect=DataAspect())
    plt = contourf!(ax, x, y, z, levels=20)
    Colorbar(fig[1,2], plt, height=Relative(0.7))
    fig
end