Permalink
Browse files

Initial draft of gganimate R package

  • Loading branch information...
dgrtwo committed Feb 1, 2016
0 parents commit 81e5e95b33e90b0222314dd2ac187a749596dab0
@@ -0,0 +1,4 @@
^.*\.Rproj$
^\.Rproj\.user$
^README\.Rmd$
^README/README-
@@ -0,0 +1,7 @@
.Rproj.user
.Rhistory
.RData
inst/doc
README/README-cache*
README/README-fig*png

@@ -0,0 +1,25 @@
Package: gganimate
Type: Package
Title: Create easy animations with ggplot2
Version: 0.1
Date: 2016-02-01
Author: c(person("David", "Robinson", email = "drobinson@stackoverflow.com",
role = c("aut", "cre")))
Maintainer: David Robinson <drobinson@stackoverflow.com>
Description: Create animations with ggplot2 by treating the "frame" as an
aesthetic similar to x, y, or color. Compatible with presenting animations
in knitr or saving to a video or animated image file.
License: GPL-2
LazyData: TRUE
VignetteBuilder: knitr
Suggests:
knitr,
rmarkdown,
gapminder
RoxygenNote: 5.0.1
Imports: plyr,
ggplot2,
grid,
animation
URL: http://github.com/dgrtwo/gganimate
BugReports: http://github.com/dgrtwo/gganimate/issues
@@ -0,0 +1,4 @@
# Generated by roxygen2: do not edit by hand

export(gg_animate)
import(ggplot2)
@@ -0,0 +1,102 @@
#' Show an animation of a ggplot2 object
#'
#' Show an animation of a ggplot2 object that contains a \code{frame} aesthetic. This
#' \code{frame} aesthetic will determine which frame the animation is shown in. For
#' example, you could add the aesthetic \code{frame = time} to a dataset including
#' a \code{time} variable.
#'
#' @param p A ggplot2 object. If no plot is provided, use the last plot by default.
#' @param filename Output file. If not given, simply prints to the screen (typical for animated
#' knitr chunks)
#' @param saver Can specify a function (or a string such as "mp4" or "html" that specifies
#' a function to use for saving
#' @param pause Amount of time to pause between displaying each plot. Only used when
#' displaying to the screen, not saving to a file (and not useful when creating an
#' animation in a knitr chunk). When saving to a file, use
#' \code{ani.options(interval = ...)}
#' @param title_frame Whether to title each image with the current \code{frame} value.
#' @param ... If saving to a file, extra arguments to pass along to the animation
#' saving function (to \code{saveVideo}/\code{saveGIF}/etc)
#'
#' @import ggplot2
#'
#' @export
gg_animate <- function(p = last_plot(), filename = NULL, saver = NULL,
pause = NULL, title_frame = TRUE, ...) {
if (is.null(p)) {
stop("no plot to save")
}

# add group mappings

built <- ggplot_build(p)

# get frames
frames <- sort(unique(unlist(plyr::compact(lapply(built$data, function(d) d$frame)))))
if (length(frames) == 0) {
stop("No frame aesthetic found; cannot create animation")
}

plots <- lapply(frames, function(f) {
# replace each data object with a subset
b <- built
for (i in seq_along(b$data)) {
if (!is.null(b$data[[i]]$frame)) {
sub <- b$data[[i]]$frame == f
b$data[[i]] <- b$data[[i]][sub, ]
}
}

# title plot according to frame
if (title_frame) {
if (!is.null(b$plot$labels$title)) {
b$plot$labels$title <- paste(b$plot$labels$title, f)
} else {
b$plot$labels$title <- f
}
}

b
})

if (!is.null(filename)) {
saver_func <- animation_saver(saver, filename)

saver_func(for (pl in plots) {
plot_ggplot_build(pl)
}, filename, ...)
} else {
for (pl in plots) {
plot_ggplot_build(pl)
if (!is.null(pause)) {
Sys.sleep(pause)
}
}
}
}


#' Retrieve a function for saving animations based on a string/function and a filename
#'
#' @param saver A function or string describing an animation saver
#' @param filename File name to save to
animation_saver <- function(saver, filename) {
if (is.function(saver)) {
return(saver)
}
if (is.null(saver)) {
saver <- tolower(tools::file_ext(filename))
}
savers <- list(gif = animation::saveGIF,
mp4 = animation::saveVideo,
webm = animation::saveVideo,
avi = animation::saveVideo,
html = function(expr, filename, ...) animation::saveHTML(expr, htmlfile = filename, ...),
swf = animation::saveSWF)

if(is.null(savers[[saver]])) {
stop("Don't know how to save animation of type ", saver)
}

savers[[saver]]
}
@@ -0,0 +1,29 @@
#' Plot a built ggplot object
#'
#' We needed a customized version of ggplot2's \code{print.ggplot2},
#' because we need to build plots from the intermediate results of
#' \code{\link{ggplot_build}} rather than from a \code{gg} object.
#'
#' @param b A list resulting from \code{\link{ggplot_build}}
#' @param newpage draw new (empty) page first?
#' @param vp viewport to draw plot in
plot_ggplot_build <- function(b, newpage = is.null(vp), vp = NULL) {
if (newpage) {
grid::grid.newpage()
}

grDevices::recordGraphics(
requireNamespace("ggplot2", quietly = TRUE),
list(),
getNamespace("ggplot2")
)

gtable <- ggplot_gtable(b)
if (is.null(vp)) {
grid::grid.draw(gtable)
} else {
if (is.character(vp)) grid::seekViewport(vp) else grid::pushViewport(vp)
grid::grid.draw(gtable)
grid::upViewport()
}
}
@@ -0,0 +1,131 @@
---
output:
md_document:
variant: markdown_github
---

## gganimate: Create easy animations with ggplot2

<!-- README.md is generated from README.Rmd. Please edit that file -->

```{r, echo = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "README/README-fig-",
cache.path = "README/README-cache-"
)
```


```{r echo = FALSE}
library(knitr)
# I want the README to have visible GIFs on GitHub, as
# GitHub cannot show .mp4s or other animation formats.
# I therefore hacked together a GIF animation hook for knitr.
library(animation)
ani.options(autobrowse = FALSE)
opts_knit$set(animation.fun = function(x, options, format = "gif") {
x = c(knitr:::sans_ext(x), knitr:::file_ext(x))
fig.num = options$fig.num
format = sub("^[.]", "", format)
fig.fname = paste0(sub(paste0(fig.num, "$"), "*", x[1]),
".", x[2])
mov.fname = paste0(sub(paste0(fig.num, "$"), "", x[1]), ".",
format)
animation::im.convert(fig.fname, output = mov.fname)
sprintf("![%s](%s)", options$label, paste0(opts_knit$get("base.url"), mov.fname))
})
opts_chunk$set(cache = TRUE, message = FALSE, warning = FALSE, fig.show = "animate")
```

**gganimate** wraps the [animation package](http://www.inside-r.org/packages/cran/animation/docs/animation) to create animated ggplot2 plots.

The core of the approach is to treat "frame" (as in, the time point within an animation) as another aesthetic, just like **x**, **y**, **size**, **color**, or so on. Thus, a variable in your data can be mapped to frame just as others are mapped to x or y.

For example, suppose we wanted to create an animation similar to the [Gapminder world](http://www.gapminder.org/world) animation.

```{r pkgs, cache = FALSE}
library(gapminder)
library(ggplot2)
theme_set(theme_bw())
```

```{r setup}
p <- ggplot(gapminder, aes(gdpPercap, lifeExp, size = pop, color = continent, frame = year)) +
geom_point() +
scale_x_log10()
```

Once we have created that plot (notice it was saved as `p`), we display it as an animation with the `gg_animate` function:

```{r dependson = "setup"}
library(gganimate)
gg_animate(p)
```

This displays each of the frames of the plot in sequence (note that if you're running it interactively it will be too fast to see all of them). When combined with knitr's `fig.show = "animate"`, we create an animation as seen above.

You can also save the animation to a file, such as an GIF, video, or an animated webpage:

```{r eval = FALSE}
gg_animate(p, "output.gif")
gg_animate(p, "output.mp4")
gg_animate(p, "output.swf")
gg_animate(p, "output.html")
```

(Each of these requires ffmpeg, ImageMagick, or other such drivers to be installed on your computer: see the [animation package](http://www.inside-r.org/packages/cran/animation/docs/animation) documentation for more).

Notice that the axis limits and legend stay fixed between animation frames, even though the points move. This is one of the advantages of the **gganimate** package as opposed to creating animations manually: the plot (and its axes and legend) is built only once but rendered for each frame.

### Customization

You can have some layers of your plot animated and others not, simply by adding the `frame` aesthetic to those layers and not others. This is useful, for example, if you want to *highlight* particular points in an animation rather than move them.

```{r}
p <- ggplot(gapminder, aes(gdpPercap, lifeExp, size = pop)) +
geom_point() +
geom_point(aes(frame = year), color = "red") +
scale_x_log10()
gg_animate(p)
```

Note that while animating over time is intuitive, you could match any variable in your data to the `frame` aesthetic. We could animate across continents instead:

```{r}
p <- ggplot(gapminder, aes(gdpPercap, lifeExp, size = pop, frame = continent)) +
geom_point() +
scale_x_log10()
gg_animate(p)
```

Finally, note that if there is a stat summarization (such as a `geom_smooth`) that you want to animate, you should also add a `group` aesthetic to that layer. Otherwise, the layer will be calculated once across all frames (and therefore be constant in the animation):

```{r}
p <- ggplot(gapminder, aes(gdpPercap, lifeExp, size = pop, frame = year)) +
geom_point() +
geom_smooth(aes(group = year), method = "lm", show.legend = FALSE) +
facet_wrap(~ continent, scales = "free") +
scale_x_log10()
gg_animate(p)
```

Note also that you can control your animation the same way you would in the animation package, using the [ani.options](http://www.inside-r.org/packages/cran/animation/docs/ani.options) function:

```{r eval = FALSE}
library(animation)
ani.options(interval = .3)
gg_animate(p, "output.gif")
```

Oops, something went wrong.

0 comments on commit 81e5e95

Please sign in to comment.