简体   繁体   中英

Error: Can't add ggsave to a ggplot object

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.

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 =.)

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()

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.

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;)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM