Using a gtable object with gganimate

醉酒当歌 提交于 2020-01-03 19:15:59

问题


I'm producing a gif with gganimate, and I'd like to make some tweaks to the plot format that can only be accomplished by converting a ggplot object to a gtable. For instance, I'd like to change the position of the plot title so that it always appears in the far left corner of the plot.

Here's an example of what the plot tweak would look like:

library(ggplot2)
library(gganimate)
library(dplyr)

# Helper function to position plot title all the way to left of plot
align_titles_left <- function(p, newpage = TRUE) {
  p_built <- invisible(ggplot2::ggplot_build(p))
  gt <- invisible(ggplot2::ggplot_gtable(p_built))

  gt$layout[which(gt$layout$name == "title"), c("l", "r")] <- c(2, max(gt$layout$r))
  gt$layout[which(gt$layout$name == "subtitle"), c("l", "r")] <- c(2, max(gt$layout$r))


  # Prints the plot to the current graphical device
  # and invisibly return the object
  gridExtra::grid.arrange(gt, newpage = newpage)
  invisible(gt)
}

# Create an example plot
static_plot <- iris %>% 
  ggplot(aes(x = Sepal.Length, y = Sepal.Width,
             color = Species)) +
  geom_point() +
  labs(title = "This title should appear in the far left.")

# Print the static plot using the adjustment function
align_titles_left(static_plot)

How can I use this function with gganimate?

Here's some example gganimate code to turn the plot in this example into an animation.

# Produce the animated plot
static_plot +
  transition_states(Species,
                    transition_length = 3,
                    state_length = 1)

回答1:


Here's the result. Explanation below:

Any hacking with grobs should done after the individual frames of the animated plot have been created, but before they get drawn on the relevant graphics device. This window occurs in the plot_frame function of gganimate:::Scene.

We can define our own version of Scene that inherits from the original, but uses a modified plot_frame function with the grob hack lines inserted:

Scene2 <- ggproto(
  "Scene2",
  gganimate:::Scene,
  plot_frame = function(self, plot, i, newpage = is.null(vp), 
                        vp = NULL, widths = NULL, heights = NULL, ...) {
    plot <- self$get_frame(plot, i)
    plot <- ggplot_gtable(plot)

    # insert changes here
    plot$layout[which(plot$layout$name == "title"), c("l", "r")] <- c(2, max(plot$layout$r))
    plot$layout[which(plot$layout$name == "subtitle"), c("l", "r")] <- c(2, max(plot$layout$r))

    if (!is.null(widths)) plot$widths <- widths
    if (!is.null(heights)) plot$heights <- heights
    if (newpage) grid::grid.newpage()
    grDevices::recordGraphics(
      requireNamespace("gganimate", quietly = TRUE),
      list(),
      getNamespace("gganimate")
    )
    if (is.null(vp)) {
      grid::grid.draw(plot)
    } else {
      if (is.character(vp)) seekViewport(vp)
      else pushViewport(vp)
      grid::grid.draw(plot)
      upViewport()
    }
    invisible(NULL)
  })

Thereafter, we have to replace Scene with our version Scene2 in the animation process. I've listed two approaches below:

  1. Define a separate animation function, animate2, plus intermediate functions as required to use Scene2 instead of Scene. This is safer, in my opinion, as it doesn't change anything in the gganimate package. However, it does involve more code, & could potentially break in the future if the function definitions change at the source.

  2. Over-write existing functions in the gganimate package for this session (based on the answer here). This requires manual effort each session, but the actual code changes required are very small, & probably won't break as easily. However, it also carries the risk of confusing the user, since the same function could lead to different results, depending on whether it's called before or after the change.

Approach 1

Define functions:

library(magrittr)

create_scene2 <- function(transition, view, shadow, ease, transmuters, nframes) {
  if (is.null(nframes)) nframes <- 100
  ggproto(NULL, Scene2, transition = transition, 
          view = view, shadow = shadow, ease = ease, 
          transmuters = transmuters, nframes = nframes)
}

ggplot_build2 <- gganimate:::ggplot_build.gganim
body(ggplot_build2) <- body(ggplot_build2) %>%
  as.list() %>%
  inset2(4,
         quote(scene <- create_scene2(plot$transition, plot$view, plot$shadow, 
                                      plot$ease, plot$transmuters, plot$nframes))) %>%
  as.call()

prerender2 <- gganimate:::prerender
body(prerender2) <- body(prerender2) %>%
  as.list() %>%
  inset2(3,
         quote(ggplot_build2(plot))) %>%
  as.call()

animate2 <- gganimate:::animate.gganim
body(animate2) <- body(animate2) %>%
  as.list() %>%
  inset2(7,
         quote(plot <- prerender2(plot, nframes_total))) %>%
  as.call()

Usage:

animate2(static_plot +
           transition_states(Species,
                             transition_length = 3,
                             state_length = 1))

Approach 2

Run trace(gganimate:::create_scene, edit=TRUE) in console, & change Scene to Scene2 in the popup edit window.

Usage:

animate(static_plot +
          transition_states(Species,
                            transition_length = 3,
                            state_length = 1))

(Results from both approaches are the same.)



来源:https://stackoverflow.com/questions/55362961/using-a-gtable-object-with-gganimate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!