Search code examples
rggplot2svg

In ggplot2 how to remove all theme + remove some data but keep aspect ratio of the data displayed?


I'am trying to create a base plot and then recreate a modified version of the same plot without some of the data and without any other element (essentially + theme_void()). The difficulty here is to keep the exact size and position of the data that the plot keep between the two versions.

Say I have the following plot:

library(ggplot2)

# Sample data frame
d <- data.frame(group = c("A", "B", "C"),
                value = c(10, 15, 5))

# Create the original bar plot
g1 <- ggplot() +
  geom_col(data = d,
           aes(x = group, 
               y = value,
               fill = group)) +
  theme_bw()

enter image description here

The aim is to create (and save as .SVG) three plots with one bar per plot (+ theme_void) but with the same position/size as the first one.

Desired plot 1: enter image description here

Desired plot 2: enter image description here

Desired plot 3: enter image description here

I guess one possibility is to make everything else white/transparent, but I want to avoid this approach for I will further manipulate the plot saved as a .SVG, and the elements would be there haunting me (add complexity and bigger file size).

The other approach that I do want to pursue is to get in the middle of the ggplot2 workflow, stop it in the right time (the drawing context is already given), modify it (as in erase everything but a single bar), and finally render the modified plot.

The package gginnards has functions like delete_layers() and themes could be replaced with the %+% operator, but as far as I could see they modify the size/position (as it should, but this is not what I want).

The closest thing that I found is ggtrace package (particularly "highjack-ggproto") and the whole discussion (that is still very opaque for me) of grid/grob.

I guess I will be learning more on those issues in the weeks to come, but any advice on that would be very appreciated!

Edit: from the valuable answers below I must stress:

  1. This is a toy example, the real case will incorporate numerous theme modifications in the original plot. That´s is to say, a workaround of making the first plot more simple (that would facilitate the comparison) is not a solution in this case.

  2. The aim is to save the result in a clean SVG. By clean I mean there is suppose to be only the visible elements in the SVG file (as I inspect its source code). For example, if I have hundreads of points in my plot and I filter for one point, this single point should be alone in the new SVG (in the exact position as it was in the first plot - the plot that has multiple theme modification, title, legends, axis, and so on).


Solution

  • This is actually fairly difficult. The problem is that the exact position of the bars is determined by nested viewports. The easiest solution is probably just to walk the gTable of the ggplot object and make all objects that are not the bars be zeroGrobs

    Let's start with the plot itself:

    library(ggplot2)
    
    # Sample data frame
    d <- data.frame(group = c("A", "B", "C"),
                    value = c(10, 15, 5))
    
    # Create the original bar plot
    g1 <- ggplot() +
      geom_col(data = d,
               aes(x = group, 
                   y = value,
                   fill = group)) +
      theme_bw()
    

    Our first step is to build this into a gTable:

    gt <- ggplot_gtable(ggplot_build(g1))
    

    Note than from now on, if we want to draw the result, we can do:

    grid::grid.newpage()
    grid::grid.draw(gt)
    

    Now, let's make everything that isn't the panel a zero grob. The panel is always a gTree, so we can do:

    gt$grobs <- lapply(gt$grobs, function(x) {
      if(class(x)[1] == 'gTree') x else zeroGrob()
      })
    

    Note that this wipes everything except the panel, but leaves all the spacing the same:

    grid::grid.newpage()
    grid::grid.draw(gt)
    

    Now we want to do the same thing within the panel, removing everything that isn't a geom_rect grob:

    panel <- which(lengths(gt$grobs) > 3)
    
    gt$grobs[[panel]]$children <- lapply(gt$grobs[[panel]]$children, function(x) {
      if(grepl('geom_rect', x)) x else zeroGrob()
    })
    

    This leaves us just with our three bars:

    grid::grid.newpage()
    grid::grid.draw(gt)
    

    To get the individual bars in their own plots, we create three copies of the plot object

    gt_list <- list(gt1 = gt, gt2 = gt, gt3 = gt)
    

    Now we iterate through this list and remove all but one bar from each:

    rectangles <- which(lengths(gt$grobs[[panel]]$children) > 3)
    
    gt_list <- Map(function(x, i) {
      rect <- x$grobs[[panel]]$children[[rectangles]]
      rect$x <- rect$x[i]
      rect$y <- rect$y[i]
      rect$width <- rect$width[i]
      rect$height <- rect$height[i]
      rect$gp <- rect$gp[i]
      x$grobs[[panel]]$children[[rectangles]] <- rect
      x
    }, gt_list, seq_along(gt_list))
    

    We now have 3 plots with only a single graphical object in each one, yet the position of each graphical element is unchanged compared to the original plot.

    grid::grid.newpage()
    grid::grid.draw(gt_list[[1]])
    

    grid::grid.newpage()
    grid::grid.draw(gt_list[[2]])
    

    grid::grid.newpage()
    grid::grid.draw(gt_list[[3]])
    

    Furthermore, we can see that the resulting svg is not full of unnecessary invisible objects; only the bar is written to file:

    svg('my.svg')
    grid::grid.newpage()
    grid::grid.draw(gt_list[[1]])
    dev.off()
    

    Resulting in

    my.svg

    <?xml version="1.0" encoding="UTF-8"?>
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="504pt" height="504pt" viewBox="0 0 504 504" version="1.1">
    <g id="surface1">
    <rect x="0" y="0" width="504" height="504" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/>
    <path style=" stroke:none;fill-rule:nonzero;fill:rgb(97.254902%,46.27451%,42.745098%);fill-opacity:1;" d="M 52.492188 451.675781 L 169.050781 451.675781 L 169.050781 168.375 L 52.492188 168.375 Z M 52.492188 451.675781 "/>
    </g>
    </svg>
    

    And in case there are any nagging doubts that things don't line up, let's save the plots and animate them to prove it:

    gt <- ggplot_gtable(ggplot_build(g1))
    
    png('plot1.png')
    grid::grid.newpage()
    grid::grid.draw(gt)
    dev.off()
    
    Map(function(x, f) {
      png(f)
      grid::grid.newpage()
      grid::grid.draw(x)
      dev.off()
    }, gt_list, c('plot2.png', 'plot3.png', 'plot4.png'))
    
    library(magick)
    
    list.files(pattern = 'plot\\d+\\.png', full.names = TRUE) |> 
      image_read() |>
      image_join() |> 
      image_animate(fps=4) |> 
      image_write("barplot.gif")
    

    enter image description here

    Created on 2023-08-31 with reprex v2.0.2