tidyverse / ggplot2

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

is it possible to let `ggsave` utilize `ggplot_build` method? #6002

Closed Yunuuuu closed 1 month ago

Yunuuuu commented 1 month ago

Hi, is it possible to let ggsave utilize ggplot_build method? in this way, all objects support ggplot_build can use ggsave function

I have developed a ggplot2 extension ggheat for creating heatmaps and I would like to utilize all the functions in ggplot, including ggsave. However, creating a new function with the same name would result in duplication. Additionally, since the package automatically loads ggplot, there is no need to create a duplicate function. Tt would be beneficial to incorporate the ggplot_build method, and it would make ggplot extension more powerful and allow for seamless integration with the existing ggplot function.

library(ggheat)
#> Loading required package: ggplot2
mat <- matrix(rnorm(81), nrow = 9)
rownames(mat) <- paste0("row", seq_len(nrow(mat)))
colnames(mat) <- paste0("column", seq_len(ncol(mat)))
x <- ggheat(mat) +
  scale_fill_viridis_c() +
  htanno_dendro(aes(color = branch), position = "top", k = 3L) +
  gganno_top(aes(y = value), data = rowSums) +
  geom_bar(stat = "identity", aes(fill = factor(.panel))) +
  scale_fill_brewer(name = NULL, palette = "Dark2") +
  gganno_left(aes(x = value), data = rowSums, size = 0.5) +
  geom_bar(
    aes(y = .y, fill = factor(.y)),
    stat = "identity",
    orientation = "y"
  ) +
  scale_x_reverse() +
  htanno_dendro(aes(color = branch),
    position = "left",
    size = unit(1, "null"),
    k = 4L
  ) +
  scale_x_reverse()
ggplot2::ggsave("ggheat.png",
  plot = x,
  width = 350, height = 350, units = "px"
)
#> Error in slot(x, name): no slot of name "theme" for this object of class "ggheatmap"
x

Created on 2024-07-17 with reprex v2.1.0 ~

image

teunbrand commented 1 month ago

I'm not sure what you mean with 'incorporate the ggplot_build() method, would you mind clarifying a bit?

Yunuuuu commented 1 month ago

Something like this, other objects can utilize ggplot_build methods to use ggsave seamlessly.

ggsave <- function(filename, plot) {
  # before drawing, call `ggplot_build` or maybe `ggplotGrob`
  plot <- ggplot_build(plot)
  # ... here we do some other common work
  grid.draw(plot)
  invisible(filename)
}
teunbrand commented 1 month ago

Implicitly, it is used in the grid.draw.ggplot() method for typical ggplots. This should work fine for any subclass of ggplot. If you have a plot with a different class, you should make a grid.draw.your_class() method that renders the plot. I don't think ggplot2 should change anything here.

Also, based on the error message, it seems like your class object doesn't have a theme element that is required for rendering a typical ggplot.

yutannihilation commented 1 month ago

@Yunuuuu In my understanding, it's your responsibility to make your ggheatmap object compatible with ggplot2. While you might be able to bypass the error by asking for a tweak in this specific case of ggsave(), but I'm afraid you'll encounter various errors on the functions that assume the input is an ordinary ggplot object. It's not realistic to ask the authors to modify their functions, especially it's an extension package.

I think it's a good attempt to utilize S4 system to overcome the limitation of S3, although I'm not sure whether it will succeed or not in the end. I'm not familiar with S4, but maybe you can override $ better?

library(ggheat)
#> Loading required package: ggplot2

p1 <- ggplot()
p1$theme
#> list()

p2 <- ggheat(matrix())
p2$theme
#> Error in slot(x, name): no slot of name "theme" for this object of class "ggheatmap"

Created on 2024-07-17 with reprex v2.1.0

yutannihilation commented 1 month ago

@teunbrand

Implicitly, it is used in the grid.draw.ggplot() method for typical ggplots.

To be clear, the error happens before grid.draw().

https://github.com/tidyverse/ggplot2/blob/25ad0b17865c1280c464ccfbee38c3d7eb5df95d/R/save.R#L106

https://github.com/tidyverse/ggplot2/blob/25ad0b17865c1280c464ccfbee38c3d7eb5df95d/R/theme.R#L600-L601

Yunuuuu commented 1 month ago

Thanks for your suggestion, @yutannihilation. It is not possible for ggheat to know the value of plot.background before it is built into a patchwork object, as the elements are simply a list of ggplot objects. However, I have found a solution by creating a custom method for the $ operator with a specific consideration for ggplot2. Here is an example of how it can be done in R:

#' Subset a `ggheatmap` object
#'
#' Used by [ggplot_build][ggplot2::ggplot_build]
#'
#' @param x A ggheatmap object
#' @param name A string of slot name in `ggheatmap` object.
#' @keywords internal
methods::setMethod("$", "ggheatmap", function(x, name) {
    if (name == "theme") {
        slot(x, "heatmap")$theme
    } else if (name == "plot_env") {
        slot(x, "plot_env")
    } else {
        cli::cli_abort(c(
            "`$` is just for internal usage for ggplot2 methods",
            i = "try to use `@` method instead"
        ))
    }
})
Yunuuuu commented 1 month ago

I have implemented another S4 class to override the +.gg method, as suggested by @teunbrand you. This approach was recommended in your previous interaction in stackoverflow, which can be found here: stackoverflow.com/questions/65817557/s3-methods-extending-ggplot2-gg-function. Initially, I tried using an S3 class and adding a gg element to the ggheat object. However, there was a conflict between the +.ggheat method and the +.gg method. In order to resolve this conflict, I decided to utilize an S4 class instead. I am pleased to report that the implementation is now functioning smoothly and is capable of performing all the tasks of Complexheatmap.

Yunuuuu commented 1 month ago

By using the tricks in https://github.com/tidyverse/ggplot2/issues/6002#issuecomment-2233743223, I'm able to resolve the problem when using ggsave in ggheat package. It's well for me to close this issue. Thanks.