Karthik Thrikkadeeri
Karthik Thrikkadeeri

Reputation: 356

Breaking change in ggsave() in 3.3.6 update: cannot reproduce old plots

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 ggplot2 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

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)

Upvotes: 3

Views: 350

Answers (1)

Jon Spring
Jon Spring

Reputation: 66870

I think this comes down to taste, and you prefer the result of the rendering without anti-aliasing. It looks bolder and sharper because the cells that are partially activated are shown at full brightness, in contrast with fully black adjacent pixels. Mssr. Pederson argues that the anti-aliased version is in some respects a truer depiction of the underlying data, since it will perceptually give the dots more consistent weight and spacing that corresponds to their actual size and placement.

Two examples:

set.seed(42)
library(ggplot2)
df_rand <- data.frame(x = runif(1000), y = runif(1000))

ggplot(df_rand, aes(x,y)) +
  geom_point(colour = "#fcfa53", size = 0.01, stroke = 0) +
  theme_void() +
  theme(plot.background = element_rect(fill = "black", colour = NA),
        panel.background = element_rect(fill = "black", colour = NA))

ggsave("rand_no_anti-alias.png", device = png,
       units = "px", width = 100, height = 100)

ggsave("rand_ragg_anti-alias.png", device = ragg::agg_png(),
       units = "px", width = 100, height = 100)

enter image description here

enter image description here

Of these two, you prefer the former for your use case and particular settings.

df_grid <- data.frame(expand.grid(x = 1:30, y = 1:30))

ggplot(df_grid, aes(x,y)) +
  geom_point(colour = "#fcfa53", size = 0.01, stroke = 0) +
  theme_void() +
  theme(plot.background = element_rect(fill = "black", colour = NA),
        panel.background = element_rect(fill = "black", colour = NA))

ggsave("grid_no_anti-alias.png", device = png,
       units = "px", width = 100, height = 100)

ggsave("grid_ragg_anti-alias.png", device = ragg::agg_png(),
       units = "px", width = 100, height = 100)

enter image description here

enter image description here

Note here that the version without anti-aliasing creates phantom groupings out of a uniform grid. Its algorithm shows full intensity even when the point covers a tiny part of the pixel. The anti-aliased version reflects that the points cover a very small part of each pixel, and it attempts to even out the error by sometimes depicting a point in one pixel and sometimes using two adjacent pixels, but dimmer. While the uniform data creates some moire effect in this contrived example, in my view the perceptual distortion is smaller.

If I make the point size much larger (e.g. 0.3), it makes for a closer match in brightness with the non-anti-aliased version. This version avoids the phantom groupings that the non-anti-aliased version has, but at the cost of the pixels looking smudged at the pixel level. That's anti-aliasing for ya.

enter image description here

Technical arguments aside, use the rendering method that gives you the output you want.

Upvotes: 2

Related Questions