This tutorial aims to set out a process for animating time-series data using animation and ggplot2. It was developed for visualising animal tracks (as x/y co-ordinates at each timestamp), but presented here is a more general version, using the Legendre polynomials as a demonstration function.

Required packages are

library(ggplot2)
library(animation)

As ever with the animation package, this also requires the standalone ImageMagick software to be installed.

Now to start off, we must generate some data that represents co-ordinate time series. You can use any data for this, so this section is really just for demonstration purposes. We’ll generate 100 x values from -1 to 1.

x <- -seq(from=-1,to=1,length.out=150)
x <- rev(x)

These will be the x values for every point. We will then use the following function to generate a number of different y values for each x, according to the first 6 levels of the Legendre polynomials.

legendre <- function(x)
{
  y0 <- rep(1,length(x))
  y1 <- x
  y2 <- 0.5*(3*(x^2)-1)
  y3 <- 0.5*(5*(x^3)-3*x)
  y4 <- 0.125*(35*(x^4)-30*(x^2)+3)
  y5 <- 0.125*(63*(x^5)-70*(x^3)+15*x)
  return(data.frame(x,y0,y1,y2,y3,y4,y5))
}
curves <- legendre(x)

As you can probably see, this generates y values of increasing chaos from y0 to y5. This is what these 6 curves will actually look like:

You can imagine how these could be time-series data, or animal tracks from an experiment. Next, we need to define the parameters which will control the visualisation. “steps” is the sequence of timepoints, hence is obtained from the data. The animation will occur as a point, with a tail behind it of a certain amount of time, which is the variable “tail”

steps <- 1:150
tail <- 30

Next, we need to write our plotting function. This will take the two variables above, and generate a plot, complete with tail, for each timestamp, which will then be compressed together to form the animation

draw.plot <- function(steps,tail)
{
  for(i in steps)
  {
    #First, we select only the data at the current timestamp, to form the points
    this.points <- curves[i,]
    #Next, we compare the length of the tail to the timestamp the loop currently is. This is so that, if we are early on in the animation, it will appear to loop smoothly, by having some of the tail appear behind the point (back as far as the first timepoint), and the rest appear from the final timepoint backwards. If this is unclear, see the example at the end, which should show why we do this
    if((i-tail)<0)
    {
      #In the case that we are close to the beginning, we set the parameters ffor having the tail split in two, as discussed above
      start <- (max(steps)+i-tail):max(steps)
      end <- 1:i
      curveback <- c(start,end)
            start <- length(start)
            
      #We then take the data that will form the tails into its own dataframe
      this.tail <- curves[curveback,]

      #The graph-generating code can be modified to draw any number of points or lines quite easily. I will assume familiarity with the mechanics of ggplot2. There are 4 main graphing sections here. The first section draws the actual points
      print(ggplot(data=this.points,aes(x=x))+
  geom_point(aes(y=y0),col="black",size=4,shape=16)+
  geom_point(aes(y=y1),col="green",size=4,shape=16)+
  geom_point(aes(y=y2),col="red",size=4,shape=16)+
  geom_point(aes(y=y3),col="blue",size=4,shape=16)+
  geom_point(aes(y=y4),col="purple",size=4,shape=16)+
  geom_point(aes(y=y5),col="orange",size=4,shape=16)+
    #This next line fixes the zoom levels, such that the graph does not zoom in and out during animation
      coord_cartesian(ylim=c(-1,1),xlim=c(-1,1))+
    #This third section draws the "outer" tails, which go from as far back as specified, to the final timepoint
      geom_path(aes(y=y0),data=this.tail[1:start,],
                col="black",size=2)+
      geom_path(aes(y=y1),data=this.tail[1:start,],
                col="green",size=2)+
      geom_path(aes(y=y2),data=this.tail[1:start,],
                col="red",size=2)+
      geom_path(aes(y=y3),data=this.tail[1:start,],
                col="blue",size=2)+
      geom_path(aes(y=y4),data=this.tail[1:start,],
                col="purple",size=2)+
      geom_path(aes(y=y5),data=this.tail[1:start,],
                col="orange",size=2)+
    #And this final section draws the tails from the first timepoint to the current one
      geom_path(aes(y=y0),
                  data=this.tail[(start+1):nrow(this.tail),],
                col="black",size=2)+
      geom_path(aes(y=y1),
                data=this.tail[(start+1):nrow(this.tail),],
                col="green",size=2)+
      geom_path(aes(y=y2),
                data=this.tail[(start+1):nrow(this.tail),],
                col="red",size=2)+
      geom_path(aes(y=y3),
                data=this.tail[(start+1):nrow(this.tail),],
                col="blue",size=2)+
      geom_path(aes(y=y4),
                data=this.tail[(start+1):nrow(this.tail),],
                col="purple",size=2)+
      geom_path(aes(y=y5),
                data=this.tail[(start+1):nrow(this.tail),],
                col="orange",size=2))
    } else
    {
      #In the case that the tail does not need to wrap around, the code is much simpler. We take the tail as the points back from the current point
      this.tail <- curves[((i-tail):i),]
      
      #Then we graph it simply by drawing the points, followed by the tails.
      print(ggplot(data=this.points,aes(x=x))+
  geom_point(aes(y=y0),col="black",size=4,shape=16)+
  geom_point(aes(y=y1),col="green",size=4,shape=16)+
  geom_point(aes(y=y2),col="red",size=4,shape=16)+
  geom_point(aes(y=y3),col="blue",size=4,shape=16)+
  geom_point(aes(y=y4),col="purple",size=4,shape=16)+
  geom_point(aes(y=y5),col="orange",size=4,shape=16)+
      coord_cartesian(ylim=c(-1,1),xlim=c(-1,1))+
      geom_path(aes(y=y0),data=this.tail,col="black",size=2)+
      geom_path(aes(y=y1),data=this.tail,col="green",size=2)+
      geom_path(aes(y=y2),data=this.tail,col="red",size=2)+
      geom_path(aes(y=y3),data=this.tail,col="blue",size=2)+
      geom_path(aes(y=y4),data=this.tail,col="purple",size=2)+
      geom_path(aes(y=y5),data=this.tail,col="orange",size=2))
    }
  }
}

With our plotting function created, all we have to do now is run it through saveGIF. For more detailed explanation on how this works, and te various parameters, see my tutorial on exporting rotating graphs

saveGIF(draw.plot(steps,tail), interval = 1/30, movie.name="output.GIF",
          ani.height=640,ani.width=640)
## [1] TRUE

And there is the output! Except for that example has a pink background, due to the HTML embedding process. But as you can imagine, this can be generalised to any time series data. Even if the data does not loop on the axis (or even at all, in the case of animal tracks), the loop point is made clear by the tail. Thank you for reading!