Exporting nice plots in R

It's not always easy getting the right size. The image is CC by Kristina Gill.
It’s not always easy getting the right size. The image is CC by Kristina Gill.

A vital part of statistics is producing nice plots, an area where R is outstanding. The graphical ablility of R is often listed as a major reason for choosing the language. It is therefore funny that exporting these plots is such an issue in Windows. This post is all about how to export anti-aliased, high resolution plots from R in Windows.

There are two main problems when exporting graphics from R:

My previous solution to this problem has been to export my graph to a vector graphic (usually the SVG format), open it in Inkscape, and then export it to the resolution of choice. The vector graphics allows smooth scaling without me having to worry about the text becoming too small to read while at the same time adding the anti-aliasing. The down-side of this is that it is not really a convenient work-flow when you create knitr/Sweave documents where many plots are simultaneously generated.

The post has several sections:

  1. Raw export functions
  2. The Cairo-package
  3. The cairoDevice-package
  4. Raw export functions with the type=”cairo” activated
  5. Fills and anti-aliasing

Exporting

Let us start by looking at an exported plot from RStudio, clearly not something I would like to send to a journal:

A PNG Export from RStudio. Note the ragged looking line and the sharp letters.
A PNG Export from RStudio. Note the ragged looking line and the sharp letters.

So the first alternative would be the standard library. We can kick-off with the svg() function as I mentioned it earlier (Note: If you can’t see the plot then your browser is probably old and does not support svg images):

?View Code RSPLUS
1
2
3
4
5
6
svg(filename="Std_SVG.svg", 
    width=5, 
    height=4, 
    pointsize=12)
my_sc_plot(data)
dev.off()

A scalable vector image of the plot by the standard svg() function. The plot has long labels to enhance size comparison between different plots.

All images in this post use the same basic settings with minor tweaks:

  • Width = 5 inches
  • Height = 4 inches
  • pointsize = 12

If we now do the same using the png() command we also have to set the resolution. I’m using 72 dpi (pixels/inch), as 1 point by the modern definition is 1/72 of the international inch and therefore a good start instead of the 96 dpi that Windows uses. As you can see the size of the labels are very similar to the svg():

?View Code RSPLUS
1
2
3
4
5
6
7
8
png(filename="Std_PNG.png", 
    units="in", 
    width=5, 
    height=4, 
    pointsize=12, 
    res=72)
my_sc_plot(data)
dev.off()
A PNG image based on the png() command.
A PNG image based on the png() command. It is clearly not as smooth as we would want it to be…

Note: The Cairo packages are obsolete as it usually is built in nowadays, all you need to do is remember to reference it – see my update

The Cairo-package

The Cario package is an excellent choice for getting antia-aliased plots in Windows. Here’s the same plot from the Cairo package (the background is now transparent):

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
library(Cairo)
Cairo(file="Cairo_PNG_72_dpi.png", 
      type="png",
      units="in", 
      width=5, 
      height=4, 
      pointsize=12, 
      dpi=72)
my_sc_plot(data)
dev.off()
The same plot using the Cairo package, note the much smoother line and text
The same plot using the Cairo package, note the much smoother line and text

If we want to increase the resolution of the plot we can’t just change the resolution parameter:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
Cairo(file="Cairo_PNG_96_dpi.png", 
      type="png",
      units="in", 
      width=5, 
      height=4, 
      pointsize=12, 
      dpi=96)
my_sc_plot(data)
dev.off()
A 96 dpi version of the same image
A 96 dpi version of the same image

We also have to change the point size, the formula is size * new resolution DPI / 72 DPI:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
Cairo(file="Cairo_PNG_96_dpi_adj.png", 
      type="png",
      units="in", 
      width=5, 
      height=4, 
      pointsize=12*92/72, 
      dpi=96)
my_sc_plot(data)
dev.off()
Adjusted point size results in the labels remaining the proper size
Adjusted point size results in the labels remaining the proper size

If we double the image size we also need to double the point size:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
Cairo(file="Cairo_PNG_72_dpi_x_2.png", 
      type="png",
      units="in", 
      width=5*2, 
      height=4*2, 
      pointsize=12*2, 
      dpi=72)
my_sc_plot(data)
dev.off()
Double size image
Double size image

The cairoDevice package

An alternative to the Cairo package is the cairoDevice package. It uses the same engine but has a slightly different syntax and due to it’s limitation in setting the resolution it really doesn’t work that well. Lets try the same as above:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
11
# Detach the Cairo package just to make
# sure that there aren't any conflicts
detach("package:Cairo")
library(cairoDevice)
Cairo(filename="cairoDevice_SVG_pt12.svg", 
      surface="png", 
      width=5, 
      height=4, 
      pointsize=12)
my_sc_plot(data)
dev.off()
A completely differently looking image - not even remotely close to the original
A completely differently looking image – not even remotely close to the original

To be fair the default point size of the cairoDevice package, 8 points is less than for the Cairo package but even with that setting the image looks not that great:

Using the default 8 points still looks funny
Using the default 8 points still looks funny

After some tweaking I found that 5.2 points was fairly similar:

A tweaked cairoDevice plot - looks OK but the font is a big disappointment
A tweaked cairoDevice plot – looks OK but the font is a big disappointment

Summary

The Cairo package does the job nicely and without too much hassle you can get the look exactly right. If you want the background to be white just add the bg parameter:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
Cairo(file="Cairo_PNG_72_dpi_white.png", 
      bg="white",
      type="png",
      units="in", 
      width=5, 
      height=4, 
      pointsize=12, 
      dpi=72)
my_sc_plot(data)
dev.off()
The same as above but with a white background
The same as above Cairo images but with a white background

Update

After Matt Neilsons excellent comment I’ve discovered that the standard export functions actually allow for anti-aliasing with built-in cairo support. Another excellent part is that the dpi now works exactly as expected – you don’t need to adjust the point size :-)

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
png(filename="Std_PNG_cairo.png", 
    type="cairo",
    units="in", 
    width=5, 
    height=4, 
    pointsize=12, 
    res=96)
my_sc_plot(data)
dev.off()
A 96 DPI plot with the standard package and the type set to "cairo". A clear winner!
A 96 DPI plot with the standard package and the type set to “cairo”. A clear winner!

Here’s the plot code:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Create the data
data <- rnorm(100, sd=15)+1:100
# Create a simple scatterplot
# with long labels to enhance 
# size comparison
my_sc_plot <- function(data){
  par(cex.lab=1.5, cex.main=2)
  plot(data, 
       main="A simple scatterplot", 
       xlab="A random variable plotted", 
       ylab="Some rnorm value",
       col="steelblue")
  x <- 1:100
  abline(lm(data~x), lwd=2)
}

Lines and text anti-aliased – not fills/polygons

I recently found out the hard way that the png(…, type=”cairo”) will anti-alias everything except fills/polygons. This is due to the fact that image-maps risk of becoming distorted by the anti-aliasing function. It is therefore good to know that if you have a fill, make sure that it has a line-border if you want it to become anti-aliased. This is especially important in the ggplot2 that does not set the border by default (lattice and plain plots do).

Below is a simple example to illustrate:

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
n <- 10
xx <- c(0:n, n:0)
yy <- c(xx[1:(n+1)] + 2, xx[(n+2):(n*2 + 2)] -2)
png(height=400, width=800, filename="plot_w_cairo.png", type="cairo")
org_par <- par(mfrow=c(1,2), cex=1.5, mai=par("mai")*.75)
# First plot
plot   (xx, yy, type = "n", xlab="", ylab="", main="Without line color")
polygon(xx, yy, col = "black", border=NA)
abline(3, 1, lwd=3)
# Second plot
plot   (xx, yy, type = "n", xlab="", ylab="", main="With line color")
polygon(xx, yy, col = "black")
abline(3, 1, lwd=3)
# Reset values
par(org_par)
dev.off()
Basic plots two images with and without line colors. Note that the one to the right is properly anti-aliased.
Basic plots two images with and without line colors. Note that the one to the right is properly anti-aliased.

Now for the ggplot2 version. Note that the color line is by default NA.

?View Code RSPLUS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(ggplot2)
library(grid)
df <- data.frame(x=xx, y=yy)
png(height=400, width=800, filename="ggplot_w_cairo.png", type="cairo")
# Generate base plot
pushViewport(viewport(layout = grid.layout(1, 2)))
g <- ggplot(df, aes(x=x, y=y)) +
  geom_abline(aes(intercept=3, slobe=1), size=3) + 
  xlab("") + ylab("") + theme(text = element_text(size=25))
# First plot
print(g + geom_polygon() + ggtitle("Without line color"), vp = viewport(layout.pos.row = 1, layout.pos.col = 1))
# Seconde plot
print(g + geom_polygon(col="black") + ggtitle("With line color"), vp = viewport(layout.pos.row = 1, layout.pos.col = 2))
dev.off()
Two ggplot images with and without line colors. Note that the one to the right is properly anti-aliased.
Two ggplot images with and without line colors. Note that the one to the right is properly anti-aliased.

I hope that you find all of this useful.

flattr this!

This entry was posted in R. Bookmark the permalink.

6 Responses to Exporting nice plots in R

  1. Huidong Tian says:

    Using the combination of “par”, “axis” and “mtext”, you can adjust your figures’ details.

    • Max Gordon says:

      Yes, those are some of the ways to pre-export get the size of the labels to the right size. One of the issues with just using those is that I can create a 1500×1500 plot with nice axes but the points (circles in this case) will not adjust to my large canvas (as I’m aware of – although it would be an awesome R-feature if we could specify text size as npc). I find this knowledge of converting graph size very convenient as I often send low-resolution documents to my co-authors and just prior to publication I go for the 600 DPI images.

  2. Matt Neilson says:

    R has support for Cairo built in in most cases, and you can take advantage of it through the ‘type’ argument of png(). type = ‘cairo’ uses R’s built in png surface drawing, and type = ‘cairo-png’ will use Cairo’s png backend to do all drawing.

    • Max Gordon says:

      Thank you Matt for discovering this hidden gem! I had completely missed that option. For some reason I stumbled upon the Cairo packages when I searched for anti-aliasing and R on Windows machines, and with the knitr extensive support for the Cairo devices I didn’t think of questioning that wisdom…

      It’s a pity that it’s not the default option on Windows machines, or that at least the Cairo packages have a warning when loading that there is a better option.

  3. Carlos Uribe says:

    Nice tutorial… I just have a quick question. How can I increase the left margin a little bit so that the Y-axis label has some more white space to the left?

    • Max Gordon says:

      Thank you. The margin is a little different for the ggplot2/lattice but for basic graphs you just set the mar or the omar settings using the par() command. See code below:

      ?View Code RSPLUS
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      data <- rnorm(100, sd=15)+1:100
      # Create a simple scatterplot
      # with long labels to enhance 
      # size comparison
      my_sc_plot <- function(data){
        org_mar <- par("mar")
        m <- org_mar
        # Increase 20 %
        m[2] <- m[2]*1.2
        par(cex.lab=1.5, cex.main=2, mar=m)
        plot(data, 
             main="A simple scatterplot", 
             xlab="A random variable plotted", 
             ylab="Some rnorm value",
             col="steelblue")
        x <- 1:100
        abline(lm(data~x), lwd=2)
       
        # Go back to original setting for mar
        par(mar = org_mar)
      }
       
      my_sc_plot(data)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>