原文:https://web.archive.org/web/20210815221329/http://karolis.koncevicius.lt/posts/r_base_plotting_without_wrappers/
基本绘图和R本身一样古老,但对大多数用户来说,它仍然是神秘的。他们可能使用plot()
,甚至知道其参数的完整列表,但大多数人从未完全理解它。本文试图通过为外行提供友好的介绍来揭开基础图形的神秘面纱。
相关阅读:
在学习R之后,用户可以很快开始通过调用plot()
、hist()
或barplot()
生成各种图形。然后,当面对一个复杂的图形时,他们开始使用各种方法,如add=TRUE
, ann=FALSE
, cex=0
,将这些绘制堆叠在一起。对于大多数人来说,这标志着他们基本的绘图旅程的结束,给他们留下的印象是,这是一个需要学习和记住的特殊技巧,但否则就很难、不一致和不直观。如今,即使是撰写基本图形或将其与其他系统进行比较的专家也持有相同的观点。然而,每个人都在使用的那些初始函数只是完成所有工作的较小函数的包装。许多人会惊讶地发现,在底层基础绘图遵循一组小函数的范式,每个函数做一件事,并与另一个函数很好地协作。
让我们从一个最简单的例子开始:
plot(0:10, 0:10, xlab = "x-axis", ylab = "y-axis", main = "my plot")
上面的plot()
函数实际上只是一个包装器,它调用较低级函数的列表。
plot.new()
plot.window(xlim = c(0,10), ylim = c(0,10))
points(0:10, 0:10)
axis(1)
axis(2)
box()
title(xlab = "x-axis")
title(ylab = "y-axis")
title(main = "my plot")
这样写,构成绘图的所有元素就变得清晰了。每个新函数调用都在此之前生成的绘图上绘制单个对象。为了改变图上的某些内容,我们可以很容易地看到应该修改哪条线。作为一个例子,让我们通过以下方式来修改上面的绘图:1)添加网格,2)移除绘图周围的方框,3)移除轴线,4)将轴线标签加粗,5)将注释标签变为红色,6)将标题向左移动。
plot.new()
plot.window(xlim = c(0,10), ylim = c(0,10))
grid()
points(0:10, 0:10)
axis(1, lwd = 0, font.axis=2)
axis(2, lwd = 0, font.axis=2)
title(xlab = "x-axis", col.lab = "red3")
title(ylab = "y-axis", col.lab = "red3")
title(main = "my plot", col.main = "red3", adj = 0)
在每种情况下,为了达到想要的效果,只需要修改一行即可。函数名很直观。一个没有任何R经验的人能够轻松地说出哪条线添加了哪个元素或改变了某些参数。 因此,为了构造一个图,我们逐一调用各种函数。但是我们从哪里得到这些函数的名字呢?我们需要记住几百个吗?事实证明,你在一个plot中可能需要做的所有事情都是非常有限的。
par() # 指定多个绘图参数
plot.new() # 开始一个新的图形
plot.window() # 添加坐标系统到绘图区域
points() # 绘制点
lines() # 绘制线连接两个点
abline() # 绘制贯穿图像的无限长线条
arrows() # 绘制箭头
segments() # 绘制线段
rect() # 绘制矩形
polygon() # 绘制复杂多边形
text() # 在图形中添加文本
mtext() # 在图的边缘添加文本
title() # 图形和轴注释
axis() # 添加轴
box() # 添加边框
grid() # 在坐标系统上添加网格
legend() # 添加图例
上面的列表涵盖了重建几乎任何绘图所需的大部分功能。作为演示,可以使用example()
快速查看每个函数的功能,例如example(rect)
。R还有其他一些有用的函数,如rug()
和jitter()
,以简化某些情况,但它们不是关键的,可以使用上面列出的函数实现。
函数名很简单,但是它们的参数呢?事实上,有些参数名,如cex
,可能看起来相当含糊。但是参数名称总是图形某个属性的缩写。例如,col
是“颜色”的缩写,lwd
表示“行宽”,cex
表示“字符扩展”(character expansion)。好消息是,在所有的base R函数中,相同的参数代表相同的性质。对于特定的函数help()
总是可以用于获取所有参数及其描述的列表。
为了进一步说明参数之间的一致性,让我们回到第一个例子。现在应该很清楚了,只有一个例外axis(1)
和axis(2)
那两行。这些数字1
和2
从何而来?这些数字指定了图形周围的位置,它们从1开始,它指的是图形的底部,顺时针向上到4,它指的是右边。下图展示了数字和四边之间的关系。
plot.new()
box()
mtext("1", side = 1, col = "red3")
mtext("2", side = 2, col = "red3")
mtext("3", side = 3, col = "red3")
mtext("4", side = 4, col = "red3")
在各种不同的函数中使用相同的位置编号。当某个函数的参数需要指定边时,很可能会使用上面描述的数值表示法。下面是一些例子。
par(mar = c(0,0,4,4)) # 图的边距 c(bottom, left, right , top)
par(oma = c(1,1,1,1)) # 图的外边距
axis(3) # 显示轴的边
text(x, y, "text", pos = 3) # pos 选择“文本”显示的位置
mtext("text", side = 4) # side 指定将在右边显示“文本”
另一个重要的点是向量化。基本绘图函数的几乎所有参数都是向量化的。例如,在绘制矩形时,用户不必在一个循环内逐个添加每个矩形的每个点。相反,他或她可以用一个函数调用绘制所有相关的对象,同时为每个对象指定不同的位置和参数。
plot.new()
plot.window(xlim = c(0,3), ylim = c(0,3))
rect(xleft = c(0,1,2), ybottom = c(0,1,2), xright = c(1,2,3), ytop = c(1,2,3),
border = c("pink","red","darkred"), lwd = 10
)
这是另一个产生棋盘图案的例子。
plot.new()
plot.window(xlim = c(0,10), ylim = c(0,10))
xs <- rep(1:9, each = 9)
ys <- rep(1:9)
rect(xs-0.5, ys-0.5, xs+0.5, ys+0.5, col = c("white","darkgrey"))
基本R图形的优势之一是它的灵活性和定制的潜力。当用户需要遵循在现有示例或模板中找到的特定样式时,它真的很闪耀。下面的一些插图展示了不同的base函数如何协同工作,以及如何从零开始重建各种类型的常见图形。
x <- time(uspop)
y <- uspop
plot.new()
plot.window(xlim = range(x), ylim = range(pretty(y)))
rect(x-4, 0, x+4, y)
text(x, y, y, pos = 3, col = "red3", cex = 0.7)
mtext(x, 1, at = x, las = 2, cex = 0.7, font = 2)
axis(2, lwd = 0, las = 2, cex.axis = 0.7, font.axis = 2)
title("US population growth", adj = 0, col.main = "red2")
在这种情况下,每个矩形必须指定四组点:左下角x、y坐标,右上角x、y坐标。最后,即使这是一个更复杂的示例,我们仍然使用对rect()
、text()
和mtext()
的单个函数调用添加了所有不同的信息。
palette(c("cornflowerblue", "red3", "orange"))
plot.new()
plot.window(xlim = c(1,4), ylim = range(iris[,-5]))
grid(nx = NA, ny = NULL)
abline(v = 1:4, col = "grey", lwd = 5, lty = "dotted")
matlines(t(iris[,-5]), col = iris$Species, lty = 1)
axis(2, lwd = 0, las = 2)
mtext(variable.names(iris)[-5], 3, at = 1:4, line = 1, col = "darkgrey")
legend(x = 1, y = 2, legend = unique(iris$Species), col = unique(iris$Species),
lwd = 3, bty = 'n')
在本例中,我们使用了一个特殊的函数matlines()
,该函数为矩阵中的每一列绘制一行。到目前为止,我们还做了其他一些新颖的事情:通过palette()
更改默认的颜色配置,并在matlines()
和legend()
中使用因子级别指定实际的颜色。改变调色板允许我们定制配色方案,而为颜色参数传递因子可以确保在所有不同的函数中,相同的颜色被一致地分配给相同的因子级别。
colors <- hcl.colors(5, "Zissou")
ys <- c(1.25, 2, 1.5, 2.25)
plot.new()
plot.window(xlim = range(0,VADeaths), ylim = c(1,2.75))
abline(h = ys, col = "grey", lty = "dotted", lwd = 3)
points(VADeaths, ys[col(VADeaths)], col = colors, pch = 19, cex = 2.5)
text(0, ys, colnames(VADeaths), adj = c(0.25,-1), col = "lightslategrey")
axis(1, lwd = 0, font = 2)
title("deaths per 1000 in 1940 Virginia stratified by age, gender, and location")
legend("top", legend = rownames(VADeaths), col = colors, pch = 19, horiz = TRUE,
bty = "n", title = "age bins")
在这个图表中,组按性别、年龄和位置分层。每组的y轴高度都是手动选择的。
par(mar = c(4,4,4,4))
plot.new()
plot.window(xlim = range(mtcars$disp), ylim = range(pretty(mtcars$mpg)))
points(mtcars$disp, mtcars$mpg, col = "darkorange2", pch = 19, cex = 1.5)
axis(2, col.axis = "darkorange2", lwd = 2, las = 2)
mtext("miles per gallon", 2, col = "darkorange2", font = 2, line = 3)
plot.window(xlim = range(mtcars$disp), ylim = range(pretty(mtcars$hp)))
points(mtcars$disp, mtcars$hp, col = "forestgreen", pch = 19, cex = 1.5)
axis(4, col.axis = "forestgreen", lwd = 2, las = 2)
mtext("horse power", 4, col = "forestgreen", font = 2, line = 3)
box()
axis(1)
mtext("displacement", 1, font = 2, line = 3)
title("displacement VS mpg VS hp", adj = 0, cex.main = 1)
在这里,我们在一个图形上可视化了两个散点图,具有不同的y轴。这里的技巧是使用plot.window()
更改图形中间的坐标系统。但是请注意,双y轴的绘图是不可取的,所以不要把这个例子作为一个建议。
dens <- tapply(chickwts$weight, chickwts$feed, density)
xs <- Map(getElement, dens, "x")
ys <- Map(getElement, dens, "y")
ys <- Map(function(x) (x-min(x)) / max(x-min(x)) * 1.5, ys)
ys <- Map(`+`, ys, length(ys):1)
plot.new()
plot.window(xlim = range(xs), ylim = c(1,length(ys)+1.5))
abline(h = length(ys):1, col = "grey")
Map(polygon, xs, ys, col = hcl.colors(length(ys), "Zissou", alpha = 0.8))
axis(1, tck = -0.01)
mtext(names(dens), 2, at = length(ys):1, las = 2, padj = 0)
title("Chicken weights", adj = 0, cex = 0.8)
在本例中,通过将y值转换为0 - 1.5的范围,然后为每种馈线类型添加不同的偏移量,来完成准备密度的大部分工作。为了使绘制的密度很好地重叠,我们从顶部开始绘制它们,然后向下。之后Map()
和polygon()
完成所有工作。
dens <- tapply(chickwts$weight, chickwts$feed, density)
cols <- hcl.colors(length(dens), "Zissou")
xs <- Map(getElement, dens, "y")
ys <- Map(getElement, dens, "x")
xs <- Map(c, xs, Map(rev, Map(`*`, -1, xs)))
ys <- Map(c, ys, Map(rev, ys))
xs <- Map(function(x) (x-min(x)) / max(x-min(x) * 1.1), xs)
xs <- Map(`+`, xs, 1:length(xs))
plot.new()
plot.window(xlim = range(xs), ylim = range(ys))
grid(nx = NA, ny = NULL, lwd = 2)
Map(polygon, xs, ys, col = cols)
axis(2, las = 1, lwd = 0)
title("Chicken weight by feed type", font = 2)
legend("top", legend = names(dens), fill = cols, ncol = 3, inset = c(0, 1),
xpd = TRUE, bty = "n")
现在多边形是双面的,所以我们需要镜像和复制xs和ys。在上面的代码中,第5行和第6行完成了这项工作。之后的绘图几乎与前面的示例相同。在图例上还有一个额外的技巧,我们使用“inset”将它推到另一边。
cors <- cor(mtcars)
cols <- hcl.colors(200, "RdBu")[round((cors+1)*100)]
par(mar = c(5,5,0,0))
plot.new()
plot.window(xlim = c(0,ncol(cors)), ylim = c(0,ncol(cors)))
rect(row(cors)-1, col(cors)-1, row(cors), col(cors))
symbols(row(cors)-0.5, col(cors)-0.5, circles = as.numeric(abs(cors))/2,
inches = FALSE, asp = 1, add = TRUE, bg = cols
)
mtext(rownames(cors), 1, at=1:ncol(cors)-0.5, las=2)
mtext(colnames(cors), 2, at=1:nrow(cors)-0.5, las=2)
这里的第二行通过将相关性范围从-1:1转换为0:200为每个相关值分配颜色。然后我们使用rect()
函数获得网格,并使用symbols()
添加具有指定半径的圆。得到的图类似于corrplot library实现的图。
R基础绘图系统有几个抛光和易于使用的包装器,有时很方便,但从长远来看只会混淆和隐藏东西。因此,大多数R用户从来没有被正确地介绍过基本绘图范式背后的真正功能,并被其许多感知到的特性所迷惑。然而,如果检查得当,基本绘图可以变得强大、灵活和直观。在所有包装的引擎盖下,繁重的搬运工作是由一组相互配合的简单功能完成的。通常只需要几行代码就可以生成一个优雅的定制图形。
参考: