-
Notifications
You must be signed in to change notification settings - Fork 0
/
visualization-graphics.qmd
697 lines (573 loc) · 21.2 KB
/
visualization-graphics.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
# graphics 入门 {#sec-basic-graphics}
```{r}
#| echo: false
source("_common.R")
```
不是把每个绘图函数都挨个讲一遍,也不是把它们统统归纳总结,而是比较深入地介绍一、两种图形,一、两个例子,重点阐述 Base R 的绘图特点,使用图形时,注意图形本身的作用,最终,希望读者能够达到举一反三的效果。
基础绘图系统。相比于 **ggplot2** 和 **lattice**,**graphics** 制作示意图是优势。
## 绘图基础 {#sec-graphics-elements}
利用点、线等基础元素从零开始绘图。
### `plot()` {#sec-elements-plot}
本节将主要基于鸢尾花数据集介绍 R 语言基础绘图系统,该数据集最早来自埃德加·安德森,后来,被罗纳德·费希尔在介绍判别分析的论文中用到,从而,流行于机器学习社区。鸢尾花是非常漂亮的一种花,在统计和机器学习社区家喻户晓,更别提在植物界的名声。其实,远不止于此,在绘画艺术界也是如雷贯耳,印象派大师文森特·梵高画了一系列鸢尾花作品。万紫千红,但能入画的不多,故而,鸢尾花更显高雅。在生命最后的一段日子里,梵高受精神病折磨,在法国普罗旺斯的圣·雷米医院里,唯有盛开的鸢尾花陪着他,最著名的《星月夜》就是在这时候创作出来的。下面先让我们一睹鸢尾花芳容,图片来自维基百科鸢尾花词条。
::: {#fig-iris layout-ncol="3" layout="[30,40,30]" layout-valign="bottom"}
![versicolor 杂色鸢尾](images/iris-versicolor.jpeg){#fig-iris-versicolor width="70%"}
![setosa 山鸢尾](images/iris-setosa.jpeg){#fig-iris-setosa width="80%"}
![virginica 弗吉尼亚鸢尾](images/iris-virginica.jpeg){#fig-iris-virginica width="70%"}
三种鸢尾花
:::
鸢尾花数据集已经打包在 R 软件中,而且默认已经加载到命名空间,下面用函数 `summary()` 查看其概况。
```{r}
summary(iris)
```
函数 `plot()` 采用公式语法可以快速作图。
```{r}
#| label: fig-iris-plot
#| fig-cap: 快速作图函数 `plot()`
#| fig-width: 4
#| fig-height: 4
#| fig-showtext: true
#| par: true
#| fig-subcap:
#| - 公式语法绘制散点图
#| - 带背景参考线的散点图
#| layout-ncol: 2
plot(Sepal.Length ~ Sepal.Width, data = iris)
plot(iris$Sepal.Width, iris$Sepal.Length, panel.first = grid())
```
函数 `plot()` 是一个泛型函数,传递不同类型的参数值会调用不同的绘图方法,而不同的绘图方法的参数是不同的。当采用公式语法绘图时,会自动调用函数 `plot.formula()` ,此时,参数 `panel.first` 就不起作用。当不使用公式语法时,会调用函数 `plot.default()` ,此时,参数 `panel.first` 就起作用,利用该参数可以添加背景参考线。
### 标签 {#sec-elements-label}
函数 `plot()` 的参数 `xlab` 、`ylab` 和 `main` 可以分别设置坐标轴横、纵标签和图主标题。
```{r}
#| label: fig-iris-label
#| fig-cap: 标签
#| fig-width: 5
#| fig-height: 5
#| fig-showtext: true
plot(
Sepal.Length ~ Sepal.Width, data = iris,
xlab = "Sepal Width", ylab = "Sepal Length",
main = "Edgar Anderson's Iris Data"
)
```
### 字体 {#sec-elements-fonts}
作图函数 `plot()` 和 `title()` 都有参数 `family` ,设置该参数可以调整图形中的字体。下 @fig-iris-fonts 的横纵坐标轴标签和图标题设为宋体,坐标轴刻度标签设为无衬线字体。
```{r}
#| label: fig-iris-fonts
#| fig-cap: 字体
#| fig-width: 5
#| fig-height: 5
#| fig-showtext: true
plot(Sepal.Length ~ Sepal.Width, data = iris, ann = FALSE, family = "sans")
title(
xlab = "萼片宽度", ylab = "萼片长度",
main = "埃德加·安德森的鸢尾花数据", family = "Noto Serif CJK SC"
)
```
### 分组 {#sec-elements-group}
分组有两种方式,其一按照数据中的分类变量分组,其二按照一定的规则分组。而图形表达的方式可以借助颜色或图形元素的样式。
函数 `plot()` 的参数 `col` 和 `pch` 都可以用来分组,前者通过颜色,后者通过点的类型。简单起见,将数据集 `iris` 中的 `Species` 列传递给参数 `col` ,实现不同种类的鸢尾花配以不同的颜色。
```{r}
#| label: fig-iris-group1
#| fig-cap: 分组
#| fig-width: 4
#| fig-height: 4
#| fig-showtext: true
#| par: true
plot(Sepal.Length ~ Sepal.Width, data = iris, col = Species, pch = 16)
```
下面采用一个简单规则将数据分成两组,将鸢尾花中 setosa 山毛榉类型且 Sepal.Length 萼片长度大于 5 厘米的分成一组,以红色填充散点代表这部分数据,与余下的散点形成对比,达到区分的目的。
```{r}
#| label: fig-iris-group2
#| fig-cap: 分组
#| fig-width: 4
#| fig-height: 4
#| fig-showtext: true
#| par: true
plot(Sepal.Length ~ Sepal.Width, data = iris)
points(Sepal.Length ~ Sepal.Width, data = iris,
col = "#EA4335", pch = 16,
subset = Species == "setosa" & Sepal.Length > 5
)
```
### 配色 {#sec-elements-color}
经过探查,知道数据集 `iris` 中的 `Species` 列有三种取值。调用函数 `palette()` 设置一个超过 3 种颜色的调色板可以实现自定义配色。首先来看看当前调色板的颜色。
```{r}
palette()
```
一共是 8 种颜色,效果预览见 @fig-graphics-palette 。
```{r}
#| label: fig-graphics-palette
#| fig-width: 3
#| fig-height: 3
#| fig-cap: 默认调色板
#| fig-showtext: true
#| echo: false
scales::show_col(colours = palette())
```
设置新的调色板也是用函数 `palette()` ,参数 `value` 设置新的颜色值向量,下面依次是红、蓝、绿、黄四种颜色。
```{r}
palette(value = c("#EA4335", "#4285f4", "#34A853", "#FBBC05"))
```
函数 `plot()` 的调色板默认来自函数 `palette()` ,经过上面的调整,同一行绘图代码出来不同的效果,即 @fig-iris-group1 变成 @fig-iris-color 。
```{r}
#| label: fig-iris-color
#| fig-cap: 配色
#| fig-width: 4
#| fig-height: 4
#| fig-showtext: true
#| par: true
plot(Sepal.Length ~ Sepal.Width, data = iris, col = Species, pch = 16)
```
### 注释 {#sec-elements-annotation}
函数 `text()` 可以在图上任意位置添加文本或公式。下图在位置 (4,6.5) 处添加红色的文字 flower。
```{r}
#| label: fig-iris-text
#| fig-cap: 注释
#| fig-width: 4
#| fig-height: 4
#| fig-showtext: true
#| par: true
plot(Sepal.Length ~ Sepal.Width, data = iris)
text(x = 4, y = 6.5, labels = "flower", col = "#EA4335")
```
### 图例 {#sec-elements-legend}
函数 `plot()` 不会自动添加图例,需要使用函数 `legend()` 添加图例。
```{r}
#| label: fig-iris-legend1
#| fig-cap: 图例
#| fig-width: 5
#| fig-height: 4.5
#| fig-showtext: true
#| par: true
plot(Sepal.Length ~ Sepal.Width, data = iris, col = Species, pch = 16)
legend(x = "topright", title = "Species",
legend = unique(iris$Species), box.col = NA, bg = NA,
pch = 16, col = c("#EA4335", "#4285f4", "#34A853")
)
```
图例放置在绘图区域以外,比如右边空区域。此时,通过点和文本构造图例。
```{r}
#| label: fig-iris-legend2
#| fig-cap: 图例
#| fig-width: 6
#| fig-height: 5
#| fig-showtext: true
op <- par(mar = c(4, 4, 3, 6))
plot(
Sepal.Length ~ Sepal.Width, data = iris,
col = Species, pch = 16, main = "Edgar Anderson's Iris Data"
)
text(x = 4.7, y = 6.75, labels = "Species", pos = 4, offset = .5, xpd = T)
points(x = 4.7, y = 6.5, pch = 16, cex = 1, col = "#EA4335", xpd = T)
text(x = 4.7, y = 6.5, labels = "setosa", pos = 4, col = "#EA4335", xpd = T)
points(x = 4.7, y = 6.3, pch = 16, cex = 1, col = "#4285f4", xpd = T)
text(x = 4.7, y = 6.3, labels = "versicolor", pos = 4, col = "#4285f4", xpd = T)
points(x = 4.7, y = 6.1, pch = 16, cex = 1, col = "#34A853", xpd = T)
text(x = 4.7, y = 6.1, labels = "virginica", pos = 4, col = "#34A853", xpd = T)
on.exit(par(op), add = TRUE)
```
在函数 `plot()` 内设置较宽的坐标轴范围,获得一个较宽的绘图区域,再用函数 `points()` 添加数据点,最后,使用函数 `legend()` 添加图例。
```{r}
#| label: fig-iris-legend3
#| fig-cap: 图例
#| fig-width: 5
#| fig-height: 5
#| fig-showtext: true
plot(
x = c(2, 6), y = range(iris$Sepal.Length), type = "n",
xlab = "Sepal Width", ylab = "Sepal Length",
main = "Edgar Anderson's Iris Data"
)
points(Sepal.Length ~ Sepal.Width,
col = Species, pch = 16, data = iris
)
legend(x = "right", title = "Species",
legend = unique(iris$Species), box.col = NA, bg = NA,
pch = 16, col = c("#EA4335", "#4285f4", "#34A853")
)
```
### 统计 {#sec-elements-reg}
添加分组线性回归线。按鸢尾花种类分组,线性回归模型拟合数据,抽取回归系数。首先,使用函数 `split()` 将数据集 iris 按变量 Species 分组拆分,得到一个列表,每个元素都是数据框。接着,调用函数 `lapply()` 将函数 `lm()` 作用到列表的每个元素上,得到一个列表,每个元素都是线性拟合对象。最后,再调函数 `lapply()` 将函数 `coef()` 应用到列表的每个元素上,得到回归模型的系数向量。
```{r}
lapply(
lapply(
split(iris, ~Species), lm,
formula = Sepal.Length ~ Sepal.Width
),
coef
)
```
走到绘图这一步,往往是画什么内容比较清楚,分类数量、调色板都确定下来了。大致来说分 6 步:第一步,实现分组线性回归拟合;第二步,绘制分组散点图;第三步,添加分组回归线;第四步,添加图例并调整图例的位置;第五步,设置图形边界等绘图参数;第六步,添加背景网格线。输入线性拟合对象给函数 `abline()` 可以直接绘制回归线,不需要从拟合对象中提取回归系数。调用函数 `par()` 设置图形边界,特别是增加图形右侧边界以容纳图例,再调用函数 `legend()` 要设置 `xpd = TRUE` 以允许图例超出绘图区域。
```{r}
#| label: fig-iris-lm
#| fig-cap: 分组线性回归
#| fig-width: 6
#| fig-height: 5
#| fig-showtext: true
# 分组线性拟合
iris_lm <- lapply(
split(iris, ~Species), lm, formula = Sepal.Length ~ Sepal.Width
)
# 将分组变量和颜色映射
cols <- c("setosa" = "#EA4335", "versicolor" = "#4285f4", "virginica" = "#34A853")
# 设置图形边界以容纳标签和图例
op <- par(mar = c(4, 4, 3, 8))
# 绘制分组散点图
plot(
Sepal.Length ~ Sepal.Width,
data = iris, col = Species, pch = 16,
xlab = "Sepal Width", ylab = "Sepal Length",
main = "Edgar Anderson's Iris Data"
)
# 添加背景参考线
grid()
# 添加回归线
for (species in c("setosa", "versicolor", "virginica")) {
abline(iris_lm[[species]], col = cols[species], lwd = 2)
}
# 添加图例
legend(
x = "right", title = "Species", inset = -0.4, xpd = TRUE,
legend = unique(iris$Species), box.col = NA, bg = NA, lty = 1, lwd = 2,
pch = 16, col = c("#EA4335", "#4285f4", "#34A853")
)
# 恢复图形参数设置
on.exit(par(op), add = TRUE)
```
## 绘图进阶 {#sec-graphics-advanced}
### 组合图形 {#sec-graphics-composite}
点、线、多边形组合
```{r}
#| label: fig-math-annotation
#| fig-cap: 正态总体下两样本均值之差的检验
#| fig-width: 7
#| fig-height: 5
#| fig-showtext: true
x <- seq(-10, 10, length = 400)
y1 <- dnorm(x)
y2 <- dnorm(x, m = 3)
op <- par(mar = c(5, 4, 2, 1))
plot(x, y2,
xlim = c(-3, 8), type = "n",
xlab = quote(Z == frac(mu[1] - mu[2], sigma / sqrt(n))),
ylab = "Density"
)
polygon(c(1.96, 1.96, x[240:400], 10),
c(0, dnorm(1.96, m = 3), y2[240:400], 0),
col = "grey80", lty = 0
)
lines(x, y2)
lines(x, y1)
polygon(c(-1.96, -1.96, x[161:1], -10),
c(0, dnorm(-1.96, m = 0), y1[161:1], 0),
col = "grey30", lty = 0
)
polygon(c(1.96, 1.96, x[240:400], 10),
c(0, dnorm(1.96, m = 0), y1[240:400], 0),
col = "grey30"
)
legend(x = 4.2, y = .4,
fill = c("grey80", "grey30"),
legend = expression(
P(abs(Z) > 1.96, H[1]) == 0.85,
P(abs(Z) > 1.96, H[0]) == 0.05
), bty = "n"
)
text(0, .2, quote(H[0]:~ ~ mu[1] == mu[2]))
text(3, .2, quote(H[1]:~ ~ mu[1] == mu[2] + delta))
on.exit(par(op), add = TRUE)
```
### 多图布局 {#sec-graphics-layout}
布局函数 `layout()` 和图形参数设置函数 `par()`
```{r}
#| label: fig-anscombe
#| fig-cap: 数据可视化很重要
#| fig-width: 6
#| fig-height: 6
#| fig-showtext: true
data(anscombe)
form <- sprintf("y%d ~ x%d", 1:4, 1:4)
fit <- lapply(form, lm, data = anscombe)
op <- par(mfrow = c(2, 2), mgp = c(2, 0.7, 0),
mar = c(3, 3, 1, 1) + 0.1, oma = c(0, 0, 2, 0))
for (i in 1:4) {
plot(as.formula(form[i]),
data = anscombe, col = "black",
pch = 20, xlim = c(3, 19), ylim = c(3, 13),
xlab = as.expression(substitute(x[i], list(i = i))),
ylab = as.expression(substitute(y[i], list(i = i))),
family = "sans"
)
abline(fit[[i]], col = "black")
text(
x = 7, y = 12, family = "sans",
labels = bquote(R^2 == .(round(summary(fit[[i]])$r.squared, 3)))
)
}
mtext("数据集的四重奏", outer = TRUE)
on.exit(par(op), add = TRUE)
```
## 图形选择 {#sec-graphics-choose}
以不同的二维或三维图形可视化同一份多元数据。颜色图、透视图、等值线图和填充等值线图存在某种相似性,又有区别。
### 颜色图 {#sec-color-image}
$$
f(x,y) =
\begin{cases}
\frac{\sin(\sqrt{x^2 + y^2})}{\sqrt{x^2 + y^2}}, & (x,y) \neq (0,0)\\
1, & (x,y) = (0,0)
\end{cases}
$$
```{r}
y <- x <- seq(from = -8, to = 8, length.out = 51)
z <- outer(x, y, FUN = function(x, y) sin(sqrt(x^2 + y^2)) / sqrt(x^2 + y^2))
z[26, 26] <- 1
```
将绘图区域划分成网格,每个小网格对应一个颜色值。函数 `image()` 绘制颜色图
```{r}
#| label: fig-image
#| fig-cap: 颜色图
#| fig-width: 4.5
#| fig-height: 4.5
#| dev: 'tikz'
#| fig-process: !expr to_png
#| par: true
image(x = x, y = y, z = z, xlab = "$x$", ylab = "$y$")
```
### 透视图 {#sec-graphics-persp}
函数 `persp()` 绘制透视图
```{r}
#| label: fig-persp
#| fig-cap: 透视图
#| fig-width: 5.5
#| fig-height: 4
#| dev: 'tikz'
#| fig-process: !expr to_png
op <- par(mar = c(0, 1, 2, 1))
persp(
x = x, y = y, z = z, main = "二维函数的透视图",
theta = 30, phi = 30, expand = 0.5, col = "lightblue",
xlab = "$x$", ylab = "$y$", zlab = "$f(x,y)$"
)
on.exit(par(op), add = TRUE)
```
### 等值线图 {#sec-graphics-contour}
地理上,常用等高线图描述地形,等高线图和等值线图其实是一个意思。函数 `contour()` 绘制等值线图。
```{r}
#| label: fig-contour
#| fig-cap: 等值线图
#| fig-width: 4.5
#| fig-height: 4.5
#| dev: 'tikz'
#| fig-process: !expr to_png
#| par: true
contour(x = x, y = y, z = z, xlab = "$x$", ylab = "$y$")
```
### 填充等值线图 {#sec-graphics-filled-contour}
函数 `filled.contour()` 绘制填充等值线图。
```{r}
#| label: fig-filled-contour
#| fig-cap: 填充等值线图
#| fig-width: 5
#| fig-height: 4.5
#| dev: 'tikz'
#| fig-process: !expr to_png
filled.contour(
x = x, y = y, z = z, asp = 1,
color.palette = hcl.colors,
plot.title = {
title(
main = "二维函数的填充等值线图",
xlab = "$x$", ylab = "$y$"
)
},
plot.axes = {
grid(col = "gray")
axis(1, at = 2 * -4:4, labels = 2 * -4:4)
axis(2, at = 2 * -4:4, labels = 2 * -4:4)
points(0, 0, col = "blue", pch = 16)
},
key.axes = {
axis(4, seq(-0.2, 1, length.out = 9))
}
)
```
## 总结 {#sec-graphics-summary}
### tinyplot 包 {#sec-graphics-plot2}
[tinyplot](https://github.com/grantmcdermott/tinyplot) 包扩展 Base R 函数 `plot()` 的功能,在公式语法方面和 lattice 包很接近。另一个值得一提的 R 包是 [**basetheme**](https://github.com/karoliskoncevicius/basetheme) ,用来设置 Base R 绘图主题。
```{r}
#| label: fig-tinyplot-iris
#| fig-cap: tinyplot 包绘制分组散点图
#| fig-width: 6
#| fig-height: 4
#| fig-showtext: true
#| par: true
library(tinyplot)
tinyplot(Sepal.Length ~ Sepal.Width | Species, data = iris,
palette = "Tableau 10", pch = 16)
```
或者使用参数 `by` 指定分组变量,效果和上图一样。
```{r}
#| eval: false
with(iris, {
tinyplot(y = Sepal.Length, x = Sepal.Width, by = Species,
palette = "Tableau 10", pch = 16)
})
```
还可以使用参数 `legend` 调整图例的位置,比如放置在绘图区域下方。
```{r}
#| label: fig-tinyplot-legend
#| fig-cap: tinyplot 包调整图例位置
#| fig-width: 5
#| fig-height: 5
#| fig-showtext: true
op <- par(mar = c(5, 4, .5, .5))
tinyplot(Sepal.Length ~ Sepal.Width | Species,
data = iris, palette = "Tableau 10", pch = 16,
legend = legend("bottom!", title = "Species of iris", bty = "o")
)
on.exit(par(op), add = TRUE)
```
还可以绘制其它类型的图形,如分组密度曲线图、分面散点图等。
```{r}
#| label: fig-tinyplot-others
#| fig-cap: tinyplot 包的分组和分面功能
#| fig-subcap:
#| - 分组密度曲线图
#| - 分面散点图
#| fig-width: 6
#| fig-height: 5
#| fig-showtext: true
#| layout-ncol: 1
#| layout-nrow: 2
tinyplot(
~ Petal.Length | Species, data = iris,
type = "density",
fill = "by", # 分组填充
grid = TRUE, # 背景网格
palette = "Tableau 10"
)
tinyplot(
Petal.Width ~ Petal.Length | Sepal.Length, data = iris,
facet = ~ Species, # 分面
grid = TRUE, pch = 19, frame = TRUE,
palette = terrain.colors # 生成调色板的函数指定配色
)
```
### plot3D 包 {#sec-graphics-plot3D}
虽然不提倡大量使用三维图形,但如何绘制三维图形却是生生不息的命题,以下仅是 R 语言社区的冰山一角。
- **plotrix** [@Lemon2006] 一个坐落于 R 的红灯区的 R 包。基于 Base R 各类绘图函数。
- **scatterplot3d** [@Uwe2003] 基于 Base R 绘制三维散点图。
- **misc3d** [@misc3d2008] 绘制三维图形的杂项,支持通过 Base R、 **tcltk** 包和 **rgl** 包渲染图形。
- **plot3D** [@plot3D2021] 依赖 **misc3d** 包,加强 Base R 在制作三维图形方面的能力。
举个比较新颖的一个例子,**plot3D 包**的函数 `image2D()` 绘制二维颜色图,细看又和 `image()` 函数不同,渲染出来的图形有三维的立体感。归根结底,很多时候束缚住自己的不是工具,而是视野和思维。以奥克兰 Maunga Whau 火山地形数据 `volcano` 为例。
```{r}
#| label: fig-volcano-plot3d
#| fig-cap: 奥克兰火山地形图
#| fig-subcap:
#| - 函数 `image2D()` 二维颜色图
#| - 函数 `persp3D()` 三维透视图
#| fig-width: 6.25
#| fig-height: 5
#| fig-showtext: true
#| warning: false
#| layout-ncol: 1
#| out-width: 70%
library(plot3D)
image2D(volcano,
shade = 0.2, rasterImage = TRUE, asp = 0.7,
xlab = "南北方向", ylab = "东西方向",
main = "奥克兰 Maunga Whau 地形图", clab = "高度",
contour = FALSE, col = hcl.colors(100),
colkey = list(
at = 90 + 20 * 0:5, labels = 90 + 20 * 0:5,
length = 1, width = 1
)
)
op <- par(mar = c(1, 1.5, 0, 0))
persp3D(
x = 1:87, y = 1:61, z = volcano, col = hcl.colors(100),
ticktype = "detailed", colkey = FALSE, expand = 1,
phi = 35, theta = 125, bty = "b2", shade = TRUE,
ltheta = 100, lphi = 45,
xlab = "\n南北方向", ylab = "\n东西方向", zlab = "\n高度"
)
on.exit(par(op), add = TRUE)
```
值得一提,Python 社区的绘图模块 matplotlib 同样具有强大的绘图能力,三维图形也不在话下。不过,不同的绘图系统所采用的透视法不同,如下图所示。
```{r}
#| eval: false
#| echo: false
# 保存数据
write.table(volcano,
file = "data/volcano.csv", sep = ",",
row.names = FALSE, col.names = F, quote = F
)
```
```{python}
#| label: fig-volcano-plt
#| fig-cap: matplotlib 绘制三维透视图
#| fig-width: 5.5
#| fig-height: 5
#| code-fold: true
#| echo: !expr knitr::is_html_output()
from matplotlib import cm
from matplotlib.colors import LightSource
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# 设置 PGF 后端渲染图形
import matplotlib as mpl
mpl.use("pgf")
# XeLaTeX 编译图形
plt.rcParams.update({
"text.usetex": True,
"pgf.texsystem": "xelatex",
"pgf.rcfonts": False, # don't setup fonts from rc parameters
"pgf.preamble": "\n".join([
r"\usepackage[fontset=fandol,UTF8]{ctex}",
]),
})
# 读取数据
volcano = pd.read_csv("data/volcano.csv", header=None)
# DataFrame 转 Array
z = volcano.to_numpy().transpose()
# 数据行、列数
ncols, nrows = z.shape
# 线性序列
x = np.linspace(0, 86, nrows)
y = np.linspace(0, 60, ncols)
# 类似 R 语言函数 expand.grid()
xv, yv = np.meshgrid(x, y)
# 设置主题
fig, ax = plt.subplots(subplot_kw=dict(projection="3d"))
# 观察视角
ax.view_init(azim=30, elev=30)
# 设置坐标轴标签
ax.set_xlabel(r"南北方向", rotation=45)
ax.set_ylabel(r"东西方向", rotation=-15)
ax.set_zlabel(r"高度", rotation=90)
# 去掉多余的边空
fig.set_tight_layout(True)
# 光源照射的角度
ls = LightSource(270, 45)
# 自定义调色板
rgb = ls.shade(z, cmap=cm.viridis, vert_exag=0.1, blend_mode="soft")
# 三维透视图
surf = ax.plot_surface(
xv, yv, z, rstride=1, cstride=1, facecolors=rgb,
linewidth=0, antialiased=False, shade=False
)
# 渲染
plt.show()
```
::: content-hidden
``` python
import matplotlib.font_manager as fm
fm.get_font_names() # 查看可供 matplotlib 使用的字体名称
from matplotlib import rcParams
rcParams.keys() # 查看可配置的选项
import matplotlib.pyplot as plt
plt.savefig('demo.pdf', backend='pgf') # 保存图片
# https://matplotlib.org/stable/tutorials/text/pgf.html
```
:::