Putting the top 100 R packages into a GIF
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
You can say what you want about Twitter, but the way animated GIFs are presented on that platform is pretty nice. It’s not so surprising that they play and loop, as one would expect them to do, but the nice thing is that if you click them, they pause. This tiny change in GIF behavior has resulted in a small cottage industry of GIF games (like
here or
here) and click-the-GIF-and-see-what-you-get animations (like
Mario roulette). Here I’ll go through how I made one of the latter in R with
gganimate
showing the top 100 downloaded R packages. But first the actual GIF! Click to pause it and learn more about a popular R-package:
The recipe to make an animated GIF like this is fairly straightforward:
- Compile a data frame with one row per frame. That is, each row needs to include all the info that will go into a single image in the resulting GIF.
- Build a
ggplot
that, given a single row, produces a single frame. - Through the magic of
gganimate
and thetransition_states
function, turn this ggplot into an animation. - Render the animation to a GIF using the
animate
function.
Below, I’ll go through the whole code needed for the “Top 100 R packages”-animation, but first some other animations I’ve done using the same recipe. All can be paused by clicking/touching them.
Code for getting 100 R packages into a GIF
Except for the usual suspects, we’ll need some packages supporting
gganimate
and we’ll need
pkgsearch
to pull R package statistics and info.
library(tidyverse) # For ggplot2 etc. library(glue) # Easy string manipulation library(lubridate) # Easy date manipulation library(pkgsearch) # To pull information around R packages library(gganimate) # Add animation-powers to ggplots library(ragg) # Graphics devices that enables emojis in ggplots library(gifski) # Allows exporting gganimate plots to animated GIFs
Here getting the top 100 most downloaded packages from the Rstudio CRAN mirror from last week and some extra metadata on each package.
last_week = floor_date(today() - 7, "week", week_start = 1) cran_top_100_count <- cran_top_downloaded() cran_metadata <- cran_packages(cran_top_100_count$package) |> select("Package", "Title", "Version", "Author", "Maintainer", "Description") |> rename_all(tolower)
Now creating a data frame with everything we need to make the animation where each row will become a frame in the animated plot.
cran_top_100_animation_info <- cran_top_100_count |> left_join(cran_metadata, by = "package") |> mutate( rank = row_number(), # The y-coordinate of a 🥳 emoji that's in picture on the No 1 package, # but otherwise "off-camera". emoji_y = ifelse(rank == 1, 0.72, 2), # The y-coordinate of the little dot that scrolls along as we go from 1 to 100 dot_x = 0.1 + (rank - 1) / (max(rank) - 1) * 0.8, maintainer_info = glue( "Maintainer: { str_extract(cran_metadata$maintainer, '^[^<]+(?= )') }"), title_info = glue("{ifelse(str_starts(title, '\\\\d'), '.', '')}{title}") |> str_replace_all("\n", " ") |> str_wrap(width = 35) ) cran_top_100_animation_info ## # A data frame: 100 × 12 ## package count title version author maint…¹ descr…² rank emoji_y dot_x ## <chr> <chr> <chr> <chr> <chr> <chr> <chr> <int> <dbl> <dbl> ## 1 rlang 592905 "Funct… 1.0.6 "Lion… Lionel… "A too… 1 0.72 0.1 ## 2 ggplot2 580724 "Creat… 3.4.1 "Hadl… Thomas… "A sys… 2 2 0.108 ## 3 cli 577931 "Helpe… 3.6.0 "Gábo… Gábor … "A sui… 3 2 0.116 ## 4 vctrs 550428 "Vecto… 0.5.2 "Hadl… Lionel… "Defin… 4 2 0.124 ## 5 lifecycle 511805 "Manag… 1.0.3 "Lion… Lionel… "Manag… 5 2 0.132 ## 6 dplyr 426694 "A Gra… 1.1.0 "Hadl… Hadley… "A fas… 6 2 0.140 ## 7 ragg 391427 "Graph… 1.2.5 "Thom… Thomas… "Anti-… 7 2 0.148 ## 8 textshaping 380763 "Bindi… 0.3.6 "Thom… Thomas… "Provi… 8 2 0.157 ## 9 tidyselect 325641 "Selec… 1.2.0 "Lion… Lionel… "A bac… 9 2 0.165 ## 10 devtools 308780 "Tools… 2.4.5 "Hadl… Jennif… "Colle… 10 2 0.173 ## # … with 90 more rows, 2 more variables: maintainer_info <glue>, ## # title_info <chr>, and abbreviated variable names ¹maintainer, ²description
Next up: Creating the ggplot
. This is fairly tedious as all the different elements needs to be separately placed and styled. Then magic comes at the end with transition_states
which turns this into an animation.
bg_color <- '#211353' text_color1 <- "#FFFFFF" text_color2 <- "#BAC2E6" main_font <- "Helvetica Neue" monospace_font <- "Monaco" rank_plot_theme <- theme_void(base_family = main_font) + theme( legend.position = 'bottom', legend.background = element_rect(fill = bg_color, colour = bg_color), panel.background = element_rect(fill = bg_color, color = bg_color), plot.background = element_rect(fill = bg_color, color = bg_color), plot.title = element_text( face = 'bold', colour = text_color1, size = 7, hjust = 0.5 ), plot.subtitle = element_text(colour = text_color2, size = 6, hjust = 0.5), plot.margin = unit(c(0, 0.2,0.1,0.2), "cm"), plot.caption = element_text(colour = text_color2, hjust = 0, size = 5) ) cran_top_100_animation <- ggplot(cran_top_100_animation_info) + labs( title = "Top 100 most downloaded R packages", subtitle = ">> Click to pause. Get no. 1! <<", caption = glue(.sep = "\n", "Stats from the RStudio CRAN mirror week {last_week}", "Made by @rabaath" ) ) + geom_point(aes(dot_x), y = 0.95, color = text_color1) + geom_text( aes(label = rank), x = 0.5, y = 0.80, size = 6, color = text_color1, family = main_font ) + geom_text( aes(label = package), x = 0.5, y = 0.60, size = 5, color = text_color1, family = monospace_font ) + geom_text(aes(label = "🥳", y = emoji_y), size = 8, x = 0.95) + geom_text(aes(label = "🥳", y = emoji_y), size = 8, x = 0.05) + geom_text( aes(label = title_info), x = 0, y = 0.40, size = 2.5, color = text_color1, family = main_font, fontface = "italic", hjust = 0, vjust=1, lineheight=1 ) + geom_text( aes(label = maintainer_info), x = 0, y = 0.08, size = 2.5, color = text_color1, family = main_font, hjust = 0 ) + scale_y_continuous(limits = 0:1) + scale_x_continuous(limits = 0:1) + rank_plot_theme + transition_states( rank, transition_length = 0, state_length = 1, wrap=FALSE )
Finally rendering the animation as a GIF. Let’s make two versions: One that’s slower and possible to read as it is and one that’s super fast and made to be a “click to pause” GIF on Twitter.
animate_top_100_gif <- function(fps) { animate( cran_top_100_animation, nframes = nrow(cran_top_100_animation), fps = fps, start_pause = 0, end_pause = 0, width = 600, height = 600, res=300, device = "ragg_png", renderer = gifski_renderer() ) anim_save(glue("top_100_rpackages_{fps}_fps_{last_week}.gif")) } # A slower animation, where it's possible to read and contemplate. animate_top_100_gif(fps = 0.75) # A quick animation where it's impossible to read, but one can play the # "click to pause"-game. animate_top_100_gif(fps = 12)
But nothing happens if I click the GIFs above!?
Turns out, you cannot actually pause an animated GIF. So how does Twitter do that? By sneakily converting your GIF to a video which is something that can be paused. If you want the same effect on your own website you’ll have to emulate Twitter and
- Convert your GIF to a video. For example, using
ffmpeg
on the command line or the ever-dependable online tool EzGIF. - Wrap your video inside a
<video>
-tag with the following properties:
<video width="400" autoplay loop muted playsinline disablepictureinpicture onclick="this.paused ? this.play() : this.pause();"> <source src="link/to/my-animated-gif.mp4" /> </video>
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.