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()
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.
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:
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.
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).
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")
Created on 2023-08-31 with reprex v2.0.2