Is it possible to draw the axis line first, before the data?
Asked Answered
P

3

9

This is a follow up to my previous question where I was looking for a solution to get the axis drawn first, then the data. The answer works for that specific question and example, but it opened a more general question how to change the plotting order of the underlying grobs. First the axis, then the data.

Very much in the way that the panel grid grob can be drawn on top or not.

Panel grid and axis grobs are apparently generated differently - axes more as guide objects rather than "simple" grobs. (Axes are drawn with ggplot2:::draw_axis(), whereas the panel grid is built as part of the ggplot2:::Layout object).

I guess this is why axes are drawn on top, and I wondered if the drawing order can be changed.

# 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() 

Phaeton answered 30/4, 2021 at 12:40 Comment(3)
This is likely not waht you want but how about changing the "z" column in the gtable i.e. g = ggplotGrob(p) ; g$layout[g$layout$name == "panel", "z"] = 12 ; g$layout[g$layout$name == "ylab-l", "z" ] = 0 ; grid::grid.draw(g)Culbertson
@Culbertson yes and no. I think this goes very much in the right direction. I'd generally prefer a more "on the draw level solution", actually mainly out of curiosity. 2) Not sure why though, but when I try that swapping of the z column, the appearance of the axis changes. Might be a device thing. Right now about time to go to bed here, will need to dig into that tomorrow. Thanks already!Phaeton
Yes, i think the axis lines look a bit thinner ... possible as the panel is now getting draw over part of them?Culbertson
S
6

Since you are looking for a more "on the draw level" solution, then the place to start is to ask "how is the ggplot drawn in the first place?". The answer can be found in the print method for ggplot objects:

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)
#> }

where you can see that a ggplot is actually drawn by calling ggplot_build on the ggplot object, then ggplot_gtable on the output of ggplot_build.

The difficulty is that the panel, with its background, gridlines and data is created as a distinct grob tree. This is then nested as a single entity inside the final grob table produced by ggplot_build. The axis lines are drawn "on top" of that panel. If you draw these lines first, part of their thickness will be over-drawn with the panel. As mentioned in user20650's answer, this is not a problem if you don't need your plot to have a background color.

To my knowledge, there is no native way to include the axis lines as part of the panel unless you add them yourself as grobs.

The following little suite of functions allows you to take a plot object, remove the axis lines from it and add axis lines into the panel:

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
}

These can all be co-ordinated into a single function now to make the whole process much easier:

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)
}

And now you can just call underplot_axes on a ggplot object. I have modified your example a little to create a gray background panel, so that we can see more clearly what's going on:

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)

Created on 2021-05-07 by the reprex package (v0.3.0)

Now, you may consider this "creating fake axes", but I would consider it more as "moving" the axis lines from one place in the grob tree to another. It's a shame that the option doesn't seem to be built into ggplot, but I can also see that it would take a pretty major overhaul of how a ggplot is constructed to allow that option.

Saros answered 7/5, 2021 at 14:47 Comment(1)
that's very nice, Allan. Have learned a lot, thanks !Phaeton
C
8

A ggplot can be represented by its gtable. The position of the grobs are given by the layout element, and "the z-column is used to define the drawing order of the grobs".

The z value for the panel, which contains the points grob, can then be increased so that it is drawn last.

So if p is your plot then

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

However, as noted in the comment this changes how the axis look, which perhaps, is due to the panel being drawn over some of the axis.

But in new exciting news from dww

if we add theme(panel.background = element_rect(fill = NA)) to the plot, the axes are no longer partially obscured. This both proves that this is the cause of the thinner axis lines, and also provides a reasonable workaround, provided you don't need a colored panel background.

Culbertson answered 4/5, 2021 at 9:51 Comment(2)
(for those that want the axis under the points and don't care how it is done, another way is to extract the ponts layer and redraw: 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))Culbertson
if we add theme(panel.background = element_rect(fill = NA)) to the plot, the axes are no longer partially obscured. This both proves that this is the cause of the thinner axis lines, and also provides a reasonable workaround, provided you don't need a colored panel background.Kile
S
6

Since you are looking for a more "on the draw level" solution, then the place to start is to ask "how is the ggplot drawn in the first place?". The answer can be found in the print method for ggplot objects:

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)
#> }

where you can see that a ggplot is actually drawn by calling ggplot_build on the ggplot object, then ggplot_gtable on the output of ggplot_build.

The difficulty is that the panel, with its background, gridlines and data is created as a distinct grob tree. This is then nested as a single entity inside the final grob table produced by ggplot_build. The axis lines are drawn "on top" of that panel. If you draw these lines first, part of their thickness will be over-drawn with the panel. As mentioned in user20650's answer, this is not a problem if you don't need your plot to have a background color.

To my knowledge, there is no native way to include the axis lines as part of the panel unless you add them yourself as grobs.

The following little suite of functions allows you to take a plot object, remove the axis lines from it and add axis lines into the panel:

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
}

These can all be co-ordinated into a single function now to make the whole process much easier:

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)
}

And now you can just call underplot_axes on a ggplot object. I have modified your example a little to create a gray background panel, so that we can see more clearly what's going on:

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)

Created on 2021-05-07 by the reprex package (v0.3.0)

Now, you may consider this "creating fake axes", but I would consider it more as "moving" the axis lines from one place in the grob tree to another. It's a shame that the option doesn't seem to be built into ggplot, but I can also see that it would take a pretty major overhaul of how a ggplot is constructed to allow that option.

Saros answered 7/5, 2021 at 14:47 Comment(1)
that's very nice, Allan. Have learned a lot, thanks !Phaeton
B
3

Here's a hack that doesn't require going "under the hood", but rather uses patchwork to add another layer on top that is just the geom layer.

a <- [your plot above]

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

enter image description here

Blynn answered 4/5, 2021 at 5:19 Comment(2)
I appreciate this suggestion, and I didn't know inset_element, and I love the patchwork package, but I was not asking how to fake the look of data drawn on top of an axis. In this case I assume the data is even drawn twice and this might make some weird effects in the final output.Phaeton
I wanted to give a simple method that would work for most cases, e.g. where alpha is not used. If your plot involves alpha, I would suggest setting color to NA in the original geom_, and then adding the geom in the 2nd plot. Understood that it's not the elegant solution you're looking for, but you can certainly use that technique to plot the real axis and then the real data on top, with no "faking."Blynn

© 2022 - 2024 — McMap. All rights reserved.