R 数据可视化 —— 用 gtable 绘制多个 Y 轴

前言

经过前面一节的介绍,我们对 gtable 布局应该有了较为清晰的认识,下面让我们来看看 ggplot 对象的布局

首先,使用 ggplot 创建一个图形

p <- ggplot(mtcars, aes(mpg, disp)) + geom_point()
p

获取 ggplot 图形对象,是一个 gtable 对象

> g <- ggplotGrob(p)
> class(g)
[1] "gtable" "gTree"  "grob"   "gDesc" 

查看对象

> g
TableGrob (12 x 9) "layout": 18 grobs
    z         cells       name                                         grob
1   0 ( 1-12, 1- 9) background               rect[plot.background..rect.86]
2   5 ( 6- 6, 4- 4)     spacer                               zeroGrob[NULL]
3   7 ( 7- 7, 4- 4)     axis-l           absoluteGrob[GRID.absoluteGrob.74]
4   3 ( 8- 8, 4- 4)     spacer                               zeroGrob[NULL]
5   6 ( 6- 6, 5- 5)     axis-t                               zeroGrob[NULL]
6   1 ( 7- 7, 5- 5)      panel                      gTree[panel-1.gTree.66]
7   9 ( 8- 8, 5- 5)     axis-b           absoluteGrob[GRID.absoluteGrob.70]
8   4 ( 6- 6, 6- 6)     spacer                               zeroGrob[NULL]
9   8 ( 7- 7, 6- 6)     axis-r                               zeroGrob[NULL]
10  2 ( 8- 8, 6- 6)     spacer                               zeroGrob[NULL]
11 10 ( 5- 5, 5- 5)     xlab-t                               zeroGrob[NULL]
12 11 ( 9- 9, 5- 5)     xlab-b titleGrob[axis.title.x.bottom..titleGrob.77]
13 12 ( 7- 7, 3- 3)     ylab-l   titleGrob[axis.title.y.left..titleGrob.80]
14 13 ( 7- 7, 7- 7)     ylab-r                               zeroGrob[NULL]
15 14 ( 4- 4, 5- 5)   subtitle         zeroGrob[plot.subtitle..zeroGrob.82]
16 15 ( 3- 3, 5- 5)      title            zeroGrob[plot.title..zeroGrob.81]
17 16 (10-10, 5- 5)    caption          zeroGrob[plot.caption..zeroGrob.84]
18 17 ( 2- 2, 2- 2)        tag              zeroGrob[plot.tag..zeroGrob.83]

可以看到,每个位置所放置的对象及其名称,共 18 个。我们可以很容易地根据名称来获取对应的图形对象

主要包括主绘图区域 panelXY 轴及其对应的轴标签,标题,题注等

我们可以使用 gtable_show_layout 来展示整个布局

gtable_show_layout(g)

例如,(7,5) 对应的就是名称为 panel 的对象,(7,4) 对应的就是左边的 Y 轴,(7,3) 对应的就是左边 Y 轴的标签。

现在,我们对 ggplot 对象布局已经有了一定的了解,那我们要如何合并包含多个轴的图形呢?

合并图形

首先,基于现在对 ggplot 的了解,我们应该很容易地就能想到,通过 gtable 对象来获取对应位置的图形对象,然后使用 gtable_add_grob 函数将某一个对象添加到指定 gtable 的位置中

然后,对于轴线,我们可以获取一个图形的 Y 轴对象,然后对轴标签和轴刻度线进行一定的转换,让其朝向右侧

最后,通过 gtable_add_colsgtable_add_grob 将修改后的 Y 轴添加到图形的右侧

基于上面的构想,让我们先来看看如何组合两张图片

先构造如下数据

colors <- c('#5470C6', '#91CC75', '#EE6666')
data <- data.frame(
    category = factor(substr(month.name, 1, 3), levels = substr(month.name, 1, 3)),
    Evaporation = c(2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3),
    Precipitation = c(2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3),
    Temperature = c(2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2)
)

先绘制一年中蒸发量和降水量的直方图

p1 <- ggplot(data, aes(category, Evaporation)) + 
    geom_col(fill = colors[1], width = 0.3, position = position_nudge(x = -0.2)) + 
    labs(x = "month", y = "Evaporation(ml)") +
    scale_y_continuous(limits = c(0, 250), expand = c(0,0)) +
    theme(panel.grid = element_blank(), 
          panel.background = element_rect(fill = NA), 
          axis.text.y = element_text(color = colors[1]), 
          axis.ticks.y = element_line(color = colors[1]), 
          axis.title.y = element_text(color = colors[1], angle = 270), 
          axis.line.y = element_line(color = colors[1]), 
          axis.line.x = element_line(color = "black"),
          axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1)
    )
p2 <- ggplot(data, aes(category, Precipitation)) + 
    geom_col(fill = colors[2], width = 0.3, position = position_nudge(x = 0.2)) + 
    labs(x = "month", y = "Precipitation(ml)") +
    scale_y_continuous(limits = c(0, 250), expand = c(0,0)) +
    theme(panel.grid = element_blank(), 
          panel.background = element_rect(fill = NA), 
          axis.text.y = element_text(color = colors[2]), 
          axis.ticks.y = element_line(color = colors[2]), 
          axis.title.y = element_text(color = colors[2], angle = 270), 
          axis.line.y = element_line(color = colors[2]), 
          axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1)
    )

你可能注意到了,我们为直方图设置了不同的偏移,这是为了避免图形合并时将原来的图形覆盖,还有一个重要的设置,就是要将背景填充色设置为 NA,否则也会覆盖之前的图形

然后先获取 gtable 对象

g1 <- ggplotGrob(p1)
g2 <- ggplotGrob(p2)

我们以第一幅图为模板,其他图像往里面添加,所以需要知道第一幅图主绘图区域的位置

pos <- c(subset(g1$layout, name == "panel", select = t:r))

获取到位置之后,将第二幅图的图形对象添加进去

g <- gtable_add_grob(g1, g2$grobs[[which(g2$layout$name == "panel")]], 
                     pos$t, pos$l, pos$b, pos$l)
plot(g)

那现在要做的,就是将第二幅图的 Y 轴添加进去。

这一步虽然可以使用 sec_axis 实现,但是这种方式提供的操作有限,三个 Y 轴的情况也无法实现。

首先,获取第二幅图的左侧 Y 轴,名称都是见名知意的 "axis-l"

index <- which(g2$layout$name == "axis-l")
yaxis <- g2$grobs[[index]]

Y 轴对象包含两个子对象,第一个为轴线,第二个为刻度

> yaxis$children
(polyline[GRID.polyline.2180], gtable[axis]) 

然后,将 Y 轴线移动到最左边

yaxis$children[[1]]$x <- unit.c(unit(0, "npc"), unit(0, "npc"))

获取刻度线和刻度标签的布局

> ticks <- yaxis$children[[2]]
> ticks
TableGrob (1 x 2) "axis": 2 grobs
  z     cells name                           grob
  1 (1-1,1-1) axis   polyline[GRID.polyline.2183]
1 2 (1-1,2-2) axis titleGrob[GRID.titleGrob.2182]

将刻度线和刻度标签的相对位置进行交换

# 交换刻度线和刻度标签的相对位置
ticks$widths <- rev(ticks$widths)
ticks$grobs <- rev(ticks$grobs)
# 移动刻度线
ticks$grobs[[1]]$x <- ticks$grobs[[1]]$x - unit(1, "npc") + unit(3, "pt")

文本标签的修改比较复杂,不仅要交换位置,还要修改文本的对齐方式,在轴标签的设置中也可以用到,可以封装成函数

# 水平交换文本标签
hinvert_title_grob <- function(grob){
    # 交换宽度
    widths <- grob$widths
    grob$widths[1] <- widths[3]
    grob$widths[3] <- widths[1]
    grob$vp[[1]]$layout$widths[1] <- widths[3]
    grob$vp[[1]]$layout$widths[3] <- widths[1]
    
    # 修改对齐
    grob$children[[1]]$hjust <- 1 - grob$children[[1]]$hjust 
    grob$children[[1]]$vjust <- 1 - grob$children[[1]]$vjust 
    grob$children[[1]]$x <- unit(1, "npc") - grob$children[[1]]$x
    grob
}

之后,将所有的修改覆盖原来的设置

ticks$grobs[[2]] <- hinvert_title_grob(ticks$grobs[[2]])
yaxis$children[[2]] <- ticks

最后,将 Y 轴添加到图形的右侧

g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l], pos$r)
g <- gtable_add_grob(g, yaxis, pos$t, pos$r + 1, pos$b, pos$r + 1, clip = "off", name = "axis-r")

plot(g)

轴已经设置好了,就差轴标签了,有了上面的经验,可以很容易的做到这点

index <- which(g2$layout$name == "ylab-l")
ylab <- g2$grobs[[index]]
ylab <- hinvert_title_grob(ylab)

添加到右侧

g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l], pos$r)
g <- gtable_add_grob(g, ylab, pos$t, pos$r + 1, pos$b, pos$r + 1, clip = "off", name = "ylab-r")
plot(g)

诶?怎么轴标签在里面呢?回顾我们上面的代码,发现原来我们插入的位置 pos 是第一幅图的右侧边界,所以,我们只要交换一下插入的顺序就可以了

交换之后,就得到了我们想要的结果了


将所有代码封装成函数,并使其能够适用于多个 Y

hinvert_title_grob <- function(grob){
  # 交换宽度
  widths <- grob$widths
  grob$widths[1] <- widths[3]
  grob$widths[3] <- widths[1]
  grob$vp[[1]]$layout$widths[1] <- widths[3]
  grob$vp[[1]]$layout$widths[3] <- widths[1]
  
  # 修改对齐
  grob$children[[1]]$hjust <- 1 - grob$children[[1]]$hjust 
  grob$children[[1]]$vjust <- 1 - grob$children[[1]]$vjust 
  grob$children[[1]]$x <- unit(1, "npc") - grob$children[[1]]$x
  grob
}

add_another_yaxis <- function(g1, g2, offset = 0) {
  # ============ 1. 主绘图区 ============ #
  # 获取主绘图区域
  pos <- c(subset(g1$layout, name == "panel", select = t:r))
  # 添加图形
  g <- gtable_add_grob(g1, g2$grobs[[which(g2$layout$name == "panel")]], 
                       pos$t, pos$l, pos$b * ((offset - 2) * 0.00001 + 1), pos$l)
    # ============ 2. 轴标签 ============ #
    index <- which(g2$layout$name == "ylab-l")
  ylab <- g2$grobs[[index]]
  ylab <- hinvert_title_grob(ylab)
  # 添加轴标签
  g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l], pos$r)
  g <- gtable_add_grob(g, ylab, pos$t, pos$r + 1, pos$b, pos$r + 1, clip = "off", name = "ylab-r")
  # ============ 3. 轴设置 ============ #
  index <- which(g2$layout$name == "axis-l")
  yaxis <- g2$grobs[[index]]
  # 将 Y 轴线移动到最左边
  yaxis$children[[1]]$x <- unit.c(unit(0, "npc"), unit(0, "npc"))
  # 交换刻度线和刻度标签
  ticks <- yaxis$children[[2]]
  ticks$widths <- rev(ticks$widths)
  ticks$grobs <- rev(ticks$grobs)
  # 移动刻度线
  ticks$grobs[[1]]$x <- ticks$grobs[[1]]$x - unit(1, "npc") + unit(3, "pt")
  # 刻度标签位置转换和对齐
  ticks$grobs[[2]] <- hinvert_title_grob(ticks$grobs[[2]])
  yaxis$children[[2]] <- ticks
  # 添加轴,unit(3, "mm") 增加轴间距
  g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l] +  + unit(3, "mm"), pos$r)
  g <- gtable_add_grob(g, yaxis, pos$t, pos$r + 1, pos$b, pos$r + 1, clip = "off", name = "axis-r")
  g
}

# 接受可变参数,可添加多个 Y 轴
plot_multi_yaxis <- function(..., right_label_reverse = TRUE) {
  args <- list(...)
  len <- length(args)
  g <- ggplotGrob(args[[1]])
  for (i in len:2) {
    if (right_label_reverse) {
      # 为轴标签添加旋转
      args[[i]] <- args[[i]] + theme(axis.title.y = element_text(angle = 270))
    }
    g2 <- ggplotGrob(args[[i]])
    g <- add_another_yaxis(g, g2, offset = i)
  }
  # 绘制图形
  grid.newpage()
  grid.draw(g)
}

以我的习惯来说,我可能更偏向于将右侧的轴标签旋转 270 度,看起来更舒服些,所以我添加了一个 right_label_reverse 参数

现在,我们添加第三个图形

p3 <- ggplot(data, aes(category, Temperature, group = 1)) + 
  geom_line(colour = colors[3]) + 
  geom_point(aes(colour = colors[3]), fill = "white", shape = 21, show.legend = FALSE) +
  scale_y_continuous(limits = c(0, 25), expand = c(0,0)) +
  labs(x = "month", y = expression(paste("Temperature (", degree, " C)"))) +
  theme(panel.grid = element_blank(), 
        panel.background = element_rect(fill = NA), 
        axis.text.y = element_text(color = colors[3]), 
        axis.ticks.y = element_line(color = colors[3]), 
        axis.title.y = element_text(color = colors[3], angle = 270), 
        axis.line.y = element_line(color = colors[3]), 
        axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1)
  )

合并

plot_multi_yaxis(p1, p2, p3)

OK,画完收工。

代码已上传:

https://github.com/dxsbiocc/learn/blob/main/R/plot/plot_multi_yaxis.R

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容