--- title: "An introduction to 'animate'" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{An introduction to 'animate'} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` ## 1. Introduction The R package 'animate' implements a web-based graphics device to let you create animated visualisation easily with the base R syntax, supporting both frame-by-frame and key-frame animations, and share them as GIF images, MP4 videos, 'Shiny' apps or web pages. With 'animate', users can easily bring their creations to life, creating dynamic and engaging visualisations. Let's take a quick look at what 'animate' is capable of. #### Example 1 ![An animated plot showing the correspondence between the stationary points of a function $f(x)$ and the roots of the derivative $f'(x)$.](https://github.com/kcf-jackson/animate/raw/gh-pages/examples/screenshots/handdrawn_plot_2_HD.gif){width="80%"} #### Example 2 ![An animated diagram about granting parole to prisoners, replicated from [this article](https://www.themarshallproject.org/2015/08/04/the-new-science-of-sentencing)](https://github.com/kcf-jackson/animate/raw/gh-pages/examples/screenshots/parole.gif){width="80%"} More examples can be found in the gallery [here](https://github.com/kcf-jackson/animate#gallery)! ## 2. Installation and device usage ### a. Installation You could install the released version of the package from CRAN or the development version from Github. ```{r} # From CRAN install.packages("animate") # From Github remotes::install_github("kcf-jackson/animate") ``` ### b. Initialising and using the device To use the device, load the package and call `animate$new` with the `width` and `height` arguments (in pixel values) to initialise the device. It may take some time for the device to start (about half a second); making function calls before the start-up process completes would result in a warning. This could happen when you source a file, which starts the device and runs the plot functions before the device is ready. #### Usage 1 All the plotting functions are stored as the methods of the device. ```{r} library(animate) device <- animate$new(width = 500, height = 300) # takes ~0.5s device$plot(1:10, 1:10) device$points(1:10, 10 * runif(10), bg = "red") device$lines(1:100, sin(1:100 / 10 * pi / 2)) device$clear() device$off() # switch off the device when you are done ``` #### Usage 2 Sometimes it can be convenient to attach the device so that the functions of the device can be called directly. ```{r} library(animate) device <- animate$new(500, 300) attach(device) # overrides the 'base' primitives plot(1:10, 1:10) points(1:10, 10 * runif(10), bg = "red") lines(1:100, sin(1:100 / 10 * pi / 2)) clear() off() detach(device) # restore the 'base' primitives ``` #### Remarks - Only one device is supported per R session. If a device fails to initialise, it is usually because there is another device currently occupying the session. - In case one forgets to assign the device to a variable and so does not have the handle to call the `off` function, simply restarting R will close the connection. ## 3. Creating animated plots The **most important** idea of this package is that **every object to be animated on the screen must have an ID**. These IDs are used to decide which objects need to be modified to create the animation effect. ### a. Setup We first set up the device for the remaining of this section. ```{r} device <- animate$new(400, 400) attach(device) ``` ### b. Keyframe animation A basic plot can be made with the usual syntax `plot(x, y)` and the additional argument `id`. `id` expects a character vector, and its length should match the number of data points. If we provide a new set of coordinates while keeping the same `id`, the package will recognise that it should update the points rather than plot new ones. Setting the argument `transition = TRUE` creates a transition effect from the old coordinates to the new coordinates. It can handle multiple attributes, and it supports two timing options `duration` and `delay`. ```{r} x <- c(0.5, 1, 0.5, 0, -0.5, -1, -0.5, 0) y <- c(0.5, 0, -0.5, -1, -0.5, 0, 0.5, 1) id <- new_id(x) # Give each point an ID: c("ID-1", "ID-2", ..., "ID-8") plot(x, y, id = id) # Transition (basic) shuffle <- c(8, 1:7) plot(x[shuffle], y[shuffle], id = id, transition = TRUE) # Use transition # Transition (with multiple attributes and timing option) shuffle <- c(7:8, 1:6) plot(x[shuffle], y[shuffle], id = id, cex = 1:8 * 20, # another attribute for transition transition = list(duration = 2000)) # 2000ms transition duration # Note: the unit for `cex` is squared pixels, e.g., a value of 400 has dimension 20 x 20. # This follows the convention used by 'D3.js' to handle the size of different shapes. ``` Click to see the transition; click again to reset. ```{r, eval = T, echo = F} animate::insert_animate("introduction/basic_plot.json", style = "display:block; margin:auto; width:450px; height:450px; border:none;") ``` ### c. Frame-by-frame animation Some applications require rapidly plotting a sequence of frames; this can be done easily with a loop. - There should be pauses between iterations, or else the animation will happen so quickly that only the last key frame can be seen. - The `lines` function treats the entire line as a single unit, even if it contains multiple points, so only one ID is required. ```{r} clear() # Clear the canvas par(xlim = c(-17, 16), ylim = c(-18, 13)) # Use a static scale t <- seq(0, 2*pi, length.out = 150) x <- 16 * sin(t)^3 y <- 13 * cos(t) - 5 * cos(2*t) - 2 * cos(3*t) - cos(4*t) id <- "line-1" # a line needs 1 ID only (despite containing multiple points) for (n in seq_along(t)) { plot(x[1:n], y[1:n], id = id, type = 'l') Sys.sleep(0.02) # about 50 frames per second } ``` Click to see the animation. ```{r, eval = T, echo = F} animate::insert_animate("introduction/basic_lines.json.gz", animate::click_to_loop(start = 4), style = "display:block; margin:auto; width:450px; height:450px; border:none;") ``` When you are done. Don't forget to switch-off and detach the device with `off(); detach(device)`. ### d. More styling options The package currently supports the following primitives in addition to the `plot` function: `points`, `lines`, `bars`, `text`, `image` and `axis`. These primitive functions accept the commonly used graphical parameters such as `cex`, `lwd`, and `bg`. As `animate` is based on `D3.js`, it also accepts the `attr`, `style` and `transition` arguments. This is useful for specifying options that are not part of R, such as CSS styles, or that are part of R but have not yet been implemented. For instance, for the `text` function, the font family can be specified using `attr = list("font-family" = "monospace")`. More examples can be found in the other vignettes on the [package website](https://kcf-jackson.github.io/animate/). ## 4. Dynamic scale versus static scale An critical difference between `animate` and `base` is that `animate` uses dynamic scale. Each time any graphics function is called, a new scale specific to the call is created. To see why this is important, consider the following two snippets: ```{r, echo = TRUE} # Plot 1 (Left) clear() plot(0:9, 0:9, type = 'l') lines(-4:0, 4:0, col = "red") # the `lines` function seems to be wrong # Plot 2 (Right: notice the changes in the axes) clear() plot(0:9, 0:9, type = 'l') plot(-4:0, 4:0, col = "red", type = 'l') # showing what had happened when `lines` was called ``` ```{r, echo = FALSE, eval = TRUE, message = FALSE} library(animate) device <- animate$new(300, 200, virtual = TRUE) attach(device) par(mai = c(0.18, 0.082, 0.082, 0.082)) plot(0:9, 0:9, type = 'l') lines(-4:0, 4:0, col = "red") text(0.7, -1, xlim = c(1, 10), ylim = c(1, 10), "Plot 1: the red line seems to be at the wrong location.", attr = list("font-size" = "12px")) svg(300, 200, id = "svg_2", style = list(left = "350px", position = "absolute")) par(mai = c(0.18, 0.082, 0.082, 0.082)) plot(0:9, 0:9, type = 'l') plot(-4:0, 4:0, col = "red", type = 'l') text(1.5, -1, xlim = c(1, 10), ylim = c(1, 10), "Plot 2: a new scale is used for every new call.", attr = list("font-size" = "12px")) rmd_animate(device, loop(1), style = "min-width: 700px; min-height:250px; border:none;") ``` In the left plot, we saw that the `lines` function seems to plot at the incorrect location, e.g. the point (-4,4) is located at (0, 9). But if you replace the `lines` function with the `plot` function, you will see that the second `plot` function (as well as the original `lines` function) actually uses its own scale and the line is in the correct location. This is something to keep in mind when using `animate`. In case you want to use a static scale, as in the regular `base` plot, you can specify it in the call with the `xlim` and `ylim` arguments or set the global option via `par(xlim = ..., ylim = ...)` before making the plot. ```{r} # Inline static range (Left) clear() plot(0:9, 0:9, type = 'l', xlim = c(-4, 10), ylim = c(0, 9)) lines(-4:0, 4:0, col = "red", xlim = c(-4, 10), ylim = c(0, 9)) # Global static range (Right) clear() par(xlim = c(-4, 10), ylim = c(0, 9)) plot(0:9, 0:9, type = 'l') lines(-4:0, 4:0, col = "red") # par(xlim = NULL, ylim = NULL) # Clear the global setting ``` ```{r, echo = FALSE, eval = TRUE, message = FALSE} # Correct range library(animate) device <- animate$new(312, 200, virtual = TRUE) attach(device) plot(0:9, 0:9, type = 'l', xlim = c(-4, 10), ylim = c(0, 9)) lines(-4:0, 4:0, col = "red", xlim = c(-4, 10), ylim = c(0, 9)) svg(312, 200, id = "svg_2", style = list(left = "350px", position = "absolute")) par(xlim = c(-4, 10), ylim = c(0, 9)) plot(0:9, 0:9, type = 'l') lines(-4:0, 4:0, col = "red") rmd_animate(device, loop(1), style = "min-width: 700px; min-height:250px; border:none;") ``` ## 5. Three full examples ### Lorenz system $$\begin{aligned} \dfrac{dx}{dt} = \sigma (y - x), \quad \dfrac{dy}{dt} = x (\rho - z) - y, \quad \dfrac{dz}{dt} = xy - \beta z \end{aligned}$$ #### Simulating and visualising the Lorenz system ```{r} # device <- animate$new(500, 300) # attach(device) # Lorenz system parameters sigma <- 10 beta <- 8/3 rho <- 28 # Set up for the Euler method x <- y <- z <- 1 dx <- dy <- dz <- 0 xs <- x ys <- y zs <- z time_steps <- 1:2000 dt <- 0.015 # Frame-by-frame animation with a for loop for (t in time_steps) { dx <- sigma * (y - x) * dt dy <- (x * (rho - z) - y) * dt dz <- (x * y - beta * z) * dt x <- x + dx y <- y + dy z <- z + dz xs <- c(xs, x) ys <- c(ys, y) zs <- c(zs, z) # `animate` plot par(xlim = c(-30, 30), ylim = c(-30, 40)) plot(x, y, id = "ID-1") lines(xs, ys, id = "lines-1") Sys.sleep(0.025) } # Special transition to x-z plane to show the 'bufferfly' par(xlim = c(-30, 30), ylim = range(zs)) plot(x, z, id = "ID-1", transition = TRUE) lines(xs, zs, id = "lines-1", transition = TRUE) # off() # detach(device) ``` Click to begin the animation. ```{r, echo = F, eval = T} animate::insert_animate("introduction/Lorenz_system.json.gz", animate::click_to_loop(wait = 10), style = "display:block; margin:auto; width:620px; height:420px; border:none;") ``` ### A particle system $$\begin{aligned} \dfrac{dx_i}{dt} = u_i, \quad \dfrac{dy_i}{dt} = v_i, \quad i = 1, 2, ..., n \end{aligned}$$ #### Simulating and visualising the particle system ```{r} # device <- animate$new(500, 500) # attach(device) # Number of particles n <- 50 # Data of the particles ps <- list( x = runif(n), y = runif(n), # Position vx = rnorm(n) * 0.01, vy = rnorm(n) * 0.01, # Velocity color = sample(c("black", "red"), n, replace = TRUE), # Class id = new_id(x) # ID ) # Simulation of the evolution of one time step update_one_step <- function(ps) { # Turns around when the particle hits the boundary x_turn <- ps$x + ps$vx > 1 | ps$x + ps$vx < 0 ps$vx[x_turn] <- ps$vx[x_turn] * -1 y_turn <- ps$y + ps$vy > 1 | ps$y + ps$vy < 0 ps$vy[y_turn] <- ps$vy[y_turn] * -1 # Update position ps$x <- ps$x + ps$vx ps$y <- ps$y + ps$vy ps } # Visualising the system par(xlim = c(0, 1), ylim = c(0, 1)) for (t in 1:1000) { points(ps$x, ps$y, id = ps$id, bg = ps$color) ps <- update_one_step(ps) Sys.sleep(0.02) } # off() # detach(device) ``` Click to begin the animation. ```{r, echo = F, eval = T} animate::insert_animate("introduction/particle_system.json.gz", animate::click_to_loop(wait = 20), style = "display:block; margin:auto; width:520px; height:520px; border:none;") ``` ### 2 dimensional discrete random walk #### Simulating and visualising the random walk ```{r} # library(animate) # device <- animate$new(height = 500, width = 500) # attach(device) # Simulation random_walk <- function(n_steps) { steps <- sample(list(c(-1, 0), c(1, 0), c(0, -1), c(0, 1)), n_steps, replace = T) coord <- do.call(rbind, steps) x <- c(0, cumsum(coord[,1])) y <- c(0, cumsum(coord[,2])) data.frame(x = x, y = y) } # Setup set.seed(20230105) n_steps <- 250 n_walkers <- 3 color <- c("black", "orange", "blue", "green", "red") walkers <- lapply(1:n_walkers, function(ind) random_walk(n_steps)) # Use static range (combine the data frames to find the common range) xlim <- range(do.call(rbind, walkers)$x) ylim <- range(do.call(rbind, walkers)$y) par(xlim = xlim, ylim = ylim) # Plot looping through time and the walkers for (i in 1:n_steps) { for (ind in 1:n_walkers) { x <- walkers[[ind]]$x y <- walkers[[ind]]$y plot(x[1:i], y[1:i], type = 'l', id = paste0("line-", ind), col = color[ind]) points(x[i], y[i], id = paste0("point-", ind), bg = color[ind]) } Sys.sleep(0.02) } # off() # detach(device) ``` Click to begin the animation. ```{r, echo = F, eval = T} animate::insert_animate("introduction/random_walk_2d.json.gz", animate::click_to_loop(wait = 10, start = 4), style = "display:block; margin:auto; width:520px; height:520px; border:none;") ``` ## 6. Usage with RMarkdown Document and Shiny ### RMarkdown Document #### Inline usage In the code chunk of an R Markdown document, - Call `animate$new` with the `virtual = TRUE` flag, - then at the end of the code chunk, call `rmd_animate(device)`. Here is an example. (Click to begin the animation.) ```{r, echo = T, eval = T, message = F} library(animate) device <- animate$new(500, 500, virtual = TRUE) attach(device) # Data id <- new_id(1:10) s <- 1:10 * 2 * pi / 10 s2 <- sample(s) # Plot par(xlim = c(-2.5, 2.5), ylim = c(-2.5, 2.5)) plot(2*sin(s), 2*cos(s), id = id) points(sin(s2), cos(s2), id = id, transition = list(duration = 2000)) # Render in-line in an R Markdown document rmd_animate(device, click_to_play(start = 3)) # begin the plot at the third frame ``` #### Import from a file To include an exported visualisation (from `device$export`) in an R Markdown Document, simply use `animate::insert_animate` to insert the visualisation in a code chunk. The function supports several playback options, including the `loop`, `click_to_loop` and `click_to_play` options. Customisation is possible, but it would require some JavaScript knowledge. Interested readers may want to look into the source code of the functions above before deciding to pursue that option. ### Shiny To use the animate plot in a Shiny app, - use `animateOutput` in the `ui`, - pass the shiny `session` argument to `animate$new` during initialisation. - then use the device function in the `server` directly inside any of the `shiny::observeEvent`. Here is a full example: ```{r} library(shiny) library(animate) ui <- fluidPage( actionButton("buttonPlot", "Plot"), actionButton("buttonPoints", "Points"), actionButton("buttonLines", "Lines"), animateOutput() ) server <- function(input, output, session) { device <- animate$new(500, 300, session = session) id <- new_id(1:10) observeEvent(input$buttonPlot, { # Example 1 device$plot(1:10, 1:10, id = id) }) observeEvent(input$buttonPoints, { # Example 2 device$points(1:10, runif(10, 1, 10), id = id, transition = TRUE) }) observeEvent(input$buttonLines, { # Example 3 x <- seq(1, 10, 0.1) y <- sin(x) id <- "line_1" device$lines(x, y, id = id) for (n in 11:100) { x <- seq(1, n, 0.1) y <- sin(x) device$lines(x, y, id = id) Sys.sleep(0.05) } }) } shinyApp(ui = ui, server = server) ```