在绘制数据前,是否可能先绘制坐标轴线?

9
这是对我之前问题的跟进我当时在寻找一个解决方案,让轴先绘制,然后再是数据。答案适用于那个具体的问题和示例,但它引出了一个更普遍的问题如何改变底层 grobs 的绘图顺序。首先是轴,然后是数据。

很像面板网格 grob 可以上下绘制一样。

面板网格和轴 grobs 显然生成方式不同 - 轴更多地作为指导对象而非“简单”的 grobs。(轴是用ggplot2:::draw_axis()绘制的,而面板网格是作为ggplot2:::Layout对象的一部分构建的)。

我猜这就是为什么轴被放在上面,我想知道是否可以改变绘图顺序。
# An example to play with 

library(ggplot2)
df <- data.frame(var = "", val = 0)

ggplot(df) + 
  geom_point(aes(val, var), color = "red", size = 10) +
  scale_x_continuous(
    expand = c(0, 0),
    limits = c(0,1)
  ) +
  coord_cartesian(clip = "off") +
  theme_classic() 


这可能不是你想要的,但是你可以尝试更改gtable中的“z”列,即 g = ggplotGrob(p) ; g$layout[g$layout$name == "panel", "z"] = 12 ; g$layout[g$layout$name == "ylab-l", "z" ] = 0 ; grid::grid.draw(g) - user20650
@user20650 是和不是。我认为这个方向非常正确。实际上,我通常更喜欢更「即时解决方案」,主要是出于好奇心。2)不确定为什么,但当我尝试交换z列时,轴的外观会改变。可能是设备问题。现在已经到了睡觉的时间,明天需要深入研究。已经谢谢! - tjebo
是的,我认为坐标轴线看起来有点细了...可能是因为面板现在覆盖了它们的一部分? - user20650
3个回答

8
ggplot可以通过其gtable来表示。各个组件的位置由layout元素给出,"z列用于定义组件的绘制顺序"

包含点组件的panelz值可以增加,以便最后绘制。

因此,如果p是您的图表,则:

g <- ggplotGrob(p) ;
g$layout[g$layout$name == "panel", "z"] <-  max(g$layout$z) + 1L
grid::grid.draw(g)

然而,正如评论中所指出的,这将改变轴线的外观,这也许是由于绘图区域覆盖了部分轴线。

但在一个新的令人兴奋的消息中,来自 dww:

如果我们在图形中添加theme(panel.background = element_rect(fill = NA)),则轴线不再被部分遮挡。这证明了这是较细轴线的原因,并提供了一个合理的解决方法,只要您不需要有彩色的面板背景。


(对于那些想要将轴放在点下方但不在意如何实现的人,另一种方法是提取点层并重新绘制: g <- grid::grid.force(ggplotGrob(p)) ; pos <- g$layout[g$layout$name == "panel", 1:5] ; g <- gtable::gtable_add_grob(x=ggplotGrob(p), grobs=grid::getGrob(g, "point", grep=TRUE), t=pos$t, l=pos$l, z=max(g$layout$z) + 1L, clip=FALSE) - user20650
4
如果我们在图形中添加theme(panel.background = element_rect(fill = NA)),那么坐标轴将不再被部分遮挡。这证明了这是细线条的原因,并且提供了一个合理的解决方法,只要你不需要有着颜色的面板背景。 - dww

6

既然您正在寻找更高级的解决方案,那么开始的地方就是问“ggplot首先是如何绘制的?”答案可以在ggplot对象的print方法中找到:

ggplot2:::print.ggplot
#> function (x, newpage = is.null(vp), vp = NULL, ...) 
#> {
#>     set_last_plot(x)
#>    if (newpage) 
#>         grid.newpage()
#>     grDevices::recordGraphics(requireNamespace("ggplot2", 
#>         quietly = TRUE), list(), getNamespace("ggplot2"))
#>     data <- ggplot_build(x)
#>     gtable <- ggplot_gtable(data)
#>     if (is.null(vp)) {
#>         grid.draw(gtable)
#>     }
#>     else {
#>         if (is.character(vp)) 
#>             seekViewport(vp)
#>         else pushViewport(vp)
#>         grid.draw(gtable)
#>         upViewport()
#>     }
#>     invisible(x)
#> }

在这里,你可以看到绘制ggplot实际上是通过对ggplot对象调用ggplot_build,然后对ggplot_build的输出调用ggplot_gtable来完成的。

困难之处在于,面板及其背景、网格线和数据是作为单个grob树创建的。然后将其嵌套为ggplot_build产生的最终grob表中的单个实体。轴线在该面板的“顶部”绘制。如果先绘制这些线,则它们的一部分厚度将被面板覆盖。如user20650的答案所述,如果您不需要绘图具有背景颜色,则这不是问题。

据我所知,除非您自己添加grob,否则没有本地方法将轴线包括在面板中。

以下一系列小函数允许您获取绘图对象,从中删除轴线,并将轴线添加到面板中:

get_axis_grobs <- function(p_table)
{
  axes <- grep("axis", p_table$layout$name)
  axes[sapply(p_table$grobs[axes], function(x) class(x)[1] == "absoluteGrob")]
}

remove_lines_from_axis <- function(axis_grob)
{
  axis_grob$children[[grep("polyline", names(axis_grob$children))]] <- zeroGrob()
  axis_grob
}

remove_all_axis_lines <- function(p_table)
{
  axes <- get_axis_grobs(p_table)
  for(i in axes) p_table$grobs[[i]] <- remove_lines_from_axis(p_table$grobs[[i]])
  p_table
}

get_panel_grob <- function(p_table)
{
  p_table$grobs[[grep("panel", p_table$layout$name)]]
}

add_axis_lines_to_panel <- function(panel)
{
  old_order <- panel$childrenOrder
  panel <- grid::addGrob(panel, grid::linesGrob(x = unit(c(0, 0), "npc")))
  panel <- grid::addGrob(panel, grid::linesGrob(y = unit(c(0, 0), "npc")))
  panel$childrenOrder <- c(old_order[1], 
                           setdiff(panel$childrenOrder, old_order),
                           old_order[2:length(old_order)])
  panel
}

现在,所有这些都可以协调到一个单一的函数中,使整个过程更加容易:
underplot_axes <- function(p)
{
  p_built <- ggplot_build(p)
  p_table <- ggplot_gtable(p_built)
  p_table <- remove_all_axis_lines(p_table)
  p_table$grobs[[grep("panel", p_table$layout$name)]] <-
    add_axis_lines_to_panel(get_panel_grob(p_table))
  grid::grid.newpage()
  grid::grid.draw(p_table)
  invisible(p_table)
}

现在,您只需在ggplot对象上调用underplot_axes即可。我稍微修改了您的示例以创建一个灰色背景面板,这样我们可以更清楚地看到正在发生的事情:
library(ggplot2)

df <- data.frame(var = "", val = 0)

p <- ggplot(df) + 
  geom_point(aes(val, var), color = "red", size = 10) +
  scale_x_continuous(
    expand = c(0, 0),
    limits = c(0,1)
  ) +
  coord_cartesian(clip = "off") +
  theme_classic() +
  theme(panel.background = element_rect(fill = "gray90"))

p

underplot_axes(p)

此内容创建于2021年5月7日,使用reprex package(v0.3.0)

现在,您可能认为这是“创建虚假轴”,但我更倾向于将其视为将轴线从grob树中的一个位置移动到另一个位置。很遗憾这个选项似乎没有内置到ggplot中,但我也可以看出,要允许该选项需要对构建ggplot的方式进行重大改进。


太好了,艾伦。学到了很多,谢谢! - tjebo

3

这里有一个技巧,不需要深入了解“引擎”,而是使用 patchwork 作为另一层添加在顶部,仅包含几何图层。

a <- [your plot above]

library(patchwork)
a + inset_element(a + them_void(), left = 0, bottom = 0, right = 1, top = 1)

enter image description here


我很感激这个建议,我之前不知道inset_element,而且我也很喜欢patchwork包。但是我并不是在问如何伪造数据看起来像是画在坐标轴上的效果。在这种情况下,我假设数据甚至被绘制了两次,这可能会对最终输出产生一些奇怪的影响。 - tjebo
我想提供一个简单的方法,适用于大多数情况,例如不使用alpha的情况。如果您的图形涉及alpha,则建议在原始geom_中将颜色设置为NA,然后在第二个图形中添加geom。我知道这不是您要寻找的优雅解决方案,但您肯定可以使用该技术在真实轴上绘制真实数据,而没有“伪造”。 - Jon Spring

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接