Search code examples
rggplot2gridgrob

How to add a border to a rectangular rasterGrob in ggplot2?


I'm trying to add a border to a rectangular png image (found here) I've added to a ggplot, with positioning specified using npc.

library(png)
library(grid)
library(ggplot2)

img <- readPNG("gb.png")


g <- rasterGrob(img, x = unit(0.5, "npc"),
                y = unit(0.5, "npc"),
                width = unit(0.4, "npc"))

border <- rectGrob(x = unit(0.5, "npc"),
                   y = unit(0.5, "npc"),
                   width = unit(0.4, "npc"),
                   # height = resolveRasterSize(g)$height,
                   gp = gpar(lwd = 2, col = "black", fill="#00000000"))

myplot <- ggplot() +
  annotation_custom(g) +
  annotation_custom(border) +
  scale_x_continuous(limits = c(0, 1)) +
  scale_y_continuous(limits = c(0, 1))

Looks like this in the RStudio viewer:

enter image description here

As I've already specified the x and y co-ordinates and a width from the raster, it's easy enough to duplicate these for the border co-ordinates. As I haven't specified any height however, I'm not sure of the best way to figure out the npc to set the height for the border at. I don't set a height as I want to preserve any aspect ratios of the flags automatically according to the .png dimensions.

I looked at some functions which might help in grid like resolveRasterSize, which says you can

Determine the width and height of a raster grob when one or both are not given explicitly

And something else about the aspect/viewport, which I'm not too familar on how it affects plots created in ggplot2 exactly. Inside rectgrob with height = resolveRasterSize(g)$height the plot ends up looking like:

enter image description here

The border doesn't match up with the image. I noticed as well that the height variable created with resolveRasterSize is given an attribute with inches instead of npc.

If I resize the Plots plane, I noticed that height of both the flags and the border dynamically changes, and sometimes I can make it so they align, but I would want a more precise way of getting them aligned correctly, for example if I'm saving with different dimensions in ggsave or some usage.

I tried looking at other grid functions like convertHeight, with height = convertHeight(resolveRasterSize(g)$height, "npc") in rectGrob, which seems to always set the correct border in the Plot pane in RStudio, but if I resize the pane the border becomes misaligned again, and if I save with ggsave it is also misaligned.

ggsave(filename = "my_example.png", plot = myplot, width = 16, height = 9)

enter image description here


Solution

  • The problem, as you have correctly ascertained, is that the dimensions of your rectGrob are going to be affected differently by scaling of the plotting window than the dimensions of your rasterGrob. You can get round this using a little maths to correct for the aspect ratio of the raster and the plotting window. The only drawback is that you will have to rerun your calculation when you resize the plotting window. For most applications, this isn't a major problem. For example, to save as a 16 x 9 png file, you can do:

    img <- readPNG("gb.png")
    
    g <- rasterGrob(img, x = unit(0.5, "npc"),
                    y = unit(0.5, "npc"),
                    width = unit(0.4, "npc"))
    
    img_aspect <- dim(g$raster)[1] / dim(g$raster)[2]
    dev_aspect <- 16/9
    rect_aspect <- dev_aspect * img_aspect
    
    border <- rectGrob(x = unit(0.5, "npc"),
                       y = unit(0.5, "npc"),
                       width = g$width,
                       height = g$width * rect_aspect,
                       gp = gpar(lwd = 2, col = "black", fill="#00000000"))
    
    myplot <- ggplot() +
      annotation_custom(g) +
      annotation_custom(border) +
      scale_x_continuous(limits = c(0, 1)) +
      scale_y_continuous(limits = c(0, 1))
    
    ggsave(filename = "my_example.png",
           plot = myplot, width = 16, height = 9)
    

    which results in:

    my_example.png

    enter image description here

    If you want to get the border to fit on the current device in R Studio, then you can use

    dev_aspect <- dev.size()[1]/dev.size()[2]
    

    If you want a rectangle that scales whatever happens to the plot, then this can be done by creating a rasterGrob that contains a black border only.

    For example, if you do:

    border <- g$raster
    border[] <- "#00000000"
    border[1:2, ] <- "#000000FF"
    border[, 1:2] <- "#000000FF"
    border[nrow(border) + seq(-1, 0), ] <- "#000000FF"
    border[, ncol(border) + seq(-1, 0)] <- "#000000FF"
    
    border <- rasterGrob(border, x = unit(0.5, "npc"),
                    y = unit(0.5, "npc"),
                    width = unit(0.4, "npc"))
    
    myplot <- ggplot() +
      annotation_custom(g) +
      annotation_custom(border) +
      scale_x_continuous(limits = c(0, 1)) +
      scale_y_continuous(limits = c(0, 1))
    

    Then myplot will show a black border around the flag which persists with rescaling.