tidyverse / ggplot2

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

`ggsave()` in 3.3.6 has broken old plots #5016

Closed rikudoukarthik closed 1 year ago

rikudoukarthik commented 1 year ago

The problem

I have some code that creates a map with numerous points, annotated with some stats, on a monthly basis. This worked fine until I updated ggplot to 3.3.6, after which the plots have broken and I've not been able to figure out the solution. As far as I can tell, the issue lies in the ggsave() call, but I don't know what has changed and how to fix it.

There are two main issues that have come up. But to illustrate, I will attach below comparison images of the "good" and "bad" versions (click links to see full-size).

Good points

image

Bad points

image

The good version has uniformly coloured points/squares while the latter has strange points coloured irregularly.

Good text

image

Bad text

image

The non-breaking space is formatted properly in the good version, while it appears as a box in the bad version.

Debugging attempts

One potential cause I noted for the irregular points was some updates in the works for the "size" parameter (see this blog post). Such things have happened in the past as well (see this for example). However, this update is claimed to be for the next release, and moreover like I said, I have a hunch the issue I'm facing has something to do with ggsave(). And regardless, I already tried tweaking the size and stroke of the geom_point() but haven't been able to recover the old version properly.

The RStudio plot device doesn't indicate any issue with the points, and using the png() method instead of ggsave() to write produces the correct/"good" version.

png(filename = "map_cov_plain.png",
units = "in", width = 8, height = 11, bg = "transparent", res = 300)
print(map_cov_plain)
dev.off()

I tried reverting to ggplot 3.3.5 but this did not fix the issue. Moreover, two others tried the same code on their separate systems, both with ggplot 3.3.6, but only one replicated my issue while the other produced the good version. Nevertheless, the code was certainly working fine until July, only after which I updated several packages and the code broke.

For the record, I have ensured that the issue is not with the data. So, although I have used data until June to illustrate the good version, that same dataset generates the bad maps when the code is run now (i.e., after the updates).

I am hoping someone with a better understanding of the package and the update will be able to figure out what exactly the breaking change was!

Other links

4824

Reprex

There are two files required for the reprex below to work:


library(lubridate)
library(tidyverse)
library(glue)

library(magick)
library(scales) # for comma format of numbers
library(grid)

# loading objects
load("reprex.RData")

map_cov_logo <- image_convert(image_read("bcilogo-framed.png"), matte = T)

map_cov_text <- glue::glue("{label_comma()(data_cov$LOCATIONS)} locations
                      {label_comma()(data_cov$LISTS)} lists
                      {label_comma()(data_cov$HOURS)} hours
                      {label_comma()(data_cov$PEOPLE)} people

                      {label_comma()(data_cov$STATES)} states/UTs
                      {label_comma()(data_cov$DISTRICTS)} districts

                      {label_comma()(data_cov$SPECIES)} species
                      {round(data_cov$OBSERVATIONS, 1)} million observations")

map_cov_footer <- glue::glue("Data until September 2022")

### map with annotations of stats and BCI logo ###
map_cov_annot <- ggplot() +
  geom_polygon(data = indiamap, aes(x = long, y = lat, group = group), 
               colour = NA, fill = "black")+
  geom_point(data = data_loc, aes(x = LONGITUDE, y = LATITUDE), 
             colour = "#fcfa53", size = 0.05, stroke = 0) +
  # scale_x_continuous(expand = c(0,0)) +
  # scale_y_continuous(expand = c(0,0)) +
  theme_bw() +
  theme(axis.line = element_blank(),
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        axis.ticks = element_blank(),
        axis.title.x = element_blank(),
        axis.title.y = element_blank(),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.border = element_blank(),
        # panel.border = element_blank(),
        plot.background = element_rect(fill = "black", colour = NA),
        panel.background = element_rect(fill = "black", colour = NA),
        plot.title = element_text(hjust = 0.5)) +
  coord_cartesian(clip = "off") +
  theme(plot.margin = unit(c(2,2,0,23), "lines")) +
  annotation_raster(map_cov_logo, 
                    ymin = 4.5, ymax = 6.5,
                    xmin = 46.5, xmax = 53.1) +
  annotation_custom(textGrob(label = map_cov_text,
                             hjust = 0,
                             gp = gpar(col = "#FCFA53", cex = 1.5)),
                    ymin = 19, ymax = 31,
                    xmin = 40, xmax = 53)  +
  annotation_custom(textGrob(label = map_cov_footer,
                             hjust = 0,
                             gp = gpar(col = "#D2D5DA", cex = 1.0)),
                    ymin = 15, ymax = 16,
                    xmin = 40, xmax = 53) 

ggsave(map_cov_annot, file = "map_cov_annot.png", device = "png",
       units = "in", width = 13, height = 9, bg = "transparent", dpi = 300)

### plain map without annotations ###
map_cov_plain <- ggplot() +
  geom_polygon(data = indiamap, aes(x = long, y = lat, group = group), 
               colour = NA, fill = "black")+
  geom_point(data = data_loc, aes(x = LONGITUDE, y = LATITUDE), 
             colour = "#fcfa53", size = 0.05, stroke = 0.1) +
  # scale_x_continuous(expand = c(0,0)) +
  # scale_y_continuous(expand = c(0,0)) +
  theme_bw() +
  theme(axis.line = element_blank(),
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        axis.ticks = element_blank(),
        axis.title.x = element_blank(),
        axis.title.y = element_blank(),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.border = element_blank(),
        plot.margin = unit(c(0, 0, 0, 0), "cm"),
        # panel.border = element_blank(),
        plot.background = element_rect(fill = "black", colour = NA),
        panel.background = element_rect(fill = "black", colour = NA),
        plot.title = element_text(hjust = 0.5)) +
  coord_map()

ggsave(map_cov_plain, file = "map_cov_plain.png", device = "png",
       units = "in", width = 8, height = 11, bg = "transparent", dpi = 300)
thomasp85 commented 1 year ago

This is due to a switch to the ragg device in 3.3.6

The first "issue" is due to the default png() device not applying anti-aliasing to fill whereas ragg does. The reason why I put issue in quotation marks is that the difference is only noticeable when zoomed into pixel level magnification and also because the behaviour of png() is not more correct since it will give higher visual weight to points depending on where on the pixel grid they are placed.

The last issue was due to a text rendering bug in ragg on certain linux systems that should have been fixed in the latest release

rikudoukarthik commented 1 year ago

The reason why I put issue in quotation marks is that the difference is only noticeable when zoomed into pixel level magnification

I'd like to clarify that the issue is not noticeable only when zoomed in to pixel level, which is my problem. If you view the good and the bad figures produced, the difference is major and immediately obvious.

the behaviour of png() is not more correct since it will give higher visual weight to points depending on where on the pixel grid they are placed.

I didn't quite understand this; I thought the png() behaviour gives equal weight to all points?

Admittedly, I don't know much about anti-aliasing, but I understand its importance. However, this particular usecase of mine I believe is one where aliasing is useful---I am interested in each individual point being plotted and so they should appear with the same intensity. Is there an optional argument in ggsave() to turn off anti-aliasing when required? If not, I think this could be a useful one to include.

The last issue was due to a text rendering bug in ragg on certain linux systems that should have been fixed in the latest release

Yes, I updated ragg and this issue is now resolved, even though I've been using Windows.

thomasp85 commented 1 year ago

The solution is to use the png device over the default if you prefer that behaviour (eg ggsave(device = png)

There is no aliasing "on" mode in AGG as it values correctness over crispness in the output so it is better to use a device build on another graphic system if you want to go down that road

rikudoukarthik commented 1 year ago

Thanks, Thomas. This helped me realise that ggsave(device = png) is different from ggsave(device = "png"): the former uses the default png() device whereas I assume the latter defaults to ragg::agg_png(). Leaving this comment in case someone else finds it useful as well.