【问题标题】:Is it possible to draw the axis line first, before the data?是否可以在数据之前先绘制轴线?
【发布时间】:2021-07-23 19:25:28
【问题描述】:

这是to my previous question 的后续行动,我在其中寻找一种解决方案,先绘制轴,然后绘制数据。答案适用于该特定问题和示例,但它提出了一个更一般的问题,即如何更改底层 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 是和否。我认为这非常朝着正确的方向发展。我通常更喜欢“绘制级别的解决方案”,实际上主要是出于好奇。 2)虽然不知道为什么,但是当我尝试交换 z 列时,轴的外观发生了变化。可能是设备问题。现在是时候在这里睡觉了,明天需要深入研究。已经谢谢了!
  • 是的,我认为轴线看起来有点细......可能因为面板现在正在绘制其中的一部分?

标签: r ggplot2 grob


【解决方案1】:

既然您正在寻找更“在绘图级别”的解决方案,那么首先要问“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_build,然后在 ggplot_build 的输出上调用 ggplot_gtable 来看到实际绘制的 ggplot。

困难在于面板及其背景、网格线和数据被创建为一个独特的 grob 树。然后将其作为单个实体嵌套在ggplot_build 生成的最终 grob 表中。轴线绘制在该面板的“顶部”。如果你先画这些线,它们的部分粗细会被面板过度绘制。正如 user20650 的回答中提到的,如果您不需要绘图具有背景颜色,这不是问题。

据我所知,除非您自己将轴线添加为 grobs,否则无法将轴线作为面板的一部分包含在内。

以下小功能套件允许您获取绘图对象,从中删除轴线并将轴线添加到面板中:

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)

reprex package (v0.3.0) 于 2021-05-07 创建

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

【讨论】:

  • 这很好,艾伦。学到了很多,谢谢!
【解决方案2】:

ggplot 可以用它的gtable 表示。 grobs 的位置由layout 元素和"the z-column is used to define the drawing order of the grobs" 给出。

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

所以如果p 是你的情节,那么

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

但是,as noted in the comment 这会改变轴的外观,也许是由于面板被绘制在某些轴上。

但在来自dww 的令人兴奋的新消息中

如果我们将theme(panel.background = element_rect(fill = NA)) 添加到绘图中,则坐标轴不再被部分遮挡。这既证明了这是导致轴线变细的原因,也提供了一个合理的解决方法,前提是您不需要彩色面板背景。

【讨论】:

  • (对于那些想要轴在点下而不关心它是如何完成的,另一种方法是提取ponts层并重绘:g &lt;- grid::grid.force(ggplotGrob(p)) ; pos &lt;- g$layout[g$layout$name == "panel", 1:5] ; g &lt;- 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)
  • 如果我们将theme(panel.background = element_rect(fill = NA)) 添加到绘图中,轴将不再被部分遮挡。这既证明了这是导致轴线变细的原因,也提供了一种合理的解决方法,前提是您不需要彩色面板背景。
【解决方案3】:

这是一个不需要“深入了解”的 hack,而是使用 patchwork 在顶部添加另一个层,即 geom 层。

a <- [your plot above]

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

【讨论】:

  • 我很欣赏这个建议,我不知道 inset_element,我喜欢拼凑包,但我不是在问如何伪造在轴上绘制的数据的外观。在这种情况下,我假设数据甚至被绘制了两次,这可能会在最终输出中产生一些奇怪的效果。
  • 我想提供一种适用于大多数情况的简单方法,例如不使用 alpha 的地方。如果您的情节涉及 alpha,我建议在原始 geom_ 中将颜色设置为 NA,然后在第二个情节中添加 geom。了解这不是您正在寻找的优雅解决方案,但您当然可以使用该技术来绘制实轴,然后在顶部绘制真实数据,而不会“伪造”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-03
  • 2014-04-05
  • 1970-01-01
  • 1970-01-01
  • 2012-06-02
  • 1970-01-01
相关资源
最近更新 更多