thomasp85 / gganimate

A Grammar of Animated Graphics
https://gganimate.com
Other
1.95k stars 310 forks source link

Docs: Getting started example #226

Open ptoche opened 5 years ago

ptoche commented 5 years ago

I've been a fan of gganimate. Great package! I am updating several animations I had made with the earlier API... I cannot seem to even get started. I have seen the wiki, but these examples are too advanced and I didn't manage to adapt them right away to my simple needs. I would like to suggest adding simpler examples to the README and/or wiki.

Suggestion: (Help) Create a few simple examples for dummies:

In the reprex below, I suggest 4 or 5 examples that a newbie could use to get started. Please help me use the correct syntax in those cases where I haven't worked it out. Thanks!

Below two basic plots to animate:

library(reprex)
    # Data 
    library(tibble)
    df <- tibble(x = -10:10, y = abs(x)) 
    # Plots
    library(ggplot2)
    ggplot(data = df, aes(x, y)) + 
        geom_point(size = 2)

    ggplot(data = df, aes(x, y)) + 
        geom_line(size = 2)

Created on 2018-12-11 by the reprex package (v0.2.1)

A convenience function to display GIFs side by side (used in the examples below).


    library(reprex)

# Combine plots side by side 
combine_gifs <- function(plot1, plot2) {
    require(magick) 
    # read the plots and store them
    plot1 <- image_read(plot1) 
    plot2 <- image_read(plot2) 
    # sync the number of frames in each plot
    n1 = length(plot1)
    n2 = length(plot2)
    # match number of frames of the two plots
    if (!(n1 == n2)) plot1 <- rep(plot1, n2) 
    if (!(n1 == n2)) plot2 <- rep(plot2, n1)
    # initialize the combined plot
    p <- image_append(c(plot1[1], plot2[1]))
    # grow the combined plot frame by frame
    n = ifelse(n1 == n2, n1, n1 * n2) 
    n = min(1000, n)  # set max to 1000
    for (i in 2:(n-1)) {
        tmp <- image_append(c(plot1[i], plot2[i]))
        p <- c(p, tmp)
    }
    return(p) 
}
ptoche commented 5 years ago

Example 1:

Objective: reveal a single point sequentially: reveal (x_{i}, y_{i}) + hide (x_{i-1}, y_{i-1})

    library(reprex)
    # Packages + Data
    library(ggplot2)
    library(gganimate)
    library(tibble)
    df <- tibble(x = -10:10, y = abs(x)) 
    # Plots
    p1 <- ggplot(data = df, aes(x, y)) + 
        geom_point(size = 5) + 
        transition_manual(x) +
        labs(title = "transition_manual(x)")
    p1 <- animate(p1)  
#> nframes and fps adjusted to match transition
    p2 <- ggplot(data = df, aes(x, y)) + 
        geom_point(size = 5) + 
        transition_manual(y) +
        labs(title = "transition_manual(y)")
    p2 <- animate(p2)  
#> nframes and fps adjusted to match transition
    combine_gifs(plot1 = p1, plot2 = p2) 
#> Loading required package: magick
#> Linking to ImageMagick 6.9.9.39
#> Enabled features: cairo, fontconfig, freetype, lcms, pango, rsvg, webp
#> Disabled features: fftw, ghostscript, x11

Created on 2018-12-11 by the reprex package (v0.2.1)

Verdict: Success. Comment: To reveal the points in a vertical direction, use transition_manual(y). Very intuitive.

ptoche commented 5 years ago

Following on from the previous examples, below are attempts to reveal points sequentially while keeping them on the graph. Thanks Thomas for showing me how to achieve this objective.

Example 2:

Objective: reveal all points sequentially: reveal (x_{i}, y_{i}) without hiding (x_{i-1}, y_{i-1})

    # Packages + Data    
    library(reprex)
    library(ggplot2)
    library(gganimate)
    library(tibble)
    df <- tibble(x = -10:10, y = abs(x)) 
    # Plots
    p1 <- ggplot(data = df, aes(x, y, group = seq_along(x))) + 
        geom_point(size = 5) + 
        transition_reveal(x) +
        labs(title = "transition_reveal(x)") 
    p1 <- animate(p1)  
    p2 <- ggplot(data = df, aes(x, y, group = seq_along(x))) + 
        geom_point(size = 5) + 
        transition_reveal(y) + 
        labs(title = "transition_reveal(y)")
    p2 <- animate(p2)  
    combine_gifs(plot1 = p1, plot2 = p2) 
#> Loading required package: magick
#> Linking to ImageMagick 6.9.9.39
#> Enabled features: cairo, fontconfig, freetype, lcms, pango, rsvg, webp
#> Disabled features: fftw, ghostscript, x11

    anim_save("transition_reveal.gif") 

Created on 2018-12-11 by the reprex package (v0.2.1)

Verdict: Success. Comment: You need group = seq_along(x) in the aes and transition_reveal(). This will take some getting used to.

ptoche commented 5 years ago

The default nframes is 100. This seems like an excessive number of frames for an animation that would be expected to have 21 frames. I wonder if the gifs above can be created with 21 frames by default or if one is expected to override the default manually... Will look into this.

length(-10:10)
## 21 
thomasp85 commented 5 years ago

This is only possible for some transitions, e.g. transition_manual... In your case the only reason why it can be done with 21 frames is because there is only one layer and each time point fed into transition_reveal is distributed evenly across the range and no point has the same group aesthetic. I'm not going to create special cases for such things as it is likely to break all the time

thomasp85 commented 5 years ago

also, sorry for breaking all your code, but breaking changes to transition_reveal has just been merged in, removing the id argument. You should now use the group aesthetic in each layer to set which elements belongs together

ptoche commented 5 years ago

Thanks Thomas. Will look into this. I'm only just learning the new api, so I don't mind breaking changes.

Is there a way to override nframes from inside, say, transition_manual()? How about fps? Would it be too much work to allow the user to override them? I saw in the code that nframes is set to 100 by default inside create_scene (but if I understand correctly the number of frames also appears as the product of total and detail, set by default to the product of 100 and 1), but I didn't see if it can be accessed.

I haven't updated to the latest version, so let me do that first. But when I tried to set the nframes implicitly by giving the frames option the x vector, the result was that the animation lost its cumulative = TRUE effect (sorry for using old terminology here). Will check the latest update and explore further, but adding transition_manual(frames = x) correctly resulted in 21 frames being produced (instead of 100), but it also removed the "cumulative" effect. As I'm super new, no idea if what I write even makes sense.

ggplot(data = df, aes(x, y)) + 
    geom_point(size = 5) + 
    transition_reveal(x, x) +
    transition_manual(frames = x) 
thomasp85 commented 5 years ago

The number of frames is not really something the transitions should worry about unless the data provided does not support the number of frames requested. The idea is that an animation specification is dimensionless and only upon requesting a render is anything determined. It is thus the responsibility of the animate() function to request a number of frames along with a framerate. animate is called implicitly upon printing the animation object, using the defaults (which is 100 frames and 10 fps), but can either be called explicitly with other values or the defaults can be overwritten - see ?animate for more details

ptoche commented 5 years ago

I follow up with a few more attempts at making introductory examples. I'll update the code as I make progress. Comments welcome.

Example 3:

Objective: reveal a single line segment sequentially: show (x_{i}, y_{i}) -- (x_{i-1}, y_{i-1}) + hide (x_{i-1}, y_{i-1}) -- (x_{i-2}, y_{i-2})

Verdict: Thomas says there is no easy way to do this right now... To be continued.

ptoche commented 5 years ago

Example 4: Objective: reveal a line sequentially: reveal (x_{i}, y_{i}) -- (x_{i-1}, y_{i-1}) without hiding (x_{i-1}, y_{i-1})-(x_{i-2}, y_{i-2})


# Packages + Data
library(reprex)
library(ggplot2)
library(gganimate)
library(tibble)
df <- tibble(x = -10:10, y = abs(x)) 
# Plots
p1 <- ggplot(data = df, aes(x, y)) + 
    geom_path(size = 2) + 
    transition_reveal(x) +
    labs(title = "geom_path() with transition_reveal(x)")
p1 <- animate(p1)  
p2 <- ggplot(data = df, aes(x, y)) + 
    geom_path(size = 2) + 
    transition_reveal(y) +
    labs(title = "geom_path() with transition_reveal(y)")
p2 <- animate(p2)  
combine_gifs(plot1 = p1, plot2 = p2)
#> Loading required package: magick
#> Linking to ImageMagick 6.9.9.39
#> Enabled features: cairo, fontconfig, freetype, lcms, pango, rsvg, webp
#> Disabled features: fftw, ghostscript, x11

Created on 2018-12-11 by the reprex package (v0.2.1)

Verdict: Success. Comment: geom_line() is not recommended for animations. While it works in this example, in general Thomas recommends geom_path() in conjunction with transition_reveal(). Comment: In this example, transition_manual() does not work. Comment: Traces the path of the points over 100 frames, but this can be changed with (say) animate(p1, nframes = 10, fps = 1)

Example 4b: Things that go "wrong" when attempting to animate geom_line(). Use geom_path() instead.


    # Packages + Data
    library(reprex)
    library(ggplot2)
    library(gganimate)
    library(tibble)
    df <- tibble(x = -10:10, y = abs(x)) 
    # Plots
    p1 <- ggplot(data = df, aes(x, y)) + 
        geom_line(size = 2) + 
        transition_reveal(x) +
        labs(title = "geom_line() with transition_reveal(x)")
    p1 <- animate(p1)  
    p2 <- ggplot(data = df, aes(x, y)) + 
        geom_line(size = 2) + 
        transition_reveal(y) +
        labs(title = "geom_line() with transition_reveal(y)")
    p2 <- animate(p2)  
    combine_gifs(plot1 = p1, plot2 = p2)
#> Loading required package: magick
#> Linking to ImageMagick 6.9.9.39
#> Enabled features: cairo, fontconfig, freetype, lcms, pango, rsvg, webp
#> Disabled features: fftw, ghostscript, x11


# Verdict: Not recommended.

Created on 2018-12-11 by the reprex package (v0.2.1)

thomasp85 commented 5 years ago

In general, transition_manual should only be used if you want to forego gganimate's automatic handling altogether... using it to reveal a line is not what it does...

thomasp85 commented 5 years ago

You should generally be hesitant to use geom_line with transition_reveal unless you reveal along the x-axis. using geom_path will give you the expected result

ptoche commented 5 years ago

Thanks for the feedback Thomas. I have updated the examples above. Below an updated summary.

Example 1: Objective: reveal a single point sequentially: reveal (x_{i}, y_{i}) + hide (x_{i-1}, y_{i-1}). Verdict: Success.

Example 2: Objective: reveal all points sequentially: reveal (x_{i}, y_{i}) without hiding (x_{i-1}, y_{i-1}). Verdict: Success.

Example 3: Objective: reveal a single line segment sequentially: reveal (x_{i}, y_{i}) -- (x_{i-1}, y_{i-1}) + hide (x_{i-1}, y_{i-1}) -- (x_{i-2}, y_{i-2}). Verdict: Failure.

Example 4: Objective: reveal a line sequentially: reveal (x_{i}, y_{i}) -- (x_{i-1}, y_{i-1}) without hiding (x_{i-1}, y_{i-1})-(x_{i-2}, y_{i-2}). Verdict: Success.

thomasp85 commented 5 years ago

For example 2, a simple modification is all it takes:

ggplot(data = df, aes(x, y, group = seq_along(x)) + #group just need to be unique for each point
        geom_point(size = 5) + 
        transition_reveal(x) +
        labs(title = "transition_reveal(x)")
thomasp85 commented 5 years ago

I still don't understand what you wan to achieve in example 3

ptoche commented 5 years ago

Thanks for your help Thomas. For me, with or without group = seq_along(x) I get the same output. Let me try to clarify what I'm looking for in Example 3 and get back to you.

thomasp85 commented 5 years ago
library(gganimate)
#> Loading required package: ggplot2
df <- data.frame(x = -10:10, y = abs(-10:10)) 
ggplot(data = df, aes(x, y, group = seq_along(x))) + #group just need to be unique for each point
  geom_point(size = 5) + 
  transition_reveal(x)

Created on 2018-12-11 by the reprex package (v0.2.1)

ptoche commented 5 years ago

stupid me I had the group outside the aes(). So sorry! And thanks again for your patience!!

Example 4 with fewer frames than default. The first 5 frames

print_frames <- function(data, nframes = NULL) {
    require(ggplot2)
    if (is.null(nframes)) nframes = nrow(data) 
    for (i in 1:nframes) {
        filename <- paste0("frame-", i, ".png") 
        title <- paste0("Frame", i) 
        ggplot(data[1:i, ], aes(x, y)) + 
            geom_line(size = 2)  +
            scale_x_continuous(limits = c(min(data[1]), max(data[1]))) +
            scale_y_continuous(limits = c(min(data[2]), max(data[2]))) +
            labs(title = title)  
        ggsave(filename, width = 5, height = 5)
    }    
}

print_frames(data = df, nframes = 5) 

frame-1 frame-2 frame-3 frame-4 frame-5

thomasp85 commented 5 years ago

That is basically example 4 with a low framerate

thomasp85 commented 5 years ago
library(gganimate)
#> Loading required package: ggplot2
df <- data.frame(x = -10:10, y = abs(-10:10)) 
# Plots
p1 <- ggplot(data = df, aes(x, y)) + 
  geom_path(size = 2) + 
  transition_reveal(x) +
  labs(title = "geom_path() with transition_reveal(x)")
animate(p1, nframes = 10, fps = 1)  

Created on 2018-12-11 by the reprex package (v0.2.1)

ptoche commented 5 years ago

Oh I understand now: you set nframes inside the animate() function. Excellent. Indeed I was looking for a frame rate that would match the supplied data. Thanks.

I wrote too fast, I meant this for Example 3:

print_frames <- function(data, nframes = NULL) {
        require(ggplot2)
        if (is.null(nframes)) nframes = nrow(data) 
        for (i in 1:nframes) {
            filename <- paste0("frame-", i, ".png") 
            title <- paste("Frame", i) 
            ggplot(data[i:(i+1), ], aes(x, y)) + 
                geom_line(size = 2)  +
                scale_x_continuous(limits = c(min(data[1]), max(data[1]))) +
                scale_y_continuous(limits = c(min(data[2]), max(data[2]))) +
                labs(title = title)  
            ggsave(filename, width = 5, height = 5)
        }    
    }

    print_frames(data = df, nframes = 5) 

frame-1 frame-2 frame-3 frame-4 frame-5

thomasp85 commented 5 years ago

there is currently no way to do that gracefully

ptoche commented 5 years ago

I have updated Examples 1, 2, and 4. Feel free to put them together inside a "Getting Started" wiki, if you think they could be useful. I certainly feel much more comfortable with the package having worked through them and been able to compare different options side by side. I'll close by thanking you for a great package and your kind help!

ptoche commented 5 years ago

Below an animation intended to illustrate the use of counters for transition_manual() and transition_reveal(). Get the current counter with print("{frame}"). Get the total number of frames with print("{nframes}").

Example 5: Printing counters


library(reprex)
library(ggplot2)
library(gganimate)

# Data
df <- structure(list(x = 1:50, y = c(1.13220278129597, 1.19052387615662, 
                                     1.08245817019419, 1.092026178772, 1.10044827996026, 1.10761637669097, 
                                     1.15670117446199, 1.10614650904771, 1.10850359605767, 1.10888736691465, 
                                     1.13057762527772, 1.1097108873825, 1.10079690884314, 1.06299876187194, 
                                     1.02176309803305, 1.00468521705448, 1.01677335399114, 1.05763641886816, 
                                     1.04518028693855, 1.03553964517436, 1.05894958690745, 1.05640164807922, 
                                     1.03859260999251, 1.02183113310639, 1.02216937673504, 1.02203574505795, 
                                     1.02211730017492, 1.02587886449655, 1.02756514422318, 1.03358427512605, 
                                     1.04717537432021, 1.04464239078604, 1.05032082010357, 1.04757090239588, 
                                     1.04442488099086, 1.04616044456226, 1.0379115427868, 1.03534523721584, 
                                     1.02857437409865, 1.03808867100352, 1.0402482762807, 1.04328589395803, 
                                     1.03610034851675, 1.0311223037094, 1.02037361781279, 1.00817879202418, 
                                     1.02145919273311, 1.02037759867125, 1.01944090162799, 1.02264408869067
)), row.names = c(NA, 50L), class = "data.frame")

# cumulative = FALSE
n = nrow(df)  # number of frames to match sample size
p1 <- ggplot(data = df, aes(x = x, y = y)) + 
    transition_manual(x) + 
    geom_point(size = 2, color = "darkblue") +
    labs(x = "sample size", 
         y = "sampling mean",
         title = "Number of samples = {frame},  Maximum size = {nframes}") 
p1 <- animate(p1, nframes = n, fps = 1)  

# cumulative = TRUE
n = nrow(df)  # number of frames to match sample size
p2 <- ggplot(data = df, aes(x = x, y = y, group = seq_along(x))) + 
    transition_reveal(x) + 
    geom_point(size = 2, color = "darkblue") +
    labs(x = "sample size", 
         y = "sampling mean",
         title = "Number of samples = {frame},  Maximum size = {nframes}")  
p2 <- animate(p2, nframes = n, fps = 1) 

combine_gifs(plot1 = p1, plot2 = p2)
#> Loading required package: magick
#> Linking to ImageMagick 6.9.9.39
#> Enabled features: cairo, fontconfig, freetype, lcms, pango, rsvg, webp
#> Disabled features: fftw, ghostscript, x11

Created on 2018-12-14 by the reprex package (v0.2.1)

Comments: 1. having to add group = seq_along() in the aes() is not easy to get used to. Would it be feasible to add an option inside the transition_manual(x, cumulative = TRUE)? Or does it go too far against the "grammar"?

2. The following are ways to access frame information in different settings: {current_frame}, {frame_along}, {closest_state}, {frame_time}. Are there more? It seems like rather a lot. IMHO it would be more user-friendly to have a single universal variable for all that. It could be a short-hand, something like {frame_counter}. I do think {nframes} is a good, easy to remember name.

Edit: Thomas explained to me that the counter may be accessed with {frame}. I have updated the code accordingly. Thanks Thomas!

thomasp85 commented 5 years ago

For 1) You don't need to set grouping when using transition_manual, as there is really no tweeting happening at all

To answer 2): if you simply want a counter for which frame you're at, then the frame variable can be used. This is simply counting the frame along the animation and is not related to the data you are displaying (other such variables are nframes, and progress). The reason why the other variables have different names is that they are not alike. They are not simple counters but calculated from the data as well as the specific transformations that the transition makes. Giving them the same name would imply they are alike

ptoche commented 5 years ago

Thanks Thomas.

Oh I didn't realize there was a frame variable for that. Great. I'll update the code with that information.

Do you mean there's a simpler way to obtain the "cumulative" effect?

Right now, this is my understanding:

# Animate points on/off:
ggplot(data = df, aes(x, y)) + 
    transition_manual(x) +
    geom_point() + 

# Animate points on/on
ggplot(data = df, aes(x, y, group = seq_along(x))) + 
    transition_reveal(x) +
    geom_point()  

So it's easy enough to remember this:

But much harder to remember to also add group = seq_along(x) to obtain the "cumulative" effect.

I for one am likely to remember this, now that I've talked about it so much, I just think it's a bit of a hurdle for a beginner. But perhaps I've missed an easier way to do this (that is, Example 2 above)?

thomasp85 commented 5 years ago

There are many different ways to achieve seemingly identical results on. The different transitions differ, not in what type of output they can create, but in how they interpret the layer data and create an animation for that. In much the same way that "complaining" that you have to sort the data by x in order to make geom_path behave like geom_line doesn't make much sense it also doesn't make much sense to complain that transition_reveal requires special operations to behave like transition_manual (I'm using "complain" here in a very informal and collegial way)

ptoche commented 5 years ago

I see thanks Thomas. I'm not complaining, just giving feedback about a new user's experience with the package. My intention was just to give you constructive feedback. APOLOGIES if it sounded like a complain, it wasn't.

On another matter: Example 5 crashed at around 1,000 points (two plots are created each with 1,000 points and it crashes on my 2017 MacBook Pro). It's not a complaint (!). Gifs with so many points were not such a good idea to begin with. :-)

thomasp85 commented 5 years ago

I know, hence the final parenthesis - I used "complain" for lack of better word...

Your crash is related to the magick package when assembling the two plots. ImageMagick loads all images in to memory before assembling the gif, so at a certain point there are too many frames to handle... gifski is much better to handle this as it only have a single frame in memory at a time

ptoche commented 5 years ago

Oh thanks, I'll look into gifski !

ptoche commented 5 years ago

I just saw your Getting Started doc (https://gganimate.com/articles/gganimate.html). As I don't know how to leave messages there, I'll leave a brief comment here: Very nice!