Lecture 11

Interactive Visualization; Animated Plots

Byeong-Hak Choe

SUNY Geneseo

April 21, 2025

Interactive ggplot

plotly::ggplotly()

  • plotly is an alternative to ggplot with a relatively easy to learn syntax for generating many of the same kinds of plots.
  • plotly is mainly for the interactive figures of visualization.
    • We will focus only on plotly::ggplotly().
    • ggplotly() interacts with ggplot objects to make those figures interactive.
# install.packages("plotly")
library(plotly)
dat <- data.frame(cond = rep(c("A", "B"), each = 10),
                  xvar = 1:20 + rnorm(20, sd=3),
                  yvar = 1:20 + rnorm(20, sd=3))

p <- ggplot(dat, aes(x = xvar, y = yvar)) +
  geom_point(shape=1)      # Use hollow circles
fig <- ggplotly(p)
fig
  • Consider the static relationship between seniority and legislative effectiveness using CCES data.
cces <- read_csv(url("https://bcdanl.github.io/data/cces.csv"))
cces <- cces |> 
  mutate(party = recode(dem, `1` = "Democrat", `0` = "Republican"))
  • We can use recode() to create a new variable corresponding to other factor variable.
    • Here we summarize the number of Democrats and Republicans in a series of years.
    • There are usually 435 seats in total in the House of Representatives.
  • Variable les is a score for legislative effectiveness.
    • It measures how productive members of Congress are at making laws.
p <- ggplot(cces, aes(x = seniority, y = les,
                      color = party))+
  geom_point()+
  scale_color_manual(values=c("blue","red")) +
  labs(x = "Seniority", y = "Leg. Effectiveness")

p1 <- ggplotly(p)
p1
  • htmlwidgets::saveWidget() saves the interactive ggplot object as an *.html file.
# install.packages("htmlwidgets")
library(htmlwidgets)
saveWidget(p1, "fig.html")

ggiraph

  • ggiraph is a R package that allows us to create dynamic ggplot.
    • This allows us to add tooltips, hover effects and JavaScript actions to the graphics.
    • The package also allows the selection of graphical elements when used in shiny applications.
  • Interactivity is added to ggplot geometries, legends and theme elements, via the following aesthetics:
    • tooltip: tooltips to be displayed when mouse is over elements.
    • data_id: id to be associated with elements (necessary for hover and click actions)
    • onclick: JavaScript to be executed when elements are clicked.
  • The things we need to know to create a ggiraph interactive graphic:

    • Instead of using geom_point, use geom_point_interactive.
    • Instead of using geom_sf, use geom_sf_interactive.
    • Provide at least one of the aesthetics, tooltip, data_id and/or onclick, to create interactive elements.
    • Call function girafe with the ggplot object so that the graphic is translated as a web interactive graphics.
# install.packages("ggiraph")
library(ggiraph)
data <- mtcars
data$carname <- row.names(data)

gg_point <- ggplot(data = data) +
  geom_point_interactive(aes(x = wt, y = qsec, 
                             color = disp,
                             tooltip = carname, 
                             data_id = carname)) + 
  theme_minimal()

gg_point
p2 <- girafe(ggobj = gg_point)
p2

saveWidget(p2, "girafe_fig.html")

Animation Plot

gganimate

  • Consider the relationship between cyl and mpg from mtcars.
mtcars <- datasets::mtcars

p <- ggplot(data = mtcars,
       mapping = aes(x = factor(cyl), y = mpg)) +
  geom_boxplot()

p
  • Let us consider how the relationship between cyl and mpg vary by gear.
p + facet_wrap(~gear)
  • gganimate turn our ggplot visualizations into moving images.
    • gganimate takes a ggplot figure and creates many different versions of it by changing a single parameter in the data.
library(gganimate)
my_anim <- p + 
  transition_states(gear)

my_anim

transition_states()

  • transition_states() is intended to use primarily with categorical variables.

    • You can control the link for each transition and the amount of time spent on each of the states using the transition_length and state_length arguments.
my_anim2 <- p + 
  transition_states(gear,
    transition_length = 3,  # relative length
    state_length = 2)  # relative length

my_anim2

Cooperative Congressional Election Survey

  • Cooperative Congressional Election Survey (CCES) is a 50,000+ person national stratified sample survey.
  • Here we summarize the number of Democrats and Republicans in a series of years.
    • There are usually 435 seats in total in the House of Representatives.
cces <- read_csv(url("https://bcdanl.github.io/data/cces.csv"))
cces <- cces |> 
  mutate(party = recode(dem,`1`="Democrat",`0`="Republican")) #  to create a new variable corresponding to other factor variable.

cong_dat <- cces |> 
  group_by(year, party) |>
  summarise( seats =n())
p <- ggplot(cong_dat, 
            aes(x = year, y= seats, 
                fill = party)) +
  geom_col() +
  geom_hline(yintercept = 217) +  # threshold for having a majority of seats in the house.
  scale_fill_manual(values=c("blue","red"))

p
  • transition_time() is intended for time-series data like hours, minutes, days, weeks months, years, etc.
anim2 <- p + transition_time(year)

anim2

Example: Cooperative Congressional Election Survey

  • Let’s consider a scatter plot of seniority against all_pass.
    • Variable seniority is about how long a member has been in Congress.
    • Variable all_pass is about how many bills a member passed
p <- ggplot() + 
  geom_jitter(data = filter(cces, 
                            congress==115 & party=="Democrat"),
              aes(x = seniority, y = all_pass,
                  color = party) ) +
  geom_jitter(data = filter(cces, 
                            congress==115 & party=="Republican"),
              aes(x = seniority, y = all_pass,
                  color = party) ) +
  geom_smooth(data = filter(cces, 
                            congress==115 & party=="Democrat"),
              aes(x = seniority, y = all_pass,
                  color = party) ) +
  geom_smooth(data = filter(cces, 
                            congress==115 & party=="Republican"),
              aes(x = seniority, y = all_pass,
                  color = party) ) +
  scale_color_manual(values=c("blue","red"))

p
  • transition_layers() allows for building up a plot layer by layer with an animation.
anim2 <- p + transition_layers()

anim2

enter_*()/exit_*()

  • enter_*() and exit_*() allow for controlling the entering and exiting in gganimate.

    • *_fade() will set the alpha value to zero making the elements fade in/out during the transition.
anim <- ggplot(mtcars, aes(factor(cyl), mpg)) +
  geom_boxplot() +
  transition_states(factor(cyl))

# Fade-in, fade-out
anim1 <- anim +
  enter_fade() +
  exit_fade()

anim1

shadow_*()

  • shadow_*() allows for retaining previous or preview future frames of the animation.

  • shadow_wake() shows preceding frames with gradual falloff.

    • alpha is for transparency modification of the wake.
    • wrap should the shadow wrap around, so that the first frame will get shadows from the end of the animation.
  • There are also shadow_mark(), shadow_null(), and shadow_trail().

p <- ggplot(cong_dat,
                aes(x = year, y = seats, fill = party)) +
  geom_bar(stat = "identity") +
  geom_hline(yintercept = 217) +
  scale_fill_manual(values = c("blue","red"))
anim3 <- p + transition_time(year) +
  shadow_wake(wake_length = 1,
              alpha = TRUE,
              wrap = TRUE)

anim3

Example: Gapminder

library(gapminder)
p <- gapminder |>
  ggplot() + 
    geom_point(aes(x = gdpPercap, y = lifeExp, 
                   color = continent, size = pop), 
               alpha = 0.75) + 
  theme_minimal() + theme(legend.position = "top") + 
  guides(size = "none") + 
  labs(x = "GDP per Capita", y = "Life Expetancy", color = "") 

Here is with transition_time().

library(gganimate)
p +
  transition_time(year)
  • labs(title = "TIME_VARIABLE: {frame_time}")
    • We can indicate the number of frame on the transition using labs()
p +
  transition_time(year) +
  labs(title = "Year: {frame_time}")

Example: Gapminder

  • It is much more visually fascinating to include the data on the same graph with an extra ggplot layer.
p +
  geom_text(aes(x = min(gdpPercap), 
                y = min(lifeExp), 
                label = as.factor(year)) , 
            hjust=-2, vjust = -0.2, alpha = 0.2,  
            color = "gray", size = 20) +
  transition_states(as.factor(year), state_length = 0)
  • state_length allows us to control for how long will pause before changing to the new state.
  • transition_reveal(year) is adding each year of the data on top of ‘old’ data.
gapminder |>
  filter(country == "United States") |>
  ggplot(aes(x = year, y = pop)) + 
  geom_point() + geom_line() +
  theme_minimal() +
  transition_reveal(year)

view_follow()

  • If we want to better see how the variables grow, it is better to adjust the scale in each frame.
    • For this, we can use the view_follow() function.
gapminder |>
  filter(country == "United States") |>
  ggplot(aes(x = year, y = pop)) + 
  geom_point() + geom_line() + 
  geom_text(aes(x = min(year), y = min(pop), 
                label = as.factor(year)) , 
            hjust=-2, vjust = -0.2, alpha = 0.5,  
            color = "gray", size = 20) +
  theme_minimal() +
  transition_reveal(year) + 
  view_follow()

Bar Chart Race

  • Frames and duration are the key for a good quality of animation.

  • We can adjust several key elements of our animations, such as:

    • The width and height of the animation to create an animation.
    • Duration and number of frames per second (fps): this will make us the animation see fluently.
      • The fps parameter is recommended to be higher than 12.
    • Output file format: if we don’t want to create a gif, you can also create a video too (e.g., mp4).
  • Calculate the ranking of gdpPercap for each year:
gapminder_sum <- gapminder |>
  group_by(year) |>
  arrange(year, desc(gdpPercap)) |>
  mutate(ranking = row_number()) |>
  filter(ranking <=15)
p <- gapminder_sum |>
  ggplot() +
  geom_col(aes(x = ranking, y = gdpPercap, fill = country)) +
  geom_text(aes(x = ranking, y = gdpPercap, label = round(gdpPercap, 0)), 
            hjust=-0.1) +
  geom_text(aes(x = ranking, y = 0 , 
                label = country), hjust=1.1) + 
  geom_text(aes(x = 15, 
                y = max(gdpPercap), 
                label = as.factor(year)), 
            vjust = 0.2, alpha = 0.5,  color = "gray", size = 20) +
  coord_flip(clip = "off", expand = FALSE) + 
  scale_x_reverse() +
  theme_minimal() + 
  theme(
    panel.grid = element_blank(), 
    legend.position = "none",
    axis.ticks.y = element_blank(),
    axis.title.y = element_blank(),
    axis.text.y = element_blank(),
    plot.margin = margin(1, 4, 1, 3, "cm")
  )
anim <- p +
  transition_states(year, state_length = 0, transition_length = 2) +
  enter_fade() +
  exit_fade() + 
  ease_aes('quadratic-in-out') 
p_anim <- animate(anim, 
                  width = 700, height = 432, 
                  fps = 25, duration = 15, 
                  rewind = FALSE)
p_anim
  • rewind: Controls what happens at the end of the sequence.
    • FALSE: Jumps back to the first frame and repeats from the beginning
    • TRUE: After reaching the last frame it’ll play the animation in reverse back to the start
  • We can use anim_save() to save animation as gif file or video files.
anim_save("first_saved_animation.gif", animation = p_anim)