如何为ggplot中的每个构面行添加y轴标题?

Posted

技术标签:

【中文标题】如何为ggplot中的每个构面行添加y轴标题?【英文标题】:How to add y axis title for each facet row in ggplot? 【发布时间】:2021-12-29 11:26:45 【问题描述】:

我正在用 facet_grid() 做一个散点图:

library(ggplot2)
ggplot(df, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)

我希望 y 轴标题 y 像这样位于每一行的中间(绘制解决方案):

在此示例中,构面行数为 2,因为 df$group2 有两个不同的值。对于我的实际用例,可能有两行以上,具体取决于使用的构面变量; y 轴标题应该在 每个 facet 行的中间。

目前为止最好的解决方案是adding spaces,这是一团糟,因为使用不同长度的 y 轴标题会将文本从行的中间移开。 它必须与 ggplot2 一起使用,即不使用额外的包。我做了一个包,不想依赖/包含太多包。

此处使用的数据:

df <- data.frame(x= rnorm(100), y= rnorm(100),
                 group1= rep(0:1, 50), group2= rep(2:3, each= 50))

【问题讨论】:

【参考方案1】:

在不使用其他软件包的情况下,我认为最好的方法是建立在您在原始问题中链接的空间解决方案的基础上。所以我写了一个函数来让标签间距更健壮一些。

ylabel <- function(label1,label2)
  L1 <- nchar(label1)
  L2 <- nchar(label2)
  scaler <- ifelse(L1 + L2 > 8, 4, 0)
  space1 = paste0(rep("",27 - (L1/2)),collapse = " ")
  space2 = paste0(rep("",44 - (L1/2 + L2/2) - scaler), collapse = " ")
  space3 = paste0(rep("",22 - (L2/2)), collapse = " ")
  paste0(space1,label1,space2,label2,space3)

应用:

test <- ylabel("automobiles", "trucks")
ggplot(df, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2) +
  ylab(test)

仍在使用scaler 参数,它并不完美:

test2 <- ylabel("super long label", "a")
ggplot(df, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2) +
  ylab(test2)

将继续完善功能/参数,但我认为这会让您更接近您正在寻找的东西。

【讨论】:

我喜欢这样,但在目前的形式下,它是不可扩展的——不能用于超过两行。此外,您应该尽量不要在基本 R 函数之后命名任何对象,而“c”可能是所有对象中最糟糕的选择 @tjebo 您的积分有效。我已经相应地调整了我的对象名称。另外,我承认我忽略了 OP 对灵活行数的要求。这为这个棘手的问题增加了一层。【参考方案2】:

您可以将轴标签复制到 gtable 中的新 grobs 中。请注意,虽然这使用了 gridgtable 包,但它们已经由 ggplot2 导入,因此这不会添加任何新的依赖项,这些依赖项尚不可用并由 ggplot 在内部使用。

library(grid)
library(gtable)

g = ggplot(df, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)

gt = ggplot_gtable(ggplot_build(g))
which.ylab = grep('ylab-l', gt$layout$name)
gt = gtable_add_grob(gt, gt$grobs[which.ylab], 8, 3)
gt = gtable_add_grob(gt, gt$grobs[which.ylab], 10, 3)
gt = gtable_filter(gt, 'ylab-l', invert = TRUE) # remove the original axis title
grid.draw(gt)

以上适用于只有两个方面的 OP 示例。如果我们想将其推广到任意数量的方面,我们可以通过搜索 gtable 以查看哪些行包含 y 轴来做到这一点。

gt = ggplot_gtable(ggplot_build(g))
which.ylab = grep('ylab-l', gt$layout$name)
which.axes = grep('axis-l', gt$layout$name)
axis.rows  = gt$layout$t[which.axes]
label.col  = gt$layout$l[which.ylab]
gt = gtable::gtable_add_grob(gt, rep(gt$grobs[which.ylab], length(axis.rows)), axis.rows, label.col)
gt = gtable::gtable_filter  (gt, 'ylab-l', invert = TRUE) 
grid::grid.draw(gt)

在上面的版本中,我还使用:: 来明确指定来自 grid 和 gtable 包的函数的命名空间。这将允许代码工作,甚至无需将其他包加载到搜索路径中。

用另一个具有四个方面行的示例演示此代码:

df <- data.frame(x= rnorm(100), y= rnorm(100),
                 group1= rep(1:4, 25), group2= rep(1:2, each= 50))

【讨论】:

真的很喜欢那个解决方案。但是关于依赖项-(我个人不介意再添加一些依赖项)-我认为即使 ggplot2 导入了这些包的整个命名空间,如果您明确使用这些函数,您仍然需要直接从这些包中导入,因此还将其添加到描述中的依赖项中。 是的,您仍然需要导入它们,但考虑到它们必须已经安装,我不明白会有什么缺点。 "这些都已经被ggplot2导入了" 是不是意味着任何安装了ggplot2的人都可以运行library(grid); library(gtable)之后的代码? 是的。虽然如果你想在包中使用代码,你会做的略有不同。在包中,不是使用library 函数,而是在一个名为DESCRIPTION 的文件中列出要导入的包(例如参见here)。如果你在你的包中使用 ggplot2,你应该已经这样做了以访问 ggplot 函数。 哦,不,我错误地将赏金给了另一个答案,因为它显示在顶部。我希望接受的答案位于顶部。无法撤消..【参考方案3】:

您可以考虑切换到库(cowplot)以获得更多控制

以下代码可以添加到函数中,但为了清楚起见,我把它留了很长时间。创建 4 个数据框并将它们提供给四个图。然后安排地块

library(tidyverse)
df <- data.frame(x= rnorm(100), y= rnorm(100),
                 group1= rep(0:1, 50), group2= rep(2:3, each= 50))


library(cowplot)
df1 <- df %>% 
  filter(group2 == 2) %>% 
         filter(group1 == 0)

df2 <- df %>% 
  filter(group2 == 3) %>% 
  filter(group1 == 0)

df3 <- df %>% 
  filter(group2 == 2) %>% 
  filter(group1 == 1)

df4 <- df %>% 
  filter(group2 == 3) %>% 
  filter(group1 == 1)

plot1 <- ggplot(df1, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)+
  xlim(c(-3, 3))+
  ylim(c(-3, 2))+
  theme(strip.text.y = element_blank(), 
        axis.title.x = element_blank(), 
        axis.text.x = element_blank(), 
        axis.ticks.x = element_blank()
        )
plot1


plot2 <- ggplot(df2, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)+
  xlim(c(-3, 3))+
  ylim(c(-3, 2))+
  theme(axis.title.y = element_blank(), 
        axis.text.y = element_blank(), 
        axis.ticks.y = element_blank(), 
        axis.title.x = element_blank(), 
        axis.text.x = element_blank(), 
        axis.ticks.x = element_blank()
        )
plot2


plot3 <- ggplot(df3, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)+
  xlim(c(-3, 3))+
  ylim(c(-3, 2))+
  theme(strip.text.x = element_blank(),
        strip.text.y = element_blank())
plot3


plot4 <- ggplot(df4, aes(x, y)) +
  geom_point() +
  facet_grid(group1 ~ group2)+
  xlim(c(-3, 3))+
  ylim(c(-3, 2))+
  theme(axis.title.y = element_blank(), 
        strip.text.x = element_blank(),
        axis.text.y = element_blank(), 
        axis.ticks.y = element_blank())
plot4

plot_grid(plot1, plot2, plot3, plot4)

【讨论】:

【参考方案4】:

这是一个带有注释的版本,仅使用 ggplot2。它应该是可扩展的。

不要弄乱grobs。缺点是 x 定位和绘图边距需要半手动定义,这可能不是很可靠。

library(ggplot2)

df <- data.frame(x= rnorm(100), y= rnorm(100),
                 group1= rep(0:1, 50), group2= rep(2:3, each= 50))

## define a new data frame based on your groups, so this is scalable
annotate_ylab <- function(df, x, y, group1, group2, label = "label") 
  ## make group2 a factor, so you know which column will be to the left
  df[[group2]] <- factor(df[[group2]])
  lab_df <- data.frame( 
    ## x positioning is a bit tricky,
    ## I think a moderately robust method is to
    ## set it relativ to the range of your values
    x = min(df[[x]]) - 0.2 * diff(range(df[[x]])),
    y = mean(df[[y]]),
    g1 = unique(df[[group1]]),
    ## draw only on the left column
    g2 = levels(df[[group2]])[1],
    label = label
  )
  names(lab_df) <- c(x, y, group1, group2, "label")
  lab_df


y_df <- annotate_ylab(df, "x", "y", "group1", "group2", "y")

ggplot(df, aes(x, y)) +
  geom_point() +
  geom_text(data = y_df, aes(x, y, label = label), angle = 90) +
  facet_grid(group1 ~ group2) +
  coord_cartesian(xlim = range(df$x), clip = "off") +
  theme(axis.title.y = element_blank(), 
        plot.margin = margin(5, 5, 5, 20))

y_df_mtcars <- annotate_ylab(mtcars, "mpg", "disp", "carb", "vs", "y")

ggplot(mtcars, aes(mpg, disp)) +
  geom_point() +
  geom_text(data = y_df_mtcars, aes(mpg, disp, label = label), angle = 90) +
  facet_grid(carb ~ vs) +
  coord_cartesian(xlim = range(mtcars$mpg), clip = "off") +
  theme(axis.title.y = element_blank(), 
        plot.margin = margin(5, 5, 5, 20))

由reprex package (v2.0.1) 于 2021 年 11 月 24 日创建

【讨论】:

这个想法很棒 (+1)。不幸的是,正如你提到的,它并不健壮。我尝试将df &lt;- mtcars; df$x &lt;- df$mpg; df$y &lt;- df$disp; df$group1 &lt;- as.factor(df$carb); df$group2 &lt;- as.factor(df$vs) 作为新数据,在结果图中,y 标签被 y 值吃掉了。 @machine 我发现了几分钟 :) 我认为在相对于您的值范围进行定位时,它应该更加健壮 - 请参阅我的更新。 我想这已经差不多完成了。我注意到的是,比例也必须根据列面的数量进行调整。使用具有比vs 更多级别的变量作为group2 再次移动y 值中的y 标签。例如,尝试使用y_df_mtcars &lt;- annotate_ylab(mtcars, "mpg", "disp", "carb", "cyl", "y") ... facet_grid(carb ~ cyl) + ...。我将x 更改为min(df[[x]]) - 0.25 * diff(range(df[[x]])) * (length(levels(df[[group2]]))*.4),这似乎在这里工作。尚未尝试其他数据。 我猜这个解决方案是有缺陷的,那就是总会有它无法工作的情况……猜猜这取决于你对用户最有可能如何使用它的期望。考虑所有用例可能会很棘手。我认为 dww 的解决方案是最健壮的,即使过去内部的 grob 结构发生了变化,也没有理由认为将来不会发生这种情况,即使这样我想它也不需要太多的调整来更新你的相应地打包

以上是关于如何为ggplot中的每个构面行添加y轴标题?的主要内容,如果未能解决你的问题,请参考以下文章

如何为构面添加不同的线

根据 p 值有条件地将 geom_smooth 添加到 ggplot 构面

R ggplot facet label在y轴标题和y轴刻度标签之间的位置

将空图添加到构面,并与另一个构面结合

在 ggplot 中使用 NA 值创建连续折线图并添加辅助 y 轴

ggplot中hline上的错误栏