10

I have set up a fresh R installation in a Windows 10 machine and can't run something as simple as:

data.frame(a = rnorm(100), b = rnorm(100)) |> 
  ggplot(aes(a, b)) +
  ggsave("temp.png")

because I get the following error:

Error: Can't add `ggsave("temp.png")` to a ggplot object.

My session info is:

R version 4.1.0 (2021-05-18)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows >= 8 x64 (build 9200)

Matrix products: default

locale:
[1] LC_COLLATE=Catalan_Spain.1252  LC_CTYPE=Catalan_Spain.1252    LC_MONETARY=Catalan_Spain.1252 LC_NUMERIC=C                  
[5] LC_TIME=Catalan_Spain.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggplot2_3.3.4 dplyr_1.0.6  

loaded via a namespace (and not attached):
 [1] magrittr_2.0.1    tidyselect_1.1.1  munsell_0.5.0     colorspace_2.0-1  R6_2.5.0          rlang_0.4.11      fansi_0.5.0       tools_4.1.0      
 [9] grid_4.1.0        data.table_1.14.0 gtable_0.3.0      utf8_1.2.1        withr_2.4.2       ellipsis_0.3.2    digest_0.6.27     tibble_3.1.2     
[17] lifecycle_1.0.0   crayon_1.4.1      purrr_0.3.4       farver_2.1.0      vctrs_0.3.8       glue_1.4.2        labeling_0.4.2    compiler_4.1.0   
[25] pillar_1.6.1      generics_0.1.0    scales_1.1.1      pkgconfig_2.0.3  

I have given permissions to the directory I'm working on and also tried in different directories and run with RScript, RStudio and Pycharm R Console, always with the same issue.

Thanks in advance.

EDIT: this used to work on ggplot2 3.3.3, it's the update to 3.3.4 that breaks things.

Eudald
  • 358
  • 3
  • 12
  • 3
    `ggsave()` shouldn't be part of a ggplot `+` "pipe". See the examples in the online doc. – Limey Jun 16 '21 at 13:30
  • Assign the plot, then save it, `my_plot = ggplot...` then `ggsave(my_plot, ...)`. – Gregor Thomas Jun 16 '21 at 13:31
  • For those who might VTC based on "typo": this is not a typo, the `+` is intentional and indicates a misunderstanding of how `ggsave` interacts with ggplot data/mechanics. Since it is within the `ggplot2` package, I don't think it's a huge leap to believe that it might "add" the same as many other ggplot2 functions (though that belief is unfortunately incorrect). – r2evans Jun 16 '21 at 13:37
  • @GregorThomas FYI, the first required argument of `ggsave()` is the filename used to save on disk, not the name of the object in the environment. – Phil Jun 16 '21 at 14:25
  • 1
    @Limey it might not have been "standard" or maybe I misunderstood the idea of ggsave, but it used to work in ggplot2 3.3.3 and it changed all of a sudden without warning; that's what I don't understand. – Eudald Jun 16 '21 at 14:28
  • Just adding that the change has been addressed by the maintainers: https://www.tidyverse.org/blog/2021/06/off-label-uses-in-ggplot2/ – teunbrand Jul 07 '21 at 17:16

4 Answers4

23

You can no longer "add" ggsave to a ggplot addition-pipe.

Edit: this is a recent change in ggplot2-3.3.4. The previous answer is preserved below if you want to work around the new behavior. If you're particular annoyed by it, you might submit a new issue to ggplot2 suggesting that they either (a) undo the breaking change, or (b) better document the change in unintended functionality.

Side note: the day after this answer was posted, commit 389b864 included the following text: "Note that, as a side effect, an unofficial hack <ggplot object> + ggsave() no longer works (#4513)."

(In truth, I don't recall seeing documentation that suggests that + ggsave(.) should work, so the response to a new issue might be that they do not want to preserve an unintended "feature" for the sake of giving up some other elegant completeness.)

The changes from 3.3.3 to 3.3.4 (for save.R) are mostly unrelated to the act of saving the file. However, one functional change is the return value from ggsave:

@@ -90,5 +98,5 @@ ggsave <- function(filename, plot = last_plot(),
   grid.draw(plot)

-  invisible()
+  invisible(filename)
 }

In retrospect, this makes sense: ggplot2's ability to use +-pipes tends to be okay with trying to add NULL-like objects. That is, this works without error:

data.frame(a = rnorm(100), b = rnorm(100)) |>
  ggplot(aes(a, b)) +
  NULL

Why is NULL relevant here? Because the previous (3.3.3) version of ggsave ends with invisible(), which is invisibly returning NULL. (Internally, ggplot2:::add_ggplot begins with if (is.null(object)) return(p), which explains why that works.)

With the change to invisible(filename) (which, imo, is actually a little better), however, this is effectively the same as

data.frame(a = rnorm(100), b = rnorm(100)) |>
  ggplot(aes(a, b)) +
  "temp.png"

which does not make sense, so the +-piping fails.

In ggplot2-3.3.3, one can replicate this error with a hack/ugly code:

data.frame(a = rnorm(100), b = rnorm(100)) |>
  ggplot(aes(a, b)) +
  { ggsave("temp.png"); "temp.png"; }
# Error: Can't add `{` to a ggplot object.
# * Can't add `    ggsave("temp.png")` to a ggplot object.
# * Can't add `    "temp.png"` to a ggplot object.
# * Can't add `}` to a ggplot object.

which is close enough to the error you saw to (I believe) prove my point: the new-and-improved ggplot2-3.3.4 is returning a string, and that is different enough to break your habit-pattern of adding ggsave to a ggplot2 object.

If you're going to submit a new issue to ggplot2, then I suggest you frame it as a "feature request": if the invisible(filename) is instead a class object for which + works, then the previous behavior can be retained while still supporting the string-return. For example (completely untested):

ggsave <- function(file, ...) {
  # .....
  class(filename) <- c("ggplot2_string", "character")
  invisible(filename)
}

and then extend the +.gg-logic to actually work for strings, perhaps something like

`+.gg` <- function (e1, e2) {
    if (missing(e2)) {
        abort("Cannot use `+.gg()` with a single argument. Did you accidentally put + on a new line?")
    }
    if (inherits(e2, "ggplot2_string")) {
      e2 <- NULL
      e2name <- "NULL"
    } else {
      e2name <- deparse(substitute(e2))
    }
    if (is.theme(e1)) 
        add_theme(e1, e2, e2name)
    else if (is.ggplot(e1)) 
        add_ggplot(e1, e2, e2name)
    else if (is.ggproto(e1)) {
        abort("Cannot add ggproto objects together. Did you forget to add this object to a ggplot object?")
    }
}

No, I don't think this is the best way, but it is one way, open for discussion.


Four things you can do:

  1. Plot it, then save it. It will be displayed in your graphic device/pane.

    data.frame(a = rnorm(100), b = rnorm(100)) |>
      ggplot(aes(a, b))
    ggsave("temp.png")
    
  2. Save to an intermediate object without rendering, and save that:

    gg <- data.frame(a = rnorm(100), b = rnorm(100)) |>
      ggplot(aes(a, b))
    ggsave("temp.png", plot = gg)
    
  3. If R-4.1, pipe it do the plot= argument. While I don't have R-4.1 yet, based on comments I am led to believe that while |> will always pass the previous result as the next call's first argument, you can work around this by naming the file= argument, which means that R-4.1 will pass to the first available argument which (in this case) happens to be plot=, what we need.

    data.frame(a = rnorm(100), b = rnorm(100)) |>
      ggplot(aes(a, b)) |>
      ggsave(file = "temp.png")
    
  4. If you're using magrittr pipes, then you can do the same thing a little more succinctly:

    library(magrittr) # or dplyr, if you're using it for other things
    data.frame(a = rnorm(100), b = rnorm(100)) %>% # or |> here
      ggplot(aes(a, b)) %>%                        # but not |> here
      ggsave("temp.png", plot = .)
    
r2evans
  • 141,215
  • 6
  • 77
  • 149
  • 2
    `|> {\(p) ggsave("temp.png", plot = p)}()` also works, but it's a lot. – Ian Campbell Jun 16 '21 at 13:51
  • 1
    Thanks for your reply, but this has changed after 3.3.3! I don't know if I missed something because IMO this is a huge change since it's broken all backwards compatibility and it's not even in the changelog... – Eudald Jun 16 '21 at 14:21
  • 2
    Interesting. To be frank, I've never assumed `ggsave` works in a `+`-pipe, assuming it to be not within that construct, but you're right, your code works in ggplot2-3.3.3 and I've never tried to do that. Bad on me for not confirming your code fails for me (it does not). I see the real culprit, stand by for an edit ... – r2evans Jun 16 '21 at 15:06
  • 2
    I think saying 'it has broken all backwards compatibility' is a bit harsh (keyword being: 'all'); at best you might say that it might break some unintended and undocumented behaviour. I think the r2evans first three suggestions are excellent canonical use of `ggsave()` (I was unfamiliar with the 4th). – teunbrand Jun 16 '21 at 21:12
  • Great write up, I'm currently trying to figure out my preferred option since I used `+ggsave()` frequently. It seems to me that although `data.frame(a = rnorm(100), b = rnorm(100)) %>% ggplot(aes(a, b)) %>% ggsave("temp.png", plot = .)` works attempting to modify the plot in any way with additional `ggplot2` arguments fails. For instance ``data.frame(a = rnorm(100), b = rnorm(100)) %>% ggplot(aes(a, b)) %>% geom_point() %>% ggsave("temp.png", plot = .)` fails as does `data.frame(a = rnorm(100), b = rnorm(100)) %>% ggplot(aes(a, b)) %>% theme_bw() %>% ggsave("temp.png", plot = .)`. – John Harley Aug 02 '21 at 17:36
  • `ggplot2` does not support `%>%` pipes, unfortunately; hadley [posted](https://community.rstudio.com/t/why-cant-ggplot2-use/4372/7) some commentary about it, and while the point is made that pipes other than `+` could have been better, it is too late and too much work to support it in `ggplot2`. You need to intermingle the `%>%`/`+` operators, and because of precedence, you need to use blocks. Try `data.frame(a = rnorm(100), b = rnorm(100)) %>% { ggplot(., aes(a, b)) + geom_point(); } %>% ggsave("temp.png", plot = .)`. – r2evans Aug 02 '21 at 17:41
7

Just remove the plus sign.

data.frame(a = rnorm(100), b = rnorm(100)) |> 
  ggplot(aes(a, b))

ggsave("temp.png")

ggsave has a default input last_plot()

Pedro Alencar
  • 1,049
  • 7
  • 20
1

You could always define your own ggsave() function that restores the old behavior.

library(tidyverse)

ggsave <- function(...) {
  ggplot2::ggsave(...)
  invisible()
}

data.frame(a = rnorm(100), b = rnorm(100)) |>
  ggplot(aes(a, b)) +
  ggsave("temp.png")
#> Saving 7 x 5 in image

Created on 2021-06-17 by the reprex package (v1.0.0)

Note: I do not think this is a good idea. Just pointing out that it's possible.

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104
  • Sir, following your post/blog, can you please tell why `ggtext::element_markdown()` was not working [here](https://stackoverflow.com/q/68948902/2884859) till I stopped by antivirus. PS: I just want to tag you. No relation with this question – AnilGoyal Aug 27 '21 at 15:07
  • Excellent! At least old scripts will work this way. Thank you – Clem Snide Sep 28 '22 at 13:28
1

I don't know if my setup differs from others in some way, or if it's just the way my ggsave code happens to be configured, but it still works for me, while also now kicking up the error.

In this, Thomas says:

First, while you’ll get an error in v3.3.4, the plot is actually saved to a file since the error is thrown after the evaluation of ggsave(). This means that you can “fix” your code by putting the whole expression in a try() block (please don’t do this though )

But you don't have to do the try block. My code blocks are:

ggplot(stuff) +
  ggsave(paste0(today(), "_PlotName", ".png"),
         plot = last_plot(),
         device = "png", path = "", scale = 3.5, width = 8,
         height = 4, units = "in", dpi = 300, limitsize = TRUE)

Presumably specifying plot = last_plot() keeps things working. But don't tell anyone, in case they take it away ;)

dez93_2000
  • 1,730
  • 2
  • 23
  • 34