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.
You could install the released version of the package from CRAN or the development version from Github.
# From CRAN
install.packages("animate")
# From Github
remotes::install_github("kcf-jackson/animate")
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.
Sometimes it can be convenient to attach the device so that the functions of the device can be called directly.
off
function, simply
restarting R will close the connection.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 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
.
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.
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.
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.
When you are done. Don’t forget to switch-off and detach the device
with off(); detach(device)
.
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.
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:
# 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
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.
# 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
\[\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}\]
# 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.
\[\begin{aligned} \dfrac{dx_i}{dt} = u_i, \quad \dfrac{dy_i}{dt} = v_i, \quad i = 1, 2, ..., n \end{aligned}\]
# 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.
# 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.
In the code chunk of an R Markdown document,
animate$new
with the virtual = TRUE
flag,rmd_animate(device)
.Here is an example. (Click to begin the animation.)
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
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.
To use the animate plot in a Shiny app,
animateOutput
in the ui
,session
argument to
animate$new
during initialisation.server
directly
inside any of the shiny::observeEvent
.Here is a full example:
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)