Search code examples
rggplot2gganimate

Multiple gganimate plots both stacked and side by side


I have four animated plots with the same number of frames, of which I want three stacked and the fourth to the right of the three stacked animations. Normally I would use something like grid.arrange, but that does not seem to work with gganimate.

I got the following piece of code based on the code from here: https://github.com/thomasp85/gganimate/wiki/Animation-Composition

a_mgif <- image_read(a_gif)
b_mgif <- image_read(b_gif)
c_mgif <- image_read(c_gif)
d_mgif <- image_read(d_gif)

new_gif <- image_append(c(a_mgif[1], b_mgif[1], c_mgif[1], d_mgif[1]), stack = TRUE)

for(i in 2:100){
  combined <- image_append(c(a_mgif[i], b_mgif[i], c_mgif[i], d_mgif[i]), stack = TRUE)
  new_gif <- c(new_gif, combined)
}

magick::image_write(new_gif, path="animation.1.gif")

This works fine and produces the four stacked animated plots. I tried to modify it to have the fourth plot to the right of the first three stacked plots as follows:

new_gif.1 <- image_append(c(a_mgif[1], b_mgif[1], c_mgif[1]), stack = TRUE)
new_gif.2 <- image_append(c(new_gif.1, d_mgif[1]), stack = FALSE)

for(i in 2:100){
  combined.1 <- image_append(c(a_mgif[i], b_mgif[i], c_mgif[i]), stack = TRUE)
  combined.2 <- image_append(c(combined.1, d_mgif[i]), stack = FALSE)
  new_gif.2 <- c(new_gif.2,combined.2)
}

magick::image_write(new_gif.2, path="animation.2.gif")

This seems to work, up to the point of writing the file. It takes forever to write the file, I let it run for hours and it still didn't finish, while the first version where they are all stacked is written within a minute, so clearly something is wrong. It's exactly the same data/plots in both examples. Any ideas on what goes wrong here?


Solution

  • What worked for me was to wrap the image_append calls into image_flatten. I also first combined img1 and img4 horizontally and then stacked this with img2 and img3.

    Here I only recycled the two animations from the example, but this solution should also work with four independent animations - at least for me, on R4.0 with ImageMagick 6.9.10.23 and magick_2.3, although it takes about 3 minutes and maybe 630 MB of temp storage:

    library(dplyr)
    library(ggplot2)
    library(magick)
    library(gganimate)
    
    A <- rnorm(100,50,10)
    B <- rnorm(100,50,10)
    DV <- c(A,B)
    IV <- rep(c("A","B"), each=100)
    sims <- rep(rep(1:10, each=10), 2)
    df <- data.frame(sims, IV, DV)
    
    means_df <- df %>%
        group_by(sims,IV) %>%
        summarize(means=mean(DV),
                  sem = sd(DV)/sqrt(length(DV)))
    
    stats_df <- df %>%
        group_by(sims) %>%
        summarize(ts = t.test(DV~IV,var.equal=TRUE)$statistic)
    
    a <- ggplot(means_df, aes(x = IV,y = means, fill = IV)) +
        geom_bar(stat = "identity") +
        geom_point(aes(x = IV, y = DV), data = df, alpha = .25) +
        geom_errorbar(aes(ymin = means - sem, ymax = means + sem), width = .2) +
        theme_classic() +
        transition_states(
            states = sims,
            transition_length = 2,
            state_length = 1
        ) + 
        enter_fade() + 
        exit_shrink() +
        ease_aes('sine-in-out')
    
    a_gif <- animate(a, width = 240, height = 240, renderer = magick_renderer())
    
    b <- ggplot(stats_df, aes(x = ts))+
        geom_vline(aes(xintercept = ts, frame = sims))+
        geom_line(aes(x=x,y=y),
                  data = data.frame(x = seq(-5,5, .1),
                                    y = dt(seq(-5,5, .1), df = 18))) +
        theme_classic() +
        ylab("density") +
        xlab("t value") +
        transition_states(
            states = sims,
            transition_length = 2,
            state_length = 1
        ) +
        enter_fade() + 
        exit_shrink() +
        ease_aes('sine-in-out')
    
    b_gif <- animate(b, width = 240, height = 240, renderer = magick_renderer())
    
    c_gif <- animate(b, width = 240, height = 240, renderer = magick_renderer())
    d_gif <- animate(a, width = 240, height = 240, renderer = magick_renderer())
    
    i=1
    combined <- image_append(c(a_gif[i], d_gif[i]))
    new_gif <- image_append(c(image_flatten(combined),
                              b_gif[i], c_gif[i]), stack=TRUE)
    
    for(i in 2:100){
        combined <- image_append(c(a_gif[i], d_gif[i]))
        fullcombined <- image_append(c(image_flatten(combined), 
                                       b_gif[i], c_gif[i]), stack=TRUE)
        new_gif <- c(new_gif, fullcombined)
    }
    
    image_write(new_gif, format="gif", path="animation.2.gif")
    
    

    Edit: Alternative Alternatively, you could use cowplot to arrange the plots and generate the individual frames in a loop and then use gifski to make the animation; that is even more flexible in terms of image placement, since you can add coordinates to draw_image (see corresponding cowplot examples). Below is a simple grid example:

    library(dplyr)
    library(ggplot2)
    library(magick)
    library(gganimate)
    library(cowplot)
    library(gifski)
    
    A <- rnorm(100,50,10)
    B <- rnorm(100,50,10)
    DV <- c(A,B)
    IV <- rep(c("A","B"), each=100)
    sims <- rep(rep(1:10, each=10), 2)
    df <- data.frame(sims, IV, DV)
    
    means_df <- df %>%
        group_by(sims,IV) %>%
        summarize(means=mean(DV),
                  sem = sd(DV)/sqrt(length(DV)))
    
    stats_df <- df %>%
        group_by(sims) %>%
        summarize(ts = t.test(DV~IV,var.equal=TRUE)$statistic)
    
    a <- ggplot(means_df, aes(x = IV,y = means, fill = IV)) +
        geom_bar(stat = "identity") +
        geom_point(aes(x = IV, y = DV), data = df, alpha = .25) +
        geom_errorbar(aes(ymin = means - sem, ymax = means + sem), width = .2) +
        theme_classic() +
        transition_states(
            states = sims,
            transition_length = 2,
            state_length = 1
        ) + 
        enter_fade() + 
        exit_shrink() +
        ease_aes('sine-in-out')
    
    a_gif <- animate(a, width = 240, height = 240, renderer = magick_renderer())
    
    b <- ggplot(stats_df, aes(x = ts))+
        geom_vline(aes(xintercept = ts, frame = sims))+
        geom_line(aes(x=x,y=y),
                  data = data.frame(x = seq(-5,5, .1),
                                    y = dt(seq(-5,5, .1), df = 18))) +
        theme_classic() +
        ylab("density") +
        xlab("t value") +
        transition_states(
            states = sims,
            transition_length = 2,
            state_length = 1
        ) +
        enter_fade() + 
        exit_shrink() +
        ease_aes('sine-in-out')
    
    b_gif <- animate(b, width = 240, height = 240, renderer = magick_renderer())
    
    c_gif <- animate(b, width = 240, height = 240, renderer = magick_renderer())
    d_gif <- animate(a, width = 240, height = 240, renderer = magick_renderer())
    
    tdir <- tempdir()
    for(i in 1:100){
        new_gif <- plot_grid(ggdraw() + draw_image(a_gif[i], scale = 0.9), 
                             ggdraw() + draw_image(d_gif[i], scale = 0.9),
                             ggdraw() + draw_image(b_gif[i], scale = 0.9),
                             ggdraw(),
                             ggdraw() + draw_image(c_gif[i], scale = 0.9),
                             ncol=2)
        ggsave(
            filename = file.path(tdir, paste0("out_", sprintf("%03d", i), ".png")),
            plot = new_gif, width = 2.4, height = 3.6, device = "png")
    }
    
    png_files <- sort(list.files(path = tdir, pattern = "out_", full.names = TRUE))
    gifski(png_files, gif_file = "out.gif", width = 480, height = 720, delay = .1,
           progress = TRUE)
    
    

    Edit #2 (May 2021):

    Something changed since my initial answer, and, as @agbarnett pointed out, the renderer now has to be set explicitly to renderer = magick_renderer() in all the animate commands.