Hot questions for Using Ggplot2 in gganimate

Question:

How would you go at reproducing this chart from Jaime Albella in R ?

See the animation on visualcapitalist.com or on twitter (giving several references in case one breaks).

I'm tagging this as ggplot2 and gganimate but anything that can be produced from R is relevant.

data (thanks to https://github.com/datasets/gdp )

gdp <- read.csv("https://raw.github.com/datasets/gdp/master/data/gdp.csv")
# remove irrelevant aggregated values
words <- scan(
  text="world income only total dividend asia euro america africa oecd",
  what= character())
pattern <- paste0("(",words,")",collapse="|")
gdp  <- subset(gdp, !grepl(pattern, Country.Name , ignore.case = TRUE))

Edit:

Another cool example from John Murdoch :

Most populous cities from 1500 to 2018


Answer:

Edit: added spline interpolation for smoother transitions, without making rank changes happen too fast. Code at bottom.


I've adapted an answer of mine to a related question. I like to use geom_tile for animated bars, since it allows you to slide positions.

I worked on this prior to your addition of data, but as it happens, the gapminder data I used is closely related.

library(tidyverse)
library(gganimate)
library(gapminder)
theme_set(theme_classic())

gap <- gapminder %>%
  filter(continent == "Asia") %>%
  group_by(year) %>%
  # The * 1 makes it possible to have non-integer ranks while sliding
  mutate(rank = min_rank(-gdpPercap) * 1) %>%
  ungroup()

p <- ggplot(gap, aes(rank, group = country, 
                     fill = as.factor(country), color = as.factor(country))) +
  geom_tile(aes(y = gdpPercap/2,
                height = gdpPercap,
                width = 0.9), alpha = 0.8, color = NA) +

  # text in x-axis (requires clip = "off" in coord_*)
  # paste(country, " ")  is a hack to make pretty spacing, since hjust > 1 
  #   leads to weird artifacts in text spacing.
  geom_text(aes(y = 0, label = paste(country, " ")), vjust = 0.2, hjust = 1) +

  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +

  labs(title='{closest_state}', x = "", y = "GFP per capita") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(),  # These relate to the axes post-flip
        axis.text.y  = element_blank(),  # These relate to the axes post-flip
        plot.margin = margin(1,1,1,4, "cm")) +

  transition_states(year, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

animate(p, fps = 25, duration = 20, width = 800, height = 600)

For the smoother version at the top, we can add a step to interpolate the data further before the plotting step. It can be useful to interpolate twice, once at rough granularity to determine the ranking, and another time for finer detail. If the ranking is calculated too finely, the bars will swap position too quickly.

gap_smoother <- gapminder %>%
  filter(continent == "Asia") %>%
  group_by(country) %>%
  # Do somewhat rough interpolation for ranking
  # (Otherwise the ranking shifts unpleasantly fast.)
  complete(year = full_seq(year, 1)) %>%
  mutate(gdpPercap = spline(x = year, y = gdpPercap, xout = year)$y) %>%
  group_by(year) %>%
  mutate(rank = min_rank(-gdpPercap) * 1) %>%
  ungroup() %>%

  # Then interpolate further to quarter years for fast number ticking.
  # Interpolate the ranks calculated earlier.
  group_by(country) %>%
  complete(year = full_seq(year, .5)) %>%
  mutate(gdpPercap = spline(x = year, y = gdpPercap, xout = year)$y) %>%
  # "approx" below for linear interpolation. "spline" has a bouncy effect.
  mutate(rank =      approx(x = year, y = rank,      xout = year)$y) %>%
  ungroup()  %>% 
  arrange(country,year)

Then the plot uses a few modified lines, otherwise the same:

p <- ggplot(gap_smoother, ...
  # This line for the numbers that tick up
  geom_text(aes(y = gdpPercap,
                label = scales::comma(gdpPercap)), hjust = 0, nudge_y = 300 ) +
  ...
  labs(title='{closest_state %>% as.numeric %>% floor}', 
   x = "", y = "GFP per capita") +
...
transition_states(year, transition_length = 1, state_length = 0) +
enter_grow() +
exit_shrink() +
ease_aes('linear')

animate(p, fps = 20, duration = 5, width = 400, height = 600, end_pause = 10)

Question:

Goal

I would like to zoom in on the GDP of Europe throughout the years. The phantastic ggforce::facet_zoom allows this for static plots (i.e., for one specific year) very easily.

Moving scales, however, prove harder than expected. gganimate seems to take the x-axis limits from the first frame (year == 1952) and continute until the end of the animation. This related, but code-wise outdated question did not yield an answer, unfortunately. Neither + coord_cartesian(xlim = c(from, to)), nor facet_zoom(xlim = c(from, to)) seems to be able to influence the facet_zoom window beyond static limits.

  • Is there any way to make gganimate 'recalculate' the facet_zoom scales for every frame?
Ideal result
First frame

Last frame

Current code
library(gapminder)
library(ggplot2)
library(gganimate)
library(ggforce)
p <- ggplot(gapminder, aes(gdpPercap, lifeExp, size = pop, color = continent)) +
    geom_point() + scale_x_log10() +
    facet_zoom(x = continent == "Europe") +
    labs(title = "{frame_time}") +
    transition_time(year) 

animate(p, nframes = 30)

Answer:

I don't think it's possible quite yet with the current dev version of gganimate as of Dec 2018; there seem to be some bugs which prevent facet_zoom from playing nice with gganimate. Fortunately, I don't think a workaround is too painful.

First, we can tween to fill in the intermediate years:

# Here I tween by fractional years for more smooth movement
years_all <- seq(min(gapminder$year), 
                 max(gapminder$year), 
                 by = 0.5)

gapminder_tweened <- gapminder %>%
  tweenr::tween_components(time = year, 
                           id   = country, 
                           ease = "linear", 
                           nframes = length(years_all))

Then, adopting your code into a function that takes a year as input:

render_frame <- function(yr) {
  p <- gapminder_tweened %>%
    filter(year == yr) %>%
    ggplot(aes(gdpPercap, lifeExp, size = pop, color = continent)) +
    geom_point() +
    scale_x_log10(labels = scales::dollar_format(largest_with_cents = 0)) +
    scale_size_area(breaks = 1E7*10^0:3, labels = scales::comma) +
    facet_zoom(x = continent == "Europe") +
    labs(title = round(yr + 0.01) %>% as.integer) 
    # + 0.01 above is a hack to override R's default "0.5 rounds to the
    #   closest even" behavior, which in this case gives more frames
    #   (5 vs. 3) to the even years than the odd years
  print(p) 
}  

Finally, we can save an animation by looping through through the years (which in this case include fractional years):

library(animation)
oopt = ani.options(interval = 1/10)
saveGIF({for (i in 1:length(years_all)) {
  render_frame(years_all[i])
  print(paste0(i, " out of ",length(years_all)))
  ani.pause()}
},movie.name="facet_zoom.gif",ani.width = 400, ani.height = 300) 

or, alternatively, using gifski for a smaller file <2MB:

gifski::save_gif({ for (i in 1:length(years_all) {
  render_frame(years_all[i])
  print(paste0(i, " out of ",length(years_all)))
}
},gif_file ="facet_zoom.gif", width = 400, height = 300, delay = 1/10, progress = TRUE) 

(When I have more time, I'll try to remove the distracting changes in the legends by using manually specified breaks.)

Question:

Skimming through the options of gganimate that can be set when gg_animate() is called to render the animated sequence, it seems that there is no option to change the frame title so to make it clearer to the observer of what is the parameter that the frame is based on.

In other words, suppose that frame = year in a layer: how do I make the frame's title be year: #### where #### is the year of the current frame? Am I missing something or is it a limitation of the gganimate library?

How would you achieve the same result by a workaround? Thanks for your advice.


Answer:

Update for new gganimate API

gganimate has been redesigned with a new API. The frame title can now be animated with the code below. state_length and transition_length set the relative amount of time spent in a given "state" (meaning a given value of cyl here) and transitioning between states:

p = ggplot(mtcars, aes(wt, mpg)) + 
  geom_point() + 
  transition_states(cyl, transition_length=1, state_length=30) +
  labs(title = 'Cylinders: {closest_state}')

animate(p, nframes=40)

gganimate can be installed from github by running devtools::install_github('thomasp85/gganimate')

Original Answer

The frame's subset value is appended to any pre-existing title. You can therefore add a title with explanatory text. For example:

library(gganimate)

p = ggplot(mtcars, aes(wt, mpg, frame=cyl)) + geom_point() + 
    ggtitle("Cylinders: ")

gg_animate(p)

As you can see in the GIF below, the prefix "Cylinders: " is now added to the title before the value of cyl:

Question:

I'm using gganimate to create some .gif files that I want to insert into my reports. I'm able to save the files and view them fine, however, I find that the displayed size is small: 480x480. Is there a way to adjust that - perhaps along the lines of height and width arguments in ggsave()?

I can zoom in but that impacts the quality poorly and makes it rather unreadable for my use case.

Here's some sample code:

gplot<- ggplot(gapminder, aes(x = gdpPercap, y = lifeExp, colour = continent, 
               size = pop, frame = year)) +
        geom_point(alpha = 0.6) + scale_x_log10()

gganimate(gplot, "test.gif")

Below is the output for this code.


Answer:

There can be issues with using the magick package.

I think a better solution is use the animate() function in gganimate to create an object which is then passed to the anim_save() function. No need to use another package.

library(gganimate)
library(gapminder)

my.animation <- 
  ggplot(
  gapminder,
  aes(x = gdpPercap, y = lifeExp, colour = continent, size = pop)
 ) +
geom_point(alpha = 0.6) +
scale_x_log10() +
transition_time(year)

# animate in a two step process:
animate(my.animation, height = 800, width =800)
anim_save("Gapminder_example.gif")

Question:

I want to slow the transition speed between states when using library(gganimate).

Here is a mini example:

# devtools::install_github("thomasp85/gganimate")
library(gganimate) # v0.9.9.9999

dat_sim <- function(t_state, d_state) {
  data.frame(
    x = runif(1000, 0, 1),
    y = runif(1000, 0, 1),
    t_state = t_state*d_state
    )
}

dat <- purrr::map_df(1:100, ~ dat_sim(., 1))

ggplot(dat, aes(x, y)) +
  geom_hex(bins = 5) +
  theme_void() +
  lims(x = c(.3, .7),
       y = c(.3, .7)) +
  theme(legend.position = "none") +
  transition_time(t_state)

My ideal behavior would be much slower (10-100x), so color changes gradually evolve and nobody has a seizure.

If I try to use transition_states() for more manual control, I get a gif with mostly blank frames. I've tried various combinations for transition_legnth= and state_length= without a noticeable effect.

ggplot(dat, aes(x, y)) +
  geom_hex(bins = 5) +
  theme_void() +
  lims(x = c(.3, .7),
       y = c(.3, .7)) +
  theme(legend.position = "none") +
  transition_states(t_state, transition_length = .1, state_length = 2)


Answer:

I found in docs animate function which can take fps and detail parameters.

@param fps The frame rate of the animation in frames/sec

@param detail The number of additional frames to calculate, per frame

The result:

p <- ggplot(dat, aes(x, y)) +
      geom_hex(bins = 5) +
      theme_void() +
      lims(x = c(.3, .7),
           y = c(.3, .7)) +
      theme(legend.position = "none") +
      transition_time(t_state)
animate(p, fps=1)

Also there you can specify output format such as png, jpeg, svg.

Question:

The package gganimate creates gifs (MWE code from here):

    library(ggplot2)
    #devtools::install_github('thomasp85/gganimate')
    library(gganimate)

    p <- ggplot(mtcars, aes(factor(cyl), mpg)) + 
            geom_boxplot() + 
            # Here comes the gganimate code
            transition_states(
                    gear,
                    transition_length = 2,
                    state_length = 1
            ) +
            enter_fade() + 
            exit_shrink() +
            ease_aes('sine-in-out')

How can export this gif now? In the previous (now archived) version of gganimate this was simple:

    gganimate(p, "output.gif")

However, I could not find an equivalent function in the current gganimate package.


Note: This question seems like an exact duplicated of the question from which I took the code for the MWE. However, gganimate has been updated and in the new version, displaying an animation in the viewer pane vs. exporting it seem to be different issues.


Answer:

You can do like this:

anim <- animate(p)
magick::image_write(anim, path="myanimation.gif")

Question:

I have a time-series of data, where I'm plotting diagnosis rates for a disease on the y-axis DIAG_RATE_65_PLUS, and geographical groups for comparison on the x-axis NAME as a simple bar graph. My time variable is ACH_DATEyearmon, which the animation is cycling through as seen in the title.

df %>% ggplot(aes(reorder(NAME, DIAG_RATE_65_PLUS), DIAG_RATE_65_PLUS)) +
  geom_bar(stat = "identity", alpha = 0.66) +
  labs(title='{closest_state}') +
  theme(plot.title = element_text(hjust = 1, size = 22),
        axis.text.x=element_blank()) +
  transition_states(ACH_DATEyearmon, transition_length = 1, state_length = 1) +
  ease_aes('linear')

I've reordered NAME so it gets ranked by DIAG_RATE_65_PLUS.

What gganimate produces:

I now have two questions:

1) How exactly does gganimate reorder the data? There is some overall general reordering, but each month has no frame where the groups are perfectly ordered by DIAG_RATE_65_PLUS from smallest to biggest. Ideally, I would like the final month "Aug 2018" to be ordered perfectly. All of the previous months can have their x-axis based on the ordered NAME for "Aug 2018`.

2) Is there an option in gganimate where the groups "shift" to their correct rank for each month in the bar chart?

Plots for my comment queries:

https://i.stack.imgur.com/s2UPw.gif https://i.stack.imgur.com/Z1wfd.gif

@JonSpring

    df %>%
  ggplot(aes(ordering, group = NAME)) +
  geom_tile(aes(y = DIAG_RATE_65_PLUS/2, 
                height = DIAG_RATE_65_PLUS,
                width = 0.9), alpha = 0.9, fill = "gray60") +
  geom_hline(yintercept = (2/3)*25, linetype="dotdash") +
  # text in x-axis (requires clip = "off" in coord_cartesian)
  geom_text(aes(y = 0, label = NAME), hjust = 2) + ## trying different hjust values
  theme(plot.title = element_text(hjust = 1, size = 22),
        axis.ticks.y = element_blank(), ## axis.ticks.y shows the ticks on the flipped x-axis (the now metric), and hides the ticks from the geog layer
        axis.text.y = element_blank()) + ## axis.text.y shows the scale on the flipped x-axis (the now metric), and hides the placeholder "ordered" numbers from the geog layer
  coord_cartesian(clip = "off", expand = FALSE) +
  coord_flip() +
  labs(title='{closest_state}', x = "") +
  transition_states(ACH_DATEyearmon, 
                    transition_length = 2, state_length = 1) +
  ease_aes('cubic-in-out')

With hjust=2, labels are not aligned and move around.

Changing the above code with hjust=1

@eipi10

df %>% 
  ggplot(aes(y=NAME, x=DIAG_RATE_65_PLUS)) +
  geom_barh(stat = "identity", alpha = 0.66) +
  geom_hline(yintercept=(2/3)*25, linetype = "dotdash") + #geom_vline(xintercept=(2/3)*25) is incompatible, but geom_hline works, but it's not useful for the plot
  labs(title='{closest_state}') +
  theme(plot.title = element_text(hjust = 1, size = 22)) +
  transition_states(ACH_DATEyearmon, transition_length = 1, state_length = 50) +
  view_follow(fixed_x=TRUE) +
  ease_aes('linear')

Answer:

To add on to @eipi10's great answer, I think this is a case where it's worth replacing geom_bar for more flexibility. geom_bar is normally quite convenient for discrete categories, but it doesn't let us take full advantage of gganimate's silky-smooth animation glory.

For instance, with geom_tile, we can recreate the same appearance as geom_bar, but with fluid movement on the x-axis. This helps to keep visual track of each bar and to see which bars are shifting order the most. I think this addresses the 2nd part of your question nicely.

To make this work, we can add to the data a new column showing the ordering that should be used at each month. We save this order as a double, not an integer (by using* 1.0). This will allow gganimate to place a bar at position 1.25 when it's animating between position 1 and 2.

df2 <- df %>%
  group_by(ACH_DATEyearmon) %>%
  mutate(ordering = min_rank(DIAG_RATE_65_PLUS) * 1.0) %>%
  ungroup() 

Now we can plot in similar fashion, but using geom_tile instead of geom_bar. I wanted to show the NAME both on top and at the axis, so I used two geom_text calls with different y values, one at zero and one at the height of the bar. vjust lets us align each vertically using text line units.

The other trick here is to turn off clipping in coord_cartesian, which lets the bottom text go below the plot area, into where the x-axis text would usually go.

p <- df2 %>%
  ggplot(aes(ordering, group = NAME)) +

  geom_tile(aes(y = DIAG_RATE_65_PLUS/2, 
                height = DIAG_RATE_65_PLUS,
                width = 0.9), alpha = 0.9, fill = "gray60") +
  # text on top of bars
  geom_text(aes(y = DIAG_RATE_65_PLUS, label = NAME), vjust = -0.5) +
  # text in x-axis (requires clip = "off" in coord_cartesian)
  geom_text(aes(y = 0, label = NAME), vjust = 2) +
  coord_cartesian(clip = "off", expand = FALSE) +

  labs(title='{closest_state}', x = "") +
  theme(plot.title = element_text(hjust = 1, size = 22),
        axis.ticks.x = element_blank(),
        axis.text.x  = element_blank()) + 

  transition_states(ACH_DATEyearmon, 
                    transition_length = 2, state_length = 1) +
  ease_aes('cubic-in-out')

animate(p, nframes = 300, fps = 20, width = 400, height = 300)

Back to your first question, here's a color version that I made by removing fill = "gray60" from the geom_tile call. I sorted the NAME categories in order of Aug 2017, so they will look sequential for that one, as you described.

There's probably a better way to do that sorting, but I did it by joining df2 to a table with just the Aug 2017 ordering.

Aug_order <- df %>%
  filter(ACH_DATEyearmon == "Aug 2017") %>%
  mutate(Aug_order = min_rank(DIAG_RATE_65_PLUS) * 1.0) %>%
  select(NAME, Aug_order)

df2 <- df %>%
  group_by(ACH_DATEyearmon) %>%
  mutate(ordering = min_rank(DIAG_RATE_65_PLUS) * 1.0) %>%
  ungroup() %>%
  left_join(Aug_order) %>%
  mutate(NAME = fct_reorder(NAME, -Aug_order))

Question:

I've been looking with envy and admiration at the various ggplot animations appearing on twitter since David Robinson released his gganimate package and thought I'd have a play myself. I am having an issue with gganimate when using geom_bar. Hopefully the following example demonstrates the problem.

First generate some data for a reproducible example:

df <- data.frame(x = c(1, 2, 1, 2),
                 y = c(1, 2, 3, 4),
                 z = c("A", "A", "B", "B"))

To demonstrate what I'm trying to do I thought it would be useful to plot an ordinary ggplot, facetted by z. I'm trying to get gganimate to produce a gif that cycles between these 2 plots.

ggplot(df, aes(x = x, y = y)) +
   geom_bar(stat = "Identity") +
   facet_grid(~z)

But when I use gganimate the plot for B behaves oddly. In the second frame the bars start at the values that the first frame's bars finish at, rather than starting at the origin. As if it was a stacked bar chart.

p <- ggplot(df, aes(x = x, y = y, frame = z)) +
   geom_bar(stat = "Identity") 
gg_animate(p)

Incidentally when trying the same plot with geom_point everything works as expected.

q <- ggplot(df, aes(x = x, y = y, frame = z)) +
    geom_point() 
gg_animate(q)

I tried to post some images, but apparently I don't have sufficient reputation, so I hope it makes sense without them. Is this a bug, or am I missing something?

Thanks in advance,

Thomas


Answer:

The reason is that without faceting, the bars are stacked. Use position = "identity":

p <- ggplot(df, aes(x = x, y = y, frame = z)) +
  geom_bar(stat = "Identity", position = "identity") 
gg_animate(p)

In order to avoid confusion in situations like this, it is much more useful to replace frame by fill (or colour, depending on the geom you are using`):

p <- ggplot(df, aes(x = x, y = y, fill = z)) +
  geom_bar(stat = "Identity") 
p

The two plots that are drawn, when you replace fill by frame correspond exactly to exclusively drawing one of the colours at a time.

Question:

I am trying to animate some monthly data using gganimate. The plots are working great, except that the presence of descenders (letters that go below the baseline, i.e. g, j, p, q, and y) changes the amount of space that the title takes up. This, in turn, moves the baseline of the title just a bit, which detracts from the animation. That is, the title noticably "jumps" up a bit when there is a descender in the title.

An example:

myDF <-
  data.frame(
    Date = seq(as.Date("2015-01-15")
               , as.Date("2015-12-15")
               , "1 month")
    , x = 1:12
    , y = 1:12
  )

myDF$frame <-
  factor(format(myDF$Date, "%Y-%b")
         , levels = paste0("2015-", month.abb))

toAnimate <-
  ggplot(
    myDF
    , aes(x = x
          , y = y
          , frame = frame)
  ) +
  geom_point() +
  theme_gray()

gganimate::gganimate(toAnimate)

Using an older version of gganimate the issue was more obvious(and did not require the inclusion of the year to demonstrate), as it moved the plot instead of the title:

gganimate::gg_animate(toAnimate)

I can "fix" the issue by using all caps (which has no descenders), but I don't particularly like the look of all caps for this (particularly as part of larger titles for the actual use case). I could also prepend the frame title with something that already has a descender e.g. ggtitle("Timeperiod: ") though I'd rather not add irrelevant text just to workaround this issue (adding "Timeperiod: " is what I have gone with for now though).

I've looked through the help on theme in ggplot2, but I am not seeing anything that looks like it would address this issue.


Answer:

It looks like the title only gets the height of the text, and not the font's height, when reserving space for the title.

So you could instead use geom_text to place a title somewhere in the plot. For example, if I do:

ggplot(myDF, aes(x=x,y=y, label=frame)) +
   geom_point()+theme_gray() + 
   geom_text(x=2.5,y=5,aes(label=frame),adj=0)

(just as a ggplot, not animated yet...) I can see all the 2015's exactly overlapping, and the descenders of the month names are clearly there, and the text baseline is constant.

So if you can put your title in a handy space on the plot, you could use that, and use title_frame=FALSE in your gganimate.

I'd also consider a bug/enhancement report to ggplot2. If you make the title large enough it actually stomps on the plot:

ggplot(myDF, aes(x=x,y=y)) +geom_point()+theme(plot.title=element_text(size=rel(10),debug=TRUE)) + labs(title="y")

Question:

I'm trying to create a GIF using gganimate for a data set covering 90 years, i.e. I want to have a GIF running through 90 states/years. However, it seems like gganimate is only able to deal with less than 50 states.

So here's an example:

library(tidyverse)
# devtools::install_github('thomasp85/gganimate')
library(gganimate)

df = expand.grid(  x = 1,
                   y = c(2,3),
                year = 1670:1760) %>% mutate( z = 0.03* year,
                                              u = .2 * year)

this all works fine for 49 years:

ggplot(data=df %>% filter(., year %in% 1670:1719) , aes()) + 
  geom_point( aes(x = x, y = y, fill = z, size = u), shape = 21 ) + 
  labs( title = 'Year: {closest_state}') +
  enter_appear() +
  transition_states(year, transition_length = 1, state_length = 2) 

Yet, it gets weird when I include 50 (or more) years:

ggplot(data=df %>% filter(., year %in% 1670:1720) , aes()) + 
  geom_point( aes(x = x, y = y, fill = z, size = u), shape = 21 ) + 
  labs( title = 'Year: {closest_state}') +
  enter_appear() +
  transition_states(year, transition_length = 1, state_length = 2) 

How can I create a GIF for all 90 years? Any ideas are welcome! I'm still new to gganimate, am I using transition_states incorrectly?


Answer:

This has to do with the fact that gganmiate uses a fixed number of 100 frames for the animation. For up to 50 years (note that 1670:1719 has length 50, not 49), this is alright, but if you want to plot more years, you need more frames. You can control the number of frames by calling animate() explicitly.

For your example, this means that you should first store your plot in a variable:

p <- ggplot(df) + 
      geom_point(aes(x = x, y = y, fill = z, size = u), shape = 21) + 
      labs( title = 'Year: {closest_state}') +
      enter_appear() +
      transition_states(year, transition_length = 1, state_length = 2)

You can then start the animation by typing any of the following

p
animate(p)
animate(p, nframes = 100)

These three lines are equivalent. The first one is what you did in your example: this will implicitly call animate() to render the animation. The second line makes the call to animate() explicit and the third also explicitly sets the number of frames to 100. Since nframes = 100 is the default value, also this last line does the same as the others.

In order to make the animation work, you need to set a higher number of frames. 100 frames worked for 50 years, so 182 frames should work for the 91 years in your full data frame. Again, the following two lines are the same:

animate(p, nframes = 182)
animate(p, nframes = 2 * length(unique(df$year)))

And now it works:

I don't know for certain why you need twice the number of frames as you have years, but after reading the following statement from the documentation on transition_states()

It then tweens between the defined states and pauses at each state.

I would guess that one frame is used for the transition between two years and one frame is used to represent the date for a given year.

This would mean that you actually need one frame less than twice the number of years, because there is no frame needed for the transition after the last year. Indeed, the output from gganimate() for nframes = 100 and nframes = 182, respectively, is:

Frame 99 (100%)
Finalizing encoding... done!

Frame 181 (100%)
Finalizing encoding... done!

So it is indeed creating exactly the number of frames to be expected if my guess was correct.

Question:

I have been playing with the new version of gganimate, I tend to use animations a lot in my classes. I am trying to build a graph that shows how Nitrous oxide changes over time in a station in Spain. I want two features in the animations

  1. Stop for a while at each year
  2. Have the year in the title for each time

I have been able to build this two graphs using the following data

Madrid3 <- structure(list(month = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 
                     1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 
                     9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 
                     4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 
                     11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5,  
                     6, 7, 8, 9, 10, 11, 12), name = c("Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                       "Cuatro Caminos", "Cuatro Caminos"), year = c(2010, 2010, 2010, 
                                                                                                     2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2011, 2011, 
                                                                                                     2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2011, 2012, 
                                                                                                     2012, 2012, 2012, 2012, 2012, 2012, 2012, 2012, 2012, 2012, 2012, 
                                                                                                     2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 
                                                                                                     2013, 2014, 2014, 2014, 2014, 2014, 2014, 2014, 2014, 2014, 2014, 
                                                                                                     2014, 2014, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 
                                                                                                     2015, 2015, 2015, 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016, 
                                                                                                     2016, 2016, 2016, 2016, 2017, 2017, 2017, 2017, 2017, 2017, 2017, 
                                                                                                     2017, 2017, 2017, 2017, 2017), NO_2 = c(52.7411978235155, 49.9936308697576, 
                                                                                                                                             45.3346567988235, 42.7514465030941, 35.8548923634714, 47.1773919094889, 
                                                                                                                                             53.7143816896664, 41.9823522292158, 63.525647942449, 72.838654011127, 
                                                                                                                                             67.8730001012484, 74.422916644363, 72.6258411843876, 82.929955290611, 
                                                                                                                                             54.8495702005731, 52.7180555555556, 46.2647849462366, 50.0291666666667, 
                                                                                                                                             41.483039348711, 39.4808510638298, 57.9651324965132, 58.7177419354839, 
                                                                                                                                             51.3212795549374, 54.7997311827957, 65.4245283018868, 52.0502873563218, 
                                                                                                                                             46.5370121130552, 28.3212795549374, 35.5846774193548, 28.4361111111111, 
                                                                                                                                             31.822102425876, 26.3978494623656, 39.7367688022284, 57.5685483870968, 
                                                                                                                                             50.7777777777778, 59.7415881561238, 52.8936742934051, 48.1741071428571, 
                                                                                                                                             34.8891891891892, 36.7805555555556, 34.9381720430108, 33.1390820584145, 
                                                                                                                                             38.257065948856, 29.1467025572005, 46.3147632311978, 48.7190860215054, 
                                                                                                                                             48.9763560500695, 66.9152086137281, 45.5302826379542, 40.3288690476191, 
                                                                                                                                             46.7063599458728, 36.5340751043115, 34.25, 34.5805555555556, 
                                                                                                                                             33.1009421265141, 25.4072580645161, 38.3157162726008, 52.9743243243243, 
                                                                                                                                             47.8969359331476, 66.6617250673854, 70.5094594594595, 39.5111773472429, 
                                                                                                                                             47.6205962059621, 30.6193820224719, 32.2088948787062, 35.2154929577465, 
                                                                                                                                             35.3301886792453, 24.688679245283, 37.933147632312, 46.2293080054274, 
                                                                                                                                             65.5738161559889, 73.0350404312669, 44.7102425876011, 39.2126436781609, 
                                                                                                                                             37.7466307277628, 34.9527777777778, 32.7379032258064, 33.7051460361613, 
                                                                                                                                             35.6263440860215, 28.3189771197847, 46.3207810320781, 55.5389784946237, 
                                                                                                                                             54.9066852367688, 66.5080862533693, 59.8812415654521, 46.010447761194, 
                                                                                                                                             43.7183288409704, 34.3513888888889, 33.4, 35.7649513212796, 33.9986486486486, 
                                                                                                                                             26.2876344086022, 43.5251396648045, 59.6370967741936, 73.4442896935933, 
                                                                                                                                             60.0040431266846), n = c(743L, 672L, 744L, 720L, 744L, 720L, 
                                                                                                                                                                      744L, 744L, 720L, 744L, 720L, 744L, 743L, 672L, 720L, 720L, 744L, 
                                                                                                                                                                      720L, 744L, 720L, 720L, 744L, 720L, 744L, 743L, 696L, 744L, 720L, 
                                                                                                                                                                      744L, 720L, 744L, 744L, 720L, 744L, 720L, 744L, 743L, 672L, 744L, 
                                                                                                                                                                      720L, 744L, 720L, 744L, 744L, 720L, 744L, 720L, 744L, 743L, 672L, 
                                                                                                                                                                      744L, 720L, 744L, 720L, 744L, 744L, 720L, 744L, 720L, 744L, 743L, 
                                                                                                                                                                      672L, 744L, 720L, 744L, 720L, 744L, 744L, 720L, 744L, 720L, 744L, 
                                                                                                                                                                      743L, 696L, 744L, 720L, 744L, 720L, 744L, 744L, 720L, 744L, 720L, 
                                                                                                                                                                      744L, 743L, 672L, 744L, 720L, 744L, 720L, 744L, 744L, 720L, 744L, 
                                                                                                                                                                      720L, 744L)), row.names = c(NA, -96L), class = c("grouped_df", 
                                                                                                                                                                                                                       "tbl_df", "tbl", "data.frame"), vars = c("month", "name"), .Names = c("month", 
                                                                                                                                                                                                                                                                                             "name", "year", "NO_2", "n"), indices = list(c(0L, 12L, 24L, 
                                                                                                                                                                                                                                                                                                                                            36L, 48L, 60L, 72L, 84L), c(1L, 13L, 25L, 37L, 49L, 61L, 73L, 
                                                                                                                                                                                                                                                                                                                                                                        85L), c(2L, 14L, 26L, 38L, 50L, 62L, 74L, 86L), c(3L, 15L, 27L, 
                                                                                                                                                                                                                                                                                                                                                                                                                          39L, 51L, 63L, 75L, 87L), c(4L, 16L, 28L, 40L, 52L, 64L, 76L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                      88L), c(5L, 17L, 29L, 41L, 53L, 65L, 77L, 89L), c(6L, 18L, 30L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        42L, 54L, 66L, 78L, 90L), c(7L, 19L, 31L, 43L, 55L, 67L, 79L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    91L), c(8L, 20L, 32L, 44L, 56L, 68L, 80L, 92L), c(9L, 21L, 33L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      45L, 57L, 69L, 81L, 93L), c(10L, 22L, 34L, 46L, 58L, 70L, 82L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  94L), c(11L, 23L, 35L, 47L, 59L, 71L, 83L, 95L)), group_sizes = c(8L, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L), biggest_group_size = 8L, labels = structure(list(
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      month = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), name = c("Cuatro Caminos", 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos", 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 "Cuatro Caminos", "Cuatro Caminos", "Cuatro Caminos")), row.names = c(NA, 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       -12L), class = "data.frame", vars = c("month", "name"), .Names = c("month", 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          "name")))
Using transition_time

When I use transition time, using the following code:

ggplot(Madrid2,aes(x = month, y = NO_2)) + stat_smooth(method = "lm", formula = y ~ x + I(x^2), alpha = 0.5,aes(fill = name)) + geom_point() + 
   # Here comes the gganimate code
  transition_time(year) +
  enter_fade() + 
  exit_shrink() +
  ease_aes('linear') + labs(title = 'Year: {round(frame_time,0)}', x = 'Month', y = 'NO_2')

I get this image, which is good because I get the years as a title, but I would like the gif to stop for a while on each year. It follows condition 2 but not 1 of my list

So I try the following code:

ggplot(Madrid3,aes(x = month, y = NO_2)) + stat_smooth(method = "lm", formula = y ~ x + I(x^2), alpha = 0.5,aes(fill = name)) + geom_point() + 
  # Here comes the gganimate code
  transition_time(year, state_length = 2, transition_length = 1) +
  enter_fade() + 
  exit_shrink() +
  ease_aes('linear') + labs(title = 'Year: {round(frame_time,0)}', x = 'Month', y = 'NO_2')

but I get the following error:

 Error in transition_time(year, state_length = 2, transition_length = 1) : 
  unused arguments (state_length = 2, transition_length = 1)

So I tried with transition_states instead of transition_times

using transition states

With transtition_states I have a different problem, it works fine if I do this:

ggplot(Madrid3,aes(x = month, y = NO_2)) + stat_smooth(method = "lm", formula = y ~ x + I(x^2), alpha = 0.5,aes(fill = name)) + geom_point() + 
   # Here comes the gganimate code
   transition_states(year, state_length = 2, transition_length = 1) +
   enter_fade() + 
   exit_shrink() +
   ease_aes('linear') 

Which gives me the following graph:

In this case I have the transtition pauses I wanted (condition 1), but I can't get the titles to work (condition 2), I have tried:

ggplot(Madrid3,aes(x = month, y = NO_2)) + stat_smooth(method = "lm", formula = y ~ x + I(x^2), alpha = 0.5,aes(fill = name)) + geom_point() + 
  # Here comes the gganimate code
  transition_states(year, state_length = 2, transition_length = 1) +
  enter_fade() + 
  exit_shrink() +
  ease_aes('linear') + labs(title = 'Year: {round(frame_time,0)}', x = 'Month', y = 'NO_2')

Which gives me the following error:

Error in eval(parse(text = text, keep.source = FALSE), envir) : 
   object 'frame_time' not found

So then I though, maybe change frame_time to frame_states

 ggplot(Madrid3,aes(x = month, y = NO_2)) + stat_smooth(method = "lm", formula = y ~ x + I(x^2), alpha = 0.5,aes(fill = name)) + geom_point() + 
  # Here comes the gganimate code
  transition_states(year, state_length = 2, transition_length = 1) +
  enter_fade() + 
  exit_shrink() +
  ease_aes('linear') + labs(title = 'Year: {round(frame_states,0)}', x = 'Month', y = 'NO_2')

But it gives me the following error:

Error in eval(parse(text = text, keep.source = FALSE), envir) : 
  object 'frame_states' not found

I am not sure what else to try


Answer:

?transition_states tells you which "variables [are] available for string literal interpretation". So these are the variables you can use for your title. You want one of the following:

  • previous_state
  • next_state
  • closest_state

depending on preference.

Question:

I'd like to insert another column value of my data into a gganimate animation title.

Example, here the states level variable is x and I'd like to add to title variable y:

df <- tibble(x = 1:10, y = c('a', 'a', 'b', 'd', 'c', letters[1:5]))
df

A tibble: 10 x 2
       x y    
   <int> <chr>
 1     1 a    
 2     2 a    
 3     3 b    
 4     4 d    
 5     5 c    
 6     6 a    
 7     7 b    
 8     8 c    
 9     9 d    
10    10 e 

This works as expected:

ggplot(df, aes(x, x)) +
  geom_point() +
  labs(title = '{closest_state}') +
  transition_states(x,
                    transition_length = 0.1,
                    state_length = 0.1)

This fails:

ggplot(df, aes(x, x)) +
  geom_point() +
  labs(title = '{closest_state}, another_var: {y}') +
  transition_states(x,
                    transition_length = 0.1,
                    state_length = 0.1)

Error in eval(parse(text = text, keep.source = FALSE), envir) : object 'y' not found

Also tried this, but y will not change:

ggplot(df, aes(x, x)) +
  geom_point() +
  labs(title = str_c('{closest_state}, another_var: ', df$y)) +
  transition_states(x,
                    transition_length = 0.1,
                    state_length = 0.1)

Another option is to map y as the states level variable and use the frame variable instead of x, but in my application y is either a not-necessarily-unique character variable like above, or it is a numeric variable but again not-necessarily-unique and not-necessarily-ordered. In which case gganimate (or ggplot?) will order it as it sees fit, making the final result weird not ordered by x:

ggplot(df, aes(x, x)) +
  geom_point() +
  labs(title = '{frame}, another_var: {closest_state}') +
  transition_states(y,
                    transition_length = 0.1,
                    state_length = 0.1)

So how to simply add the changing value of the un-ordered, not numeric, y variable?

Finally: This question was asked here but without a reproducible example so it was not answered, hoping this one is better.


Answer:

One dirty solution would be to paste together the variables and make a new one to use in the transition_states:

df <- mutate(df, title_var = factor(paste(x, y, sep="-"), levels = paste(x, y, sep="-")))
# # A tibble: 6 x 3
# x y     title_var
# <int> <chr> <fct>    
# 1     1 a     1-a      
# 2     2 a     2-a      
# 3     3 b     3-b      
# 4     4 d     4-d      
# 5     5 c     5-c      
# 6     6 a     6-a  

Then we could use gsub() in ordet to strip closest_state from the unwanted part, like this:

gsub(pattern = "\\d+-", replacement = "", "1-a") 
"a"

So:

ggplot(df, aes(x, x)) +
  geom_point() +
  labs(title = '{gsub(pattern = "\\d+-", replacement = "", closest_state)}') +
  transition_states(title_var, transition_length = 0.1, state_length = 0.1)

Question:

I am trying to add a year title to plot from a data set that has been run through tweenr. Following the example from revolutionanalytics.com

library(tidyverse)
library(tweenr)
library(gapminder)

gapminder_edit <- gapminder %>%
  arrange(country, year) %>%
  select(gdpPercap,lifeExp,year,country, continent, pop) %>%
  rename(x=gdpPercap,y=lifeExp,time=year,id=country) %>%
  mutate(ease="linear")

gapminder_tween <- tween_elements(gapminder_edit,
                              "time", "id", "ease", nframes = 150) %>%
  mutate(year = round(time), country = .group) %>%
  left_join(gapminder, by=c("country","year","continent")) %>%
  rename(population = pop.x)

gapminder_tween %>% arrange(country, .frame) %>% head()
#          x        y     time continent population .frame      .group year     country lifeExp   pop.y gdpPercap
# 1 779.4453 28.80100 1952.000      Asia    8425333      0 Afghanistan 1952 Afghanistan  28.801 8425333  779.4453
# 2 781.7457 28.88606 1952.278      Asia    8470644      1 Afghanistan 1952 Afghanistan  28.801 8425333  779.4453
# 3 784.0462 28.97111 1952.556      Asia    8515955      2 Afghanistan 1953 Afghanistan      NA      NA        NA
# 4 786.3466 29.05617 1952.833      Asia    8561267      3 Afghanistan 1953 Afghanistan      NA      NA        NA
# 5 788.6470 29.14122 1953.111      Asia    8606578      4 Afghanistan 1953 Afghanistan      NA      NA        NA
# 6 790.9475 29.22628 1953.389      Asia    8651889      5 Afghanistan 1953 Afghanistan      NA      NA        NA

To create the gif I can use the frame titles (a bit meaningless) and set title_frame = TRUE (default) in the gganimate function..

library(gganimate)
library(animation)
p2 <- ggplot(gapminder_tween,
             aes(x=x, y=y, frame = .frame)) +
  geom_point(aes(size=population, color=continent),alpha=0.8) +
  xlab("GDP per capita") +
  ylab("Life expectancy at birth") +
  scale_x_log10()

magickPath <- shortPathName("C:\\Program Files\\ImageMagick-7.0.6-Q16\\magick.exe")
gganimate(p2, ani.options = ani.options(convert=magickPath), interval = 0.1)

I tried to use the year column (frame = year in the mapping aesthetics), but this only produces 56 frames and points appearing multiple times in each frame..

p2 <- ggplot(gapminder_tween,
             aes(x=x, y=y, frame = year)) +
  geom_point(aes(size=population, color=continent),alpha=0.8) +
  xlab("GDP per capita") +
  ylab("Life expectancy at birth") +
  scale_x_log10()

Can I (and if so, how) have the first gif with titles for each frame corresponding to the corresponding values of year in the tween'ed data frame?


Answer:

I modified the gg_animate function introducing the possibility to customize plot titles using the ttl aesthetic. Download the file here and save it in your working directory with the name mygg_animate.r. Then, run the following code:

library(tidyverse)
library(tweenr)
library(gapminder)

gapminder_edit <- gapminder %>%
  arrange(country, year) %>%
  select(gdpPercap,lifeExp,year,country, continent, pop) %>%
  rename(x=gdpPercap,y=lifeExp,time=year,id=country) %>%
  mutate(ease="linear")

gapminder_tween <- tween_elements(gapminder_edit,
                              "time", "id", "ease", nframes = 200) %>%
  mutate(year = round(time), country = .group) %>%
  left_join(gapminder, by=c("country","year","continent")) %>%
  rename(population = pop.x)

library(gganimate)
library(animation)
source("mygg_animate.r")

# Define plot titles using the new aesthetic
p2 <- ggplot(gapminder_tween,
             aes(x=x, y=y, frame=.frame, ttl=year)) +
  geom_point(aes(size=population, color=continent),alpha=0.8) +
  xlab("GDP per capita") +
  ylab("Life expectancy at birth") +
  scale_x_log10()

magickPath <- shortPathName("C:\\Program Files\\ImageMagick-7.0.6-Q16\\magick.exe")

mygg_animate(p2, ani.options = ani.options(convert=magickPath), 
     interval = 0.1, title_frame=T)

Below the resulting animated graph (the time sequence has been truncated in order to reduce the dimension of the gif file).

Question:

I am trying to use my own image for geom_point, something I can just read in. I am aware geom_point allows you to choose many shapes (well over 300) by simply writing shape = 243 but I want my own image such as a logo.

When I have not specified color = factor(Name) then it works as expected. When I do specify the colour of the line then the image becomes a solid single colour. I want this line to be coloured so is there any way around this? Thanks!

library(gganimate)
library(gifski)
library(png)
library(ggimage)


Step  <- 1:50
Name  <- rep("A",50)
Image <- rep(c("https://jeroenooms.github.io/images/frink.png"),50)
Value <- runif(50,0,10)
Final <- data.frame(Step, Name, Value, Image)

a <- ggplot(Final, aes(x = Step, y = Value, group = Name, color = factor(Name))) + 
  geom_line(size=1) + 
  geom_image(aes(image=Image)) +
  transition_reveal(Step) + 
  coord_cartesian(clip = 'off') + 
  theme_minimal() +
  theme(plot.margin = margin(5.5, 40, 5.5, 5.5)) +
  theme(legend.position = "none") 

options(gganimate.dev_args = list(width = 7, height = 6, units = 'in', res=100))
animate(a, nframes = 100)


Answer:

Is this what your are looking for ?

I Just changed the color = factor(Name) position to geom_line statement.

If you use color = factor(Name) with ggplot in first row, it will affect to whole plot. So you should take care when using this statement.

a <- ggplot(Final, aes(x = Step, y = Value, group = Name)) + 
  geom_line(size=1, aes(color = factor(Name))) + 
  geom_image(aes(image=Image)) +
  transition_reveal(Step) + 
  coord_cartesian(clip = 'off') + 
  theme_minimal() +
  theme(plot.margin = margin(5.5, 40, 5.5, 5.5)) +
  theme(legend.position = "none") 

For convenience, i captured the picture .

Question:

I created an animated bar chart which displays the scored goals by some players. Below the whole code is displayed how I came to the output.

The animation works as wished. However, bars with the same value overlap.

I would like to prevent the bars from overlapping. The best case would be for the player who scored first to be displayed above other players at the same rank.

The order of players who scored equally at the beginning of the animation does not matter.

library(tidyverse)
library(gganimate)
theme_set(theme_classic())

df <- data.frame(Player = rep(c("Aguero", "Salah", "Aubameyang", "Kane"), 6), 
                 Team = rep(c("ManCity", "Liverpool", "Arsenal", "Tottenham"), 6), 
                 Gameday = c(1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6),
                 Goals = c(0,1,2,0,1,1,3,1,2,1,3,2,2,2,4,3,3,2,4,5,5,3,5,6),
                 stringsAsFactors = F)

gap <- df %>%
  group_by(Gameday) %>%
  mutate(rank = min_rank(-Goals) * 1,
     Value_rel = Goals/Goals[rank==1],
     Value_lbl = paste0(" ", Goals)) %>%
  filter(rank <=10) %>%
  ungroup()

p <- ggplot(gap, aes(rank, group = Player, stat = "identity",
                 fill = as.factor(Player), color = as.factor(Player))) +
  geom_tile(aes(y = Goals/2,
            height = Goals,
            width = 0.9), alpha = 0.8, color = NA) +
  geom_text(aes(y = 0, label = paste(Player, " ")), vjust = 0.2, hjust = 1) +
  geom_text(aes(y=Goals,label = Value_lbl, hjust=0)) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday {closest_state}", x="", y = "Goals scored") +
  theme(plot.title = element_text(hjust = 0, size = 22),
       axis.ticks.y = element_blank(),  # These relate to the axes post-flip
       axis.text.y  = element_blank(),  # These relate to the axes post-flip
       plot.margin = margin(1,1,1,4, "cm")) +
  transition_states(Gameday, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

p

The code outputs following plot:

Additional note:

At the end, the bars should be displayed according to the example below. Preferably the bars should not be on the same height, to increase the readability.

Thank you very much for your effort!


Answer:

Edited solution based on clarification:

gap %>%

  # for each player, note his the rank from his previous day
  group_by(Player) %>%
  arrange(Gameday) %>%
  mutate(prev.rank = lag(rank)) %>%
  ungroup() %>%

  # for every game day,
  # sort players by rank & break ties by previous day's rank
  group_by(Gameday) %>%
  arrange(rank, prev.rank) %>%
  mutate(x = seq(1, n())) %>%
  ungroup() %>%

  ggplot(aes(x = x, y = Goals, fill = Player, color = Player)) +
  # geom_tile(aes(y = Goals/2, height = Goals, width = width)) +
  geom_col() +
  geom_text(aes(y = 0, label = Player), hjust = 1) +
  geom_text(aes(label = Value_lbl), hjust = 0) +

  # rest of the code below is unchanged from the question
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday {closest_state}", x="", y = "Goals scored") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(), 
        axis.text.y  = element_blank(),
        plot.margin = margin(1,1,1,4, "cm")) +
  transition_states(Gameday, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

Original solution:

gap %>%

  # for each player, note his the rank from his previous day
  group_by(Player) %>%
  arrange(Gameday) %>%
  mutate(prev.rank = lag(rank)) %>%
  ungroup() %>%

  # for every game day & every rank,
  # reduce tile width if there are multiple players sharing that rank, 
  # sort players in order of who reached that rank first, 
  # & calculate the appropriate tile midpoint depending on how many players are there
  group_by(Gameday, rank) %>%
  mutate(n = n_distinct(Player)) %>%
  mutate(width = 0.9 / n_distinct(Player)) %>%
  arrange(prev.rank) %>%
  mutate(x = rank + 0.9 * (seq(1, 2 * n() - 1, by = 2) / 2 / n() - 0.5)) %>%
  ungroup() %>%

  ggplot(aes(x = x, fill = Player, color = Player)) +
  geom_tile(aes(y = Goals/2, height = Goals, width = width)) +
  geom_text(aes(y = 0, label = Player), hjust = 1) +
  geom_text(aes(y = Goals, label = Value_lbl), hjust = 0) +

  # rest of the code below is unchanged from the question
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday {closest_state}", x="", y = "Goals scored") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(), 
        axis.text.y  = element_blank(),
        plot.margin = margin(1,1,1,4, "cm")) +
  transition_states(Gameday, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

Note: This isn't perfect. I imagine the simple logic above for determining player order within the same day / rank won't be ideal if there are too many players / too many days, since it only looks backwards by one day. But it works for this example, & I don't know enough about football (at least I think this is football?) to extrapolate about your use case.

Question:

I created an animated bar plot displaying goals scored by players (fictional).

Please see the reproduced data for the example:

df <- data.frame(Player = rep(c("Aguero", "Salah", "Aubameyang", "Kane"), 6),
                 Team = rep(c("ManCity", "Liverpool", "Arsenal", "Tottenham"), 6), 
                 Gameday = c(1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6),
                 Goals = c(0,1,2,0,1,1,3,1,2,1,3,2,2,2,4,3,3,2,4,5,5,3,5,6),
                 stringsAsFactors = F)

Following animated bar plot are created by the code below.

# loading required
library(tidyverse)
library(gganimate)
library(png)

Edited: I would like to include following icons for each player:

icon1.png <- image_read('https://raw.githubusercontent.com/sialbi/examples/master/player1.png')
icon2.png <- image_read('https://raw.githubusercontent.com/sialbi/examples/master/player2.png')
icon3.png <- image_read('https://raw.githubusercontent.com/sialbi/examples/master/player3.png')
icon4.png <- image_read('https://raw.githubusercontent.com/sialbi/examples/master/player4.png')

gap <- df %>%
  group_by(Gameday) %>%
  mutate(rank = min_rank(-Goals) * 1,
         Value_rel = Goals/Goals[rank==1],
         Value_lbl = paste0(" ", Goals)) %>%
  filter(rank <=10) %>%
  ungroup()

gap %>%
  group_by(Player) %>%
  arrange(Gameday) %>%
  mutate(prev.rank = lag(rank)) %>%
  ungroup() %>%

  group_by(Gameday) %>%
  arrange(rank, prev.rank) %>%
  mutate(x = seq(1, n())) %>%
  ungroup() %>%

  ggplot(aes(x = x, y = Goals, fill = Player, color = Player)) +
  geom_col() +
  geom_text(aes(y = 0, label = Player), size = 5, color="black", hjust = -0.05) +
  geom_text(aes(label = Value_lbl), hjust = 0) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday: {closest_state}", x="", y = "Goals scored") +
  theme(plot.title = element_text(hjust = 0, size = 26),
        axis.ticks.y = element_blank(), 
        axis.text.y  = element_blank(),
        plot.margin = margin(1,1,1,4, "cm")) +
 transition_states(Gameday, transition_length = 4, state_length = 1) +
 ease_aes('cubic-in-out')

Problem

To complete the animation I would like to include a picture of each player on the y axis. Below I edited the animation to display the desired result (the circles were chosen to avoid violating any copyrights).

The images (circles) should also move up and down as the bars.

Is there are way to include images on the y-axis?

Edited Code

After the presented suggestions I was able to fix the problems. The code below is working accordingly.

library(imager)
library(ggimage)
library(magick)
library(tidyverse)
library(gganimate)
library(png)
library(gapminder)


 #read data
 df <- data.frame(Player = rep(c("Aguero", "Salah", "Aubameyang", "Kane"), 6),
             Team = rep(c("ManCity", "Liverpool", "Arsenal", "Tottenham"), 6), 
             Gameday = c(1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6),
             Goals = c(0,1,2,0,1,1,3,1,2,1,3,2,2,2,4,3,3,2,4,5,5,3,5,6),
             stringsAsFactors = F)

# import images
df2 <- data.frame(Player = c("Aguero", "Salah", "Aubameyang", "Kane"),
              Image = sample(c("https://raw.githubusercontent.com/sialbi/examples/master/player1.png",
                        "https://raw.githubusercontent.com/sialbi/examples/master/player2.png",
                        "https://raw.githubusercontent.com/sialbi/examples/master/player3.png",
                        "https://raw.githubusercontent.com/sialbi/examples/master/player4.png")),
              stringsAsFactors = F)


gap <- df %>%
  group_by(Gameday) %>%
  mutate(rank = min_rank(-Goals) * 1,
         Value_rel = Goals/Goals[rank==1],
         Value_lbl = paste0(" ", Goals)) %>%
  filter(rank <=10) %>%
  ungroup()

p = gap %>%
  left_join(df2, by = "Player") %>% # add image file location to the dataframe being
  group_by(Player) %>%
  arrange(Gameday) %>%
  mutate(prev.rank = lag(rank)) %>%
  ungroup() %>%      
  group_by(Gameday) %>%
  arrange(rank, prev.rank) %>%
  mutate(x = seq(1, n())) %>%
  ungroup()

ggplot(p, aes(x = x, y = Goals, fill = Player, color = Player)) +
  geom_col() +
  geom_text(aes(y = 0, label = Player), size = 5, color="black", hjust = -0.05) +
  geom_text(aes(label = Value_lbl), hjust = 0) +

  # where the error occurs 
  geom_image(aes(x = x, Image = Image), y = 0,  
             size = 0.25, hjust = 1,
             inherit.aes = FALSE) +

  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday: {closest_state}", x = "", y = "Goals scored") +
  theme_classic() +
  theme(plot.title = element_text(hjust = 0, size = 26),
        axis.ticks.y = element_blank(),
        axis.text.y  = element_blank(),
        plot.margin = margin(1, 1, 1, 4, "cm")) +
  transition_states(Gameday, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')


Answer:

You can try the following:

Step 0. Create png images for use, because I don't want to worry about copyright violations either.

emoji.list <- c("grinning", "smile", "heart_eyes", "smirk")
for(i in seq_along(emoji.list)) {
  ggsave(paste0("icon", i, ".png"),
         ggplot() + 
           emojifont::geom_emoji(alias = emoji.list[i], size = 10, vjust = 0.5) +
           theme_void(),
         width = 0.4, height = 0.4, units = "in")
}
rm(emoji.list, i)

Step 1. Create a data frame mapping each player to the location of his image file.

df2 <- data.frame(Player = c("Aguero", "Salah", "Aubameyang", "Kane"),
                  Image = c("icon1.png", "icon2.png", "icon3.png", "icon4.png"),
                  stringsAsFactors = F)

Step 2. Add image to plot in a new geom_image layer, & animate everything as before.

library(ggimage)

gap %>%
  left_join(df2, by = "Player") %>% # add image file location to the dataframe being
                                    # passed to ggplot()      
  group_by(Player) %>%
  arrange(Gameday) %>%
  mutate(prev.rank = lag(rank)) %>%
  ungroup() %>%      
  group_by(Gameday) %>%
  arrange(rank, prev.rank) %>%
  mutate(x = seq(1, n())) %>%
  ungroup() %>%

  ggplot(aes(x = x, y = Goals, fill = Player, color = Player)) +
  geom_col() +
  geom_text(aes(y = 0, label = Player), size = 5, color="black", hjust = -0.05) +
  geom_text(aes(label = Value_lbl), hjust = 0) +

  geom_image(aes(x = x, image = Image), y = 0,  # add geom_image layer
             size = 0.25, hjust = 1,
             inherit.aes = FALSE) +

  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +
  labs(title = "Gameday: {closest_state}", x = "", y = "Goals scored") +
  theme_classic() +
  theme(plot.title = element_text(hjust = 0, size = 26),
        axis.ticks.y = element_blank(),
        axis.text.y  = element_blank(),
        plot.margin = margin(1, 1, 1, 4, "cm")) +
  transition_states(Gameday, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')