问题
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:
Define a separate animation function,
animate2
, plus intermediate functions as required to useScene2
instead ofScene
. This is safer, in my opinion, as it doesn't change anything in thegganimate
package. However, it does involve more code, & could potentially break in the future if the function definitions change at the source.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