ggplot2 icon indicating copy to clipboard operation
ggplot2 copied to clipboard

`geom_tile()` not square when saving high res map (may be a more fundamental R graphics issue)

Open jack-davison opened this issue 1 year ago • 3 comments

A colleague is encountering an issue saving a large geographical heatmap made using geom_tile(); when saving this quite large, high-res plot as a PNG image the squares produced by geom_tile() aren't square!

reprex:

set.seed(123)
 
testdf <-
expand.grid(
  x = seq(500, 655500, 1000),
  y = seq(500, 655500, 1000)
) 
testdf |>
  dplyr::mutate(col = sample(letters[1:4], replace = TRUE, nrow(testdf))) |>
  ggplot(aes(x, y, fill = col)) +
  geom_tile()
 
ggsave("testrprex.png", dpi = 300, width = 15, height = 15)

This produces the below - annotations mine:

image

We believe this is to do with how R/ggplot2 interpolates between rectangles as when antialiasing is turned off (snap_rect = FALSE) the grid is a lot more regular, but has a sort of grid artefact where you can see horizontal/vertical lines - almost like a mosaic.

image

Is there a fix to avoid these non-square tiles when saving without getting the gridlines? As it is a spatial heatmap, it's important for the client to be able to zoom in quite close to each square, and they may question why some are rectangular!

Thanks very much

jack-davison avatar Aug 05 '24 11:08 jack-davison

To quickly clarify one point; geom_tile() is only expected to deliver square rectangles when the aspect ratio is fixed.

On to the main thing; I think the issue you're indicating is that the tiles have different pixel widths and columns look irregular. I've opened the figure in MS Paint, and some columns are 5 pixels wide, whereas others are 6 pixels wide.

ggplot2 doesn't really concert itself with how pixels are rendered in raster images. To get pixel-perfect tiles, you probably would have to manually calculate the size of the panels to an exact multiple of your number of tiles. I don't really have experience with this so I wouldn't be able to tell you the calculation on top of my head.

teunbrand avatar Aug 05 '24 12:08 teunbrand

Hi Teun,

To quickly clarify one point; geom_tile() is only expected to deliver square rectangles when the aspect ratio is fixed.

Yes, sorry, should have included that! The tiles are indeed a fixed aspect ratio - the "real world" data effectively follows the same pattern/limits the reprex - seq(500, 655500, 1000).

ggplot2 doesn't really concert itself with how pixels are rendered in raster images. To get pixel-perfect tiles, you probably would have to manually calculate the size of the panels to an exact multiple of your number of tiles. I don't really have experience with this so I wouldn't be able to tell you the calculation on top of my head.

Understood, thanks very much - I didn't think it was a {ggplot2} issue specifically so good to verify. We can see if we can do the maths wrt to the size of the panels to see if we can force them to be square.

jack-davison avatar Aug 05 '24 12:08 jack-davison

Using grid units in this helper will allow you to set the panel size in grid units, but I'm not 100% on top of how to translate grid units to pixels.

So if you have a 1000 tiles, each of 5 pixels let's say, you need 5000 pixels. By default, scale expansion adds 10% so that is 5500 pixels. At 300 dpi that should be 5500 / 300 ~= 18.33 inches. However, when we set the panel size that way, the exported figure has a mix of 7 or 8 pixels per tile, not 5. It appears thus that I've overlooked something.

teunbrand avatar Aug 05 '24 13:08 teunbrand

Another thing that occured to me is that using geom_raster() instead of geom_tile() should get rid of the mosaic-like pattern. Lastly, if you want to zoom in so close that individual pixels start to matter, you might need one of three things:

  1. An even larger output size so that 1-pixel differences are unnoticible.
  2. Graphics software that, at the device level, is aware you want to render these rectangles aligned to the pixel grid.
  3. Save the graph as vector graphics (svg/pdf) so that you can zoom indefinitely without losing quality.

Currently ggsave() offers (1) and (3) and given that ggplot2 lacks control over (2), I think it best to close this issue as there isn't anything within ggplot2's capabilities to do here.

teunbrand avatar Oct 25 '24 08:10 teunbrand

just to chime in as a developer of the (2) option. What you run into is basically the inability to divide 10 into 3 equally large integers. It is not possible. As Teun says, you can fix this by increasing the resolution (the error on dividing 100 into 3 equally large integers is much smaller than with 10), but you are much better off saving the plot as raster

thomasp85 avatar Oct 25 '24 12:10 thomasp85

Thank you very much Tom & Teun! We'll explore the options you've outlined above.

jack-davison avatar Oct 25 '24 12:10 jack-davison