One approach is to create a graph for each non-empty factor level and an empty placeholder for each empty factor level:
Firstly, using the built-in mtcars data mtcars , we set the cut variable as a factor with 9 levels, but only 5 levels with any data:
library(ggplot2) library(grid) library(gridExtra) d = mtcars set.seed(4193) d$cyl = sample(1:9, nrow(d), replace=TRUE) d$cyl <- factor(d$cyl, levels=sort(unique(d$cyl))) d <- subset(d, cyl %in% c(1,5,7:9))
The for loop below goes through each level of the variable cut and creates a level graph with data or nullGrob (that is, an empty placeholder that displays the graph if there were data for this factor level) and adds it to the pl list.
for (i in 1:length(levels(d$cyl))) { if(i %in% blanks) { pl[[i]] = nullGrob() } else { pl[[i]] = ggplot(d[d$cyl %in% levels(d$cyl)[i], ], aes(x=am, y=wt) ) + geom_point() + facet_grid(.~ cyl) } }
Now lay out the plots and add a border around them:
do.call(grid.arrange, c(pl, ncol=3)) grid.rect(.5, .5, gp=gpar(lwd=2, fill=NA, col="black"))

UPDATE:. The function that I would like to add to my answer is to remove axis labels for graphs that are not in the leftmost column or bottom row (to be more similar to the format in OP). Below is my unsuccessful attempt.
The problem that arises when deleting axial marks and / or labels in some sections is that plot sections in different graphs have different sizes. The reason for this is that all graphs occupy the same physical region, but graphs with axis axes use some part of this region for axis labels, making their sections smaller than graphs without axis labels.
I was hoping I could solve this with plot_grid from the plot_grid package (created by @ClausWilke), but plot_grid does not work with nullGrob s. Then @baptiste added another answer to this question, which he has since deleted, but which remains visible to SO users with a reputation of at least 10,000 users. This answer let me know about its egg package and the set_panel_size function for setting the total panel size in different ggplots.
Below I tried to use set_panel_size to solve the scope area problem. This was not entirely successful, which I will discuss in more detail after showing the code and plot.
# devtools::install_github("baptiste/egg") library(egg) # Fake data for making a barplot. Once again we have 9 facet levels, # but with data for only 5 of the levels. set.seed(4193) d = data.frame(facet=rep(LETTERS[1:9],each=100), group=sample(paste("Group",1:5),900,replace=TRUE)) d <- subset(d, facet %in% LETTERS[c(1,5,7:9)]) # Identify factor levels without any data blanks = which(table(d$facet)==0) # Initialize a list pl = list() for (i in 1:length(levels(d$facet))) { if(i %in% blanks) { pl[[i]] = nullGrob() } else { # Create the plot, including a common y-range across all plots # (though this becomes the x-range due to coord_flip) pl[[i]] = ggplot(d[d$facet %in% levels(d$facet)[i], ], aes(x=group) ) + geom_bar() + facet_grid(. ~ facet) + coord_flip() + labs(x="", y="") + scale_y_continuous(limits=c(0, max(table(d$group, d$facet)))) # If the panel isn't on the left edge, remove y-axis labels if(!(i %in% seq(1,9,3))) { pl[[i]] = pl[[i]] + theme(axis.text.y=element_blank(), axis.ticks.y=element_blank()) } # If the panel isn't on the bottom, remove x-axis labels if(i %in% 1:6) { pl[[i]] = pl[[i]] + theme(axis.text.x=element_blank(), axis.ticks.x=element_blank()) } } # If the panel is a plot (rather than a nullGrob), # remove margins and set to common panel size if(any(class(pl[[i]]) %in% c("ggplot","gtable"))) { pl[[i]] = pl[[i]] + theme(plot.margin=unit(rep(-1,4), "lines")) pl[[i]] = set_panel_size(pl[[i]], width=unit(4,"cm"), height=unit(3,"cm")) } }
Now lay out the graphs:
do.call(grid.arrange, c(pl, ncol=3)) grid.rect(.5, .5, gp=gpar(lwd=2, fill=NA, col="black"))
As you can see in the graph below, even if the graphs have the same panel sizes, the fields between them are not constant, presumably due to the fact that grid.arrange processes the interval for zero levels, depending on which positions have actual plots . In addition, since set_panel_size sets the absolute dimensions, I had to manually determine the final result in order to assemble the panels as close to each other as possible, while avoiding overlapping. I hope one of the representatives of the SO residents will be grid and will offer a more efficient approach.
(Also note that with this approach, you can end without a marked graph in a given row or column. In the example below, there are no Y axis labels on the graph βEβ, and the graph βDβ is missing, so you need to look in another row, to see what the labels are. If only the graphs βBβ, βCβ, βEβ and βFβ were present, there would be no labeled graphs in the layout. I donβt know how the OP wants to deal with this situation (one of options would be adding logic to store labels on the "internal" graphs, if the "external" plot is about not present for a given row or column), but I thought it was worth pointing out.)
