tidyverse / ggplot2

An implementation of the Grammar of Graphics in R
https://ggplot2.tidyverse.org
Other
6.47k stars 2.02k forks source link

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

Open jack-davison opened 1 month ago

jack-davison commented 1 month ago

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

teunbrand commented 1 month ago

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.

jack-davison commented 1 month ago

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.

teunbrand commented 1 month ago

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.