The R package ‘animate’ implements a web-based graphics device that models on the base R syntax and is powered by d3.js. The device is developed using the sketch package and targets real-time animated visualisations. The key use cases in mind are agent-based modelling and dynamical system, and it may also find applications in sports analytics, board game analysis and basic animated charting.
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; making function calls before the start-up process completes
would result in a warning.
library(animate)
device <- animate$new(width = 600, height = 400) # 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 doneSometimes it can be convenient to attach the device so that the functions of the device can be called directly.
library(animate)
device <- animate$new(600, 400)
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' primitivesoff 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.
We first set up the device for the remaining of this section.
device <- animate$new(600, 400)
attach(device)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.
To animate the points, we provide a new set of coordinates while
using the same id. The package would know it should update
the points rather than plotting new ones. As an option, setting the
argument transition = TRUE creates a transition effect from
the old coordinates to the new coordinates.
x <- 1:10
y <- 1:10
id <- new_id(x) # Give each point an ID: c("ID-1", "ID-2", ..., "ID-10")
plot(x, y, id = id)
new_y <- 10:1
plot(x, new_y, id = id, transition = TRUE) # Use transitionClick to see the transition; click again to reset.
The transition effect can handle multiple attributes at the same
time, and the transition argument supports other
options.
clear() # Clear the canvas
x <- 1:10
y <- 10 * runif(10)
id <- new_id(y, prefix = "points") # Give each point an ID
plot(x, y, id = id, bg = "red")
new_y <- 10 * runif(10)
points(x, new_y, id = id, bg = "lightgreen", cex = 1:10 * 30, transition = list(duration = 2000))Click to see the transition; click again to reset.
Some applications require plotting a sequence of key frames rapidly. This can be done easily with a loop. There should be pauses between iterations, otherwise the animation will happen so quickly that only the last key frame can be seen.
clear() # Clear the canvas
x <- 1:100
y <- sin(x / 5 * pi / 2)
id <- "line-1" # a line needs only 1 ID (as the entire line is considered as one unit)
plot(x, y, id = id, type = 'l')
for (n in 101:200) {
new_x <- 1:n
new_y <- sin(new_x / 5 * pi / 2)
plot(new_x, new_y, 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. While they are all modelled on
the base R syntax, there are some differences. This is because static
plots and animated plots are inherently different, so different
assumptions are used to manage the device and its graphics setting.
In the base package, a plot needs to be
made before any other primitives can be used. animate
decouples that link, and each primitive uses their own scale computed
based on the data provided and can be used independently. This feature
is needed because base plot mostly works under the setting
that the scale of the plot is held constant, while for animated plot,
the scale may be changing frequently. In case one wants to keep the
scale (and axes) constant in animate, the xlim
and ylim arguments can be used - either directly in the
function call or as the default parameters of the device set using the
par function.
The primitive functions support the commonly-used graphical
parameters like cex, lwd, bg,
etc. To use options that are beyond the base R interface, e.g. the
transition argument, or options that are part of the R
interface but have not been implemented, one can use the
attr, style and transition
arguments. For instance, for the text function, the font
family can be specified using
attr = list("font-family" = "monospace").
For the lines function, the entire line is considered as
one unit despite containing multiple points, and so only one ID is
needed.
\[\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}\]
# Define the simulation system
Lorenz_sim <- function(sigma = 10, beta = 8/3, rho = 28, x = 1, y = 1, z = 1, dt = 0.015) {
# Auxiliary variables
dx <- dy <- dz <- 0
xs <- x
ys <- y
zs <- z
env <- environment() # a neat way to capture all the variables
# Update the variables using the ODE within 'env'
step <- function(n = 1) {
for (i in 1:n) {
evalq(envir = env, {
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)
})
}
}
env
}# device <- animate$new(600, 400)
# attach(device)
world <- Lorenz_sim()
for (i in 1:2000) {
plot(world$x, world$y, id = "ID-1", xlim = c(-30, 30), ylim = c(-30, 40))
lines(world$xs, world$ys, id = "lines-1", xlim = c(-30, 30), ylim = c(-30, 40))
world$step()
Sys.sleep(0.025)
}
# Switch to xz-plane
plot(world$x, world$z, id = "ID-1", xlim = c(-30, 30), ylim = range(world$zs), transition = TRUE)
lines(world$xs, world$zs, id = "lines-1", xlim = c(-30, 30), ylim = range(world$zs), transition = TRUE)
# off()
# detach(device)Click to begin the visualisation
\[\begin{aligned} \dfrac{dx_i}{dt} = u_i, \quad \dfrac{dy_i}{dt} = v_i, \quad i = 1, 2, ..., n \end{aligned}\]
particle_sim <- function(num_particles = 50) {
# Particles move within the unit box
x <- runif(num_particles)
y <- runif(num_particles)
vx <- rnorm(num_particles) * 0.01
vy <- rnorm(num_particles) * 0.01
id <- new_id(x)
color <- sample(c("black", "red"), num_particles, replace = TRUE, prob = c(0.5, 0.5))
env <- environment()
step <- function(n = 1) {
for (i in 1:n) {
evalq(envir = env, {
# The particles turn around when they hit the boundary of the box
x_turn <- x + vx > 1 | x + vx < 0
vx[x_turn] <- vx[x_turn] * -1
y_turn <- y + vy > 1 | y + vy < 0
vy[y_turn] <- vy[y_turn] * -1
x <- x + vx
y <- y + vy
})
}
}
env
}# device <- animate$new(500, 500)
# attach(device)
world <- particle_sim(num_particles = 50)
for (i in 1:1000) {
points(world$x, world$y, id = world$id, bg = world$color, xlim = c(0, 1), ylim = c(0, 1))
world$step()
Sys.sleep(0.02)
}
# off()
# detach(device)Click to begin the visualisation
random_walk_sim <- function(grid_size = 20, num_walkers = 10) {
.side <- seq(0, 1, length.out = grid_size)
grid <- expand.grid(.side, .side)
id <- paste("ID", 1:grid_size^2, sep = "-")
.index_to_coord <- function(n) c(ceiling(n / grid_size), (n-1) %% grid_size + 1)
.coord_to_index <- function(x) (x[1] - 1) * grid_size + x[2]
.step <- function(coord) {
k <- sample(list(c(-1,0), c(1,0), c(0,-1), c(0,1)), 1)[[1]]
(coord + k - 1) %% grid_size + 1
}
.walkers_index <- sample(grid_size^2, num_walkers)
.walkers_coord <- Map(.index_to_coord, .walkers_index)
.walkers_color <- sample(c("red", "green", "blue", "black", "orange"), num_walkers, replace = TRUE)
color <- rep("lightgrey", grid_size^2)
color[.walkers_index] <- .walkers_color
env <- environment()
step <- function() {
evalq(envir = env, expr = {
# Update each walker's coordinate and change the color state
.walkers_coord <- Map(.step, .walkers_coord)
.walkers_index <- unlist(Map(.coord_to_index, .walkers_coord))
color <- rep("lightgrey", grid_size^2)
color[.walkers_index] <- .walkers_color
})
}
env
}# device <- animate$new(600, 600)
# attach(device)
set.seed(123)
world <- random_walk_sim(grid_size = 15, num_walkers = 8)
for (i in 1:100) {
coord <- world$grid
points(coord[,1], coord[,2], id = world$id, bg = world$color, pch = "square", cex = 950, col = "black")
world$step()
Sys.sleep(0.3)
}
# off()
# detach(device)Click to begin the visualisation
In the code chunk of an R Markdown document,
animate$new with the virtual = TRUE
flag,rmd_animate(device).Here is an example:
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 frameTo 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,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(600, 400, 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)