# Spacing of multi-panel figures in R

**Datavore Consulting » R**, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)

Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

In a previous **post**, I showed how to keep text and symbols at the same size across figures that have different numbers of panels. The figures in that post were ugly because they used the default panel spacing associated with the **mfrow** argument of the **par( )** function. Below I will walk through how to adjust the spacing of the panels when using **mfrow**.

**For this example, we will use ****Edgar Anderson’s iris data**, which is distributed with R. The data set includes flower measurements for 3 iris species. In this case, the data would be more effectively plotted in a single panel with different colors or symbols for each species, but with larger data sets the different colors/symbols can create a jumbled mess and multi-panel figures illustrate patterns in the data more clearly.

To plot all the data on the same scale, we need to extract the max and min values of the variables that we are plotting.

min.width = min(iris$Sepal.Width) min.length = min(iris$Sepal.Length) max.width = max(iris$Sepal.Width) max.length = max(iris$Sepal.Length)

The next block of code plots 3 panels in a 2×2 arrangement with mostly default options. [Note: The optimal way to plot this data in a multi-panel figure is a 3×1 arrangement, but using the 2×2 arrangement provides a better illustration of how **mfrow** works.] An empty panel is created by calling **plot.new( )**. The empty panel can be placed in any of the 4 positions, but it is redundant to use **plot.new( )** for the bottom right panel because **mfrow** fills the graphics device by row, moving left to right along the row. I am writing to the PNG graphics device to post the figures online; using a different graphics device (e.g., TIFF) might require adjustments to the arguments that control the panel spacing.

png(filename="PanelFigure1.png",width=4,height=4,units="in",res=150) par(mfrow=c(2,2), tcl=-0.5, family="serif") # Top left panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="virginica"), xlab="Sepal Width", ylab="Sepal Length", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) # Top right panel plot.new() # Bottom left panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="versicolor"), xlab="Sepal Width", ylab="Sepal Length", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) # Bottom right panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="setosa"), xlab="Sepal Width", ylab="Sepal Length", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) dev.off()

Yikes! What a mess! We can use the **mai** argument to the **par( )** function to specify the margin (in inches) of each panel in the figure. It takes a little trial-and-error to hit on margins that produce the desired spacing. We can start to reduce the redundancy in the figure by removing the labels for the x- and y-axes.

png(filename="PanelFigure2.png",width=4,height=4,units="in",res=150) par(mfrow=c(2,2), tcl=-0.5, family="serif", mai=c(0.3,0.3,0.3,0.3)) # Top left panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="virginica"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) # Top right panel plot.new() # Bottom left panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="versicolor"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) # Bottom right panel plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="setosa"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) dev.off()

Not bad, but we still have redundancy by including numbers on the x-axis scale in the top left panel and the y-axis scale in the bottom right panel. When we drop the numbers from those panels, it will create more white space. But we want to use as much white space as possible for plotting. Instead of including only a single call to the **par( )** function to change the **mai** argument, we will call** par( )** before plotting each panel to customize the size of the margin on each side of the plot for each panel. The **mai** argument specifies the margin by sides, i.e., c(bottom, left, top, right). In the top left panel, we want a smaller bottom margin because we are going to drop the numbers from the x-axis scale. In the bottom left panel, we want larger margins on the bottom and left because we need to include the axis scales on those sides. Again, you simply use trial-and-error to get the margins how you want them, but there is one little catch. If you want all of your panels to have plots that fill the same area, then the bottom+top and left+right margins need to be the same in all panels. In our example, the sum is 0.4 for both, but bottom+top does not need to equal left+right.

png(filename="PanelFigure3.png",width=4,height=4,units="in",res=150) par(mfrow=c(2,2), tcl=-0.5, family="serif") # Top left panel par(mai=c(0.2,0.4,0.2,0)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="virginica"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length), xaxt="n") axis(1, labels=F) # Top right panel plot.new() # Bottom left panel par(mai=c(0.4,0.4,0,0)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="versicolor"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length)) # Bottom right panel par(mai=c(0.4,0.2,0,0.2)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="setosa"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length), yaxt="n") axis(2, labels=F) dev.off()

The panels are now well placed within the figure. We just need to add a little extra space around the outer margin to place our axis labels. The **omi** argument to the **par( )** function specifies the outer margins (in inches) with the sides listed in the same order as for **mai**. The **mtext( )** function allows you to place text in the outer margin. I have also labeled each panel by species, which required some small adjustments to the y-axis scale.

png(filename="PanelFigure4.png",width=4,height=4,units="in",res=150) par(mfrow=c(2,2), tcl=-0.5, family="serif", omi=c(0.2,0.2,0,0)) # Top left panel par(mai=c(0.2,0.4,0.2,0)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="virginica"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length+0.15), xaxt="n",bty="n") axis(1, labels=F) text((max.width-min.width)/2 + min.width, max.length+0.15, expression(italic("Iris virginica"))) # Top right panel plot.new() # Bottom left panel par(mai=c(0.4,0.4,0,0)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="versicolor"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length+0.15),bty="n") text((max.width-min.width)/2 + min.width, max.length+0.15, expression(italic("Iris versicolor"))) # Bottom right panel par(mai=c(0.4,0.2,0,0.2)) plot(Sepal.Length~Sepal.Width, data=iris, subset=(Species=="setosa"), xlab=" ", ylab=" ", xlim=c(min.width,max.width), ylim=c(min.length,max.length+0.15), yaxt="n",bty="n") axis(2, labels=F) text((max.width-min.width)/2 + min.width, max.length+0.15, expression(italic("Iris setosa"))) mtext("Sepal Width", side=1, outer=T, at=0.5) mtext("Sepal Length", side=2, outer=T, at=0.5) dev.off()

We now have a figure that is several steps closer to being publication quality. The main adjustment left to make is customizing the scales of the axes. In another **post**, I demonstrate how to customize the axes scales, which is particularly useful when presenting transformed data on the original scale.

Lastly, I want to point out how useful the lattice package is for quickly generating multi-panel figures with grouped data. As in the R base package, the default is not publication quality, but it requires a lot less code to generate a multi-panel figure that is at least easily readable.

library(lattice) xyplot(Sepal.Length~Sepal.Width|Species,data=iris)

**leave a comment**for the author, please follow the link and comment on their blog:

**Datavore Consulting » R**.

R-bloggers.com offers

**daily e-mail updates**about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.

Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.