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.
::install_github("kcf-jackson/animate") remotes
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)
<- 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 done device
Sometimes it can be convenient to attach the device so that the functions of the device can be called directly.
library(animate)
<- animate$new(600, 400)
device 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
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.
We first set up the device for the remaining of this section.
<- animate$new(600, 400)
device 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.
<- 1:10
x <- 1:10
y <- new_id(x) # Give each point an ID: c("ID-1", "ID-2", ..., "ID-10")
id plot(x, y, id = id)
<- 10:1
new_y plot(x, new_y, id = id, transition = TRUE) # Use transition
Click 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
<- 1:10
x <- 10 * runif(10)
y <- new_id(y, prefix = "points") # Give each point an ID
id plot(x, y, id = id, bg = "red")
<- 10 * runif(10)
new_y 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
<- 1:100
x <- sin(x / 5 * pi / 2)
y <- "line-1" # a line needs only 1 ID (as the entire line is considered as one unit)
id plot(x, y, id = id, type = 'l')
for (n in 101:200) {
<- 1:n
new_x <- sin(new_x / 5 * pi / 2)
new_y 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
<- function(sigma = 10, beta = 8/3, rho = 28, x = 1, y = 1, z = 1, dt = 0.015) {
Lorenz_sim # Auxiliary variables
<- dy <- dz <- 0
dx <- x
xs <- y
ys <- z
zs <- environment() # a neat way to capture all the variables
env
# Update the variables using the ODE within 'env'
<- function(n = 1) {
step for (i in 1:n) {
evalq(envir = env, {
<- sigma * (y - x) * dt
dx <- (x * (rho - z) - y) * dt
dy <- (x * y - beta * z) * dt
dz <- x + dx
x <- y + dy
y <- z + dz
z <- c(xs, x)
xs <- c(ys, y)
ys <- c(zs, z)
zs
})
}
}
env }
# device <- animate$new(600, 400)
# attach(device)
<- Lorenz_sim()
world 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))
$step()
worldSys.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}\]
<- function(num_particles = 50) {
particle_sim # Particles move within the unit box
<- runif(num_particles)
x <- runif(num_particles)
y <- rnorm(num_particles) * 0.01
vx <- rnorm(num_particles) * 0.01
vy <- new_id(x)
id <- sample(c("black", "red"), num_particles, replace = TRUE, prob = c(0.5, 0.5))
color
<- environment()
env <- function(n = 1) {
step for (i in 1:n) {
evalq(envir = env, {
# The particles turn around when they hit the boundary of the box
<- x + vx > 1 | x + vx < 0
x_turn <- vx[x_turn] * -1
vx[x_turn]
<- y + vy > 1 | y + vy < 0
y_turn <- vy[y_turn] * -1
vy[y_turn]
<- x + vx
x <- y + vy
y
})
}
}
env }
# device <- animate$new(500, 500)
# attach(device)
<- particle_sim(num_particles = 50)
world for (i in 1:1000) {
points(world$x, world$y, id = world$id, bg = world$color, xlim = c(0, 1), ylim = c(0, 1))
$step()
worldSys.sleep(0.02)
}# off()
# detach(device)
Click to begin the visualisation
<- function(grid_size = 20, num_walkers = 10) {
random_walk_sim <- seq(0, 1, length.out = grid_size)
.side <- expand.grid(.side, .side)
grid <- paste("ID", 1:grid_size^2, sep = "-")
id
<- function(n) c(ceiling(n / grid_size), (n-1) %% grid_size + 1)
.index_to_coord <- function(x) (x[1] - 1) * grid_size + x[2]
.coord_to_index <- function(coord) {
.step <- sample(list(c(-1,0), c(1,0), c(0,-1), c(0,1)), 1)[[1]]
k + k - 1) %% grid_size + 1
(coord
}
<- sample(grid_size^2, num_walkers)
.walkers_index <- Map(.index_to_coord, .walkers_index)
.walkers_coord <- sample(c("red", "green", "blue", "black", "orange"), num_walkers, replace = TRUE)
.walkers_color <- rep("lightgrey", grid_size^2)
color <- .walkers_color
color[.walkers_index]
<- environment()
env <- function() {
step evalq(envir = env, expr = {
# Update each walker's coordinate and change the color state
<- Map(.step, .walkers_coord)
.walkers_coord <- unlist(Map(.coord_to_index, .walkers_coord))
.walkers_index <- rep("lightgrey", grid_size^2)
color <- .walkers_color
color[.walkers_index]
})
}
env }
# device <- animate$new(600, 600)
# attach(device)
set.seed(123)
<- random_walk_sim(grid_size = 15, num_walkers = 8)
world for (i in 1:100) {
<- world$grid
coord points(coord[,1], coord[,2], id = world$id, bg = world$color, pch = "square", cex = 950, col = "black")
$step()
worldSys.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)
<- animate$new(500, 500, virtual = TRUE)
device attach(device)
# Data
<- new_id(1:10)
id <- 1:10 * 2 * pi / 10
s <- sample(s)
s2
# 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
,server
directly
inside any of the shiny::observeEvent
.Here is a full example:
library(shiny)
library(animate)
<- fluidPage(
ui actionButton("buttonPlot", "Plot"),
actionButton("buttonPoints", "Points"),
actionButton("buttonLines", "Lines"),
animateOutput()
)
<- function(input, output, session) {
server <- animate$new(600, 400, session = session)
device <- new_id(1:10)
id
observeEvent(input$buttonPlot, { # Example 1
$plot(1:10, 1:10, id = id)
device
})
observeEvent(input$buttonPoints, { # Example 2
$points(1:10, runif(10, 1, 10), id = id, transition = TRUE)
device
})
observeEvent(input$buttonLines, { # Example 3
<- seq(1, 10, 0.1)
x <- sin(x)
y <- "line_1"
id $lines(x, y, id = id)
devicefor (n in 11:100) {
<- seq(1, n, 0.1)
x <- sin(x)
y $lines(x, y, id = id)
deviceSys.sleep(0.05)
}
})
}
shinyApp(ui = ui, server = server)