This was a project to help me learn gganimate -- and do something useful for teaching the UWA unit ENVT5503 'Remediation of Soil and Groundwater'.

First we need to load the required R packages. Obviously we need gganimate (which also loads ggplot2). We use the reshape2 package to convert our data from timesteps-as-columns to timesteps-as-single-factor format. We need the gifski package to render a ggplot2 object to an animated gif file.

library(gganimate)
## Loading required package: ggplot2
## Learn more about the underlying theory at https://ggplot2-book.org/
library(reshape2)
library(gifski)
npts <- 500 # number of points to represent solute pulse
nd <- 11 # number of depths to simulate
ds <- data.frame(d=seq(1,96,l=nd), # data fram with d = list of depths,
                 s=seq(0.3,6,l=nd)) #    s = standard deviation at each depth
x0 <- runif(npts)  # spread the x values aross a nominal range
y0 <- rnorm(npts,ds$d[1],ds$s[1]) # calculate y for each x
plot(x0, y0, xlim = c(0,1.25), ylim=c(105,0),pch=16, xaxt="n", xlab="",
     ylab = "Depth", cex = 2, 
     col = rainbow(nd, v=0.7, end=0.8, alpha=0.25)[1]) # plot first depth
for(i in 2:nd) { # plot remaining depths
  points(x0, rnorm(npts, ds$d[i], ds$s[i]), cex = 2,
         pch = 16, col = rainbow(nd, v=0.7, end=0.8, alpha=0.25)[i])
}
legend("right", legend=seq(1,nd), pch =16, title = "Step", bty = "n",
       col = rainbow(nd, v=0.7, end=0.8, alpha=0.75), 
       pt.cex = 1.5, y.intersp = 1.6)
Figure 1. Plot to show our simulated dataset.

Figure 1. Plot to show our simulated dataset.

pulse <- data.frame(x0 = runif(npts))
for(i in 0:(nd-1)) {
  pulse[,paste0("time_",i)] <- rnorm(npts, ds$d[i+1], ds$s[i+1])
}
str(pulse)
## 'data.frame':    500 obs. of  12 variables:
##  $ x0     : num  0.702 0.644 0.277 0.618 0.191 ...
##  $ time_0 : num  1.471 1.205 1.121 0.891 1.049 ...
##  $ time_1 : num  9.95 11.85 10.74 9.62 11.5 ...
##  $ time_2 : num  21.9 21.6 22.8 21.5 18.5 ...
##  $ time_3 : num  32.7 27.7 29.9 29.3 31.6 ...
##  $ time_4 : num  38.6 40.6 40.1 42.1 40.6 ...
##  $ time_5 : num  45.5 49.7 52 44.1 47.6 ...
##  $ time_6 : num  58.4 52.7 59.2 59.9 56.3 ...
##  $ time_7 : num  70.3 61.6 65 71 64 ...
##  $ time_8 : num  74.9 72.6 70.5 79 72.2 ...
##  $ time_9 : num  79.9 83.3 84.9 88.7 83.1 ...
##  $ time_10: num  97.9 92.3 98.7 103.1 86.2 ...

The data we have created has time steps as separate columns. We actually want a single column factor column with the time steps as levels. For this we use the melt() function in the reshape2 package, which does this seamlessly if we get the options correct:

pulseFact <- melt(pulse, measure.vars=2:ncol(pulse), 
                     variable.name = "Timestep", value.name = "Particles")
str(pulseFact)
## 'data.frame':    5500 obs. of  3 variables:
##  $ x0       : num  0.702 0.644 0.277 0.618 0.191 ...
##  $ Timestep : Factor w/ 11 levels "time_0","time_1",..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ Particles: num  1.471 1.205 1.121 0.891 1.049 ...

Make the animation

The animation is based on a standard ggplot:

ggplot(pulseFact, aes(x=x0, y=Particles)) + 
  scale_y_reverse() +
  geom_point(size=4, shape = 16, color="#0000B040") +
  theme_bw() +
  theme(legend.position="none",
        axis.ticks.x = element_blank(),
        axis.text.x = element_blank(),
        title = element_text(size=18, face = "bold", colour = "blue3"),
        axis.title = element_text(size = 22, face = "bold", colour="black"),
        axis.text = element_text(size = 18),
        panel.border = element_rect(colour = 1,fill=NA)) + 
  labs(y="Depth (cm)", x = "") 
Figure 2. Simulated solute distribution at all time steps at once!

Figure 2. Simulated solute distribution at all time steps at once!

We can see that this just plots all our data at once, which is missing the point! We use the transition_states() function in gganimate to animate timestep by timestep, with some other options to control how the animation works.

pulseAnim <- ggplot(pulseFact, aes(x=x0, y=Particles)) + 
  scale_y_reverse() +
  geom_point(size=4, shape = 16, color="#0000B040") +
  theme_bw() +
  theme(legend.position="none",
        axis.ticks.x = element_blank(),
        axis.text.x = element_blank(),
        title = element_text(size=18, face = "bold", colour = "blue3"),
        axis.title = element_text(size = 22, face = "bold", colour="black"),
        axis.text = element_text(size = 18),
        panel.border = element_rect(colour = 1,fill=NA)) + 
  labs(y="Depth (cm)", x = "") + 
  transition_states(pulseFact$Timestep,
                    transition_length = 1,
                    state_length = c(3,rep(0,10),3),
                    wrap = FALSE) +
  enter_fade() + 
  exit_fade() + 
  shadow_wake(wake_length = 0.05, size = T, colour="#60606040") +
  ggtitle("Distribution of solute at {closest_state}")

Now that we have made a gganimate object, we can save it as an animated gif file using the anim_save() function from the gifski package:

require(gifski)
options(gganimate.dev_args = list(width = 400, height = 600))
anim_save("pulseAnim.gif", animate(pulseAnim, renderer = gifski_renderer(), 
                                  duration = 12))
Figure 3. Animation of simulated solute transport made with the gganimate R package.
Figure 3. Animation of simulated solute transport made with the gganimate R package.

CC-BY-SA • All content by Ratey-AtUWA. My employer does not necessarily know about or endorse the content of this website.
Created with rmarkdown in RStudio using the cyborg theme from Bootswatch via the bslib package, and fontawesome v5 icons.