rggplot2gganimate

Display intermediate values in transition in race bar chart


I'm working on a race bar chart using gganimate where I show the evolution of total goals scored through each month during the season. I managed to get quite close to what I want with this output:

enter image description here

(The layout is awful I know it's just to meet the requirements of the upload size on stackoverflow).

The last part I'm trying to achieve is that geom_text() shows the intermediate values during the transitions.

For instance, instead of going from 7 directly to 12, I would like the graph to show 7,8,9,10,11 while the bar is adjusting. I'm not really sure how I can get to that result and I've been stuck for quite a while on this.

Here is a sample of my dataframe:

df2<-structure(list(Season = c("2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024", 
"2023/2024", "2023/2024", "2023/2024", "2023/2024", "2023/2024"
), DATEMM = structure(c(2023.5, 2023.5, 2023.5, 2023.5, 2023.5, 
2023.5, 2023.5, 2023.5, 2023.58333333333, 2023.58333333333, 2023.58333333333, 
2023.58333333333, 2023.58333333333, 2023.58333333333, 2023.58333333333, 
2023.58333333333, 2023.66666666667, 2023.66666666667, 2023.66666666667, 
2023.66666666667, 2023.66666666667, 2023.66666666667, 2023.66666666667, 
2023.66666666667, 2023.75, 2023.75, 2023.75, 2023.75, 2023.75, 
2023.75, 2023.75, 2023.75, 2023.83333333333, 2023.83333333333, 
2023.83333333333, 2023.83333333333, 2023.83333333333, 2023.83333333333, 
2023.83333333333, 2023.83333333333, 2023.91666666667, 2023.91666666667, 
2023.91666666667, 2023.91666666667, 2023.91666666667, 2023.91666666667, 
2023.91666666667, 2023.91666666667), class = "yearmon"), League = c("Super League", 
"Jupiler Pro League", "Bundesliga", "Liga", "LigaNOS", "Ligue 1", 
"Premier League", "Serie A", "Super League", "Premier League", 
"LigaNOS", "Ligue 1", "Liga", "Bundesliga", "Serie A", "Jupiler Pro League", 
"Liga", "Premier League", "LigaNOS", "Bundesliga", "Super League", 
"Serie A", "Ligue 1", "Jupiler Pro League", "Liga", "Premier League", 
"Bundesliga", "LigaNOS", "Serie A", "Super League", "Ligue 1", 
"Jupiler Pro League", "Premier League", "Liga", "Bundesliga", 
"Serie A", "LigaNOS", "Super League", "Ligue 1", "Jupiler Pro League", 
"Premier League", "Liga", "Bundesliga", "Serie A", "LigaNOS", 
"Ligue 1", "Super League", "Jupiler Pro League"), goals = c(36, 
21, 0, 0, 0, 0, 0, 0, 94, 86, 85, 85, 80, 65, 52, 38, 210, 206, 
185, 158, 155, 148, 137, 84, 312, 300, 271, 246, 237, 217, 212, 
139, 401, 392, 353, 314, 292, 290, 267, 207, 607, 479, 454, 429, 
392, 358, 339, 316), rank = c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 
1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 
1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 
1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), image_file = c("~/lglogos/Super League.png", 
"~/lglogos/Jupiler Pro League.png", "~/lglogos/Bundesliga.png", 
"~/lglogos/Liga.png", "~/lglogos/LigaNOS.png", "~/lglogos/Ligue 1.png", 
"~/lglogos/Premier League.png", "~/lglogos/Serie A.png", "~/lglogos/Super League.png", 
"~/lglogos/Premier League.png", "~/lglogos/LigaNOS.png", "~/lglogos/Ligue 1.png", 
"~/lglogos/Liga.png", "~/lglogos/Bundesliga.png", "~/lglogos/Serie A.png", 
"~/lglogos/Jupiler Pro League.png", "~/lglogos/Liga.png", "~/lglogos/Premier League.png", 
"~/lglogos/LigaNOS.png", "~/lglogos/Bundesliga.png", "~/lglogos/Super League.png", 
"~/lglogos/Serie A.png", "~/lglogos/Ligue 1.png", "~/lglogos/Jupiler Pro League.png", 
"~/lglogos/Liga.png", "~/lglogos/Premier League.png", "~/lglogos/Bundesliga.png", 
"~/lglogos/LigaNOS.png", "~/lglogos/Serie A.png", "~/lglogos/Super League.png", 
"~/lglogos/Ligue 1.png", "~/lglogos/Jupiler Pro League.png", 
"~/lglogos/Premier League.png", "~/lglogos/Liga.png", "~/lglogos/Bundesliga.png", 
"~/lglogos/Serie A.png", "~/lglogos/LigaNOS.png", "~/lglogos/Super League.png", 
"~/lglogos/Ligue 1.png", "~/lglogos/Jupiler Pro League.png", 
"~/lglogos/Premier League.png", "~/lglogos/Liga.png", "~/lglogos/Bundesliga.png", 
"~/lglogos/Serie A.png", "~/lglogos/LigaNOS.png", "~/lglogos/Ligue 1.png", 
"~/lglogos/Super League.png", "~/lglogos/Jupiler Pro League.png"
)), row.names = c(NA, -48L), class = c("tbl_df", "tbl", "data.frame"
))

And here is my code for the graph:


colpal<-c('Ligue 1'="#E5174F",'Premier League' ="#99CBE7", 'Liga' = "#122623", 'Bundesliga' = "#00bbf9ff", 'Serie A' ="#3469A6",
          "LigaNOS" = "#5EB1BF","Jupiler Pro League" ="#F6AE2D","Super League" = "#B3001B")
colpal.order<-c('Ligue 1','Premier League', 'Liga', 'Bundesliga', 'Serie A',"LigaNOS","Jupiler Pro League",
                "Super League")

gfb<-ggplot(df2, aes(rank,group=League,fill=factor(League,levels = colpal.order),
                     color = as.factor(League)))+
  geom_tile(aes(y=goals/2,x=rank,height=goals,width = 0.5),alpha = 0.8, color = NA) +
    ggtext::geom_richtext(
    aes(
      y = 0,
      x = rank,
      label = sprintf("<img src='%s' width='70'/>", image_file)),
    hjust = 1,
    inherit.aes = FALSE,
    fill = NA,
    color = NA,
    label.padding = grid::unit(30, "pt"))+
  scale_fill_manual(values = colpal)+
  scale_color_manual(values = colpal) +
  coord_flip(clip="off",expand=FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse()+
  geom_text(aes(y=goals, label= ifelse(goals > 0, sprintf("%1.0f", goals), "")),
            color="white",fontface = "bold", size = 20,family="Archivo Black",vjust = 0.5, hjust = 1)+
  guides(color = guide_none())+
  theme_minimal()+
  theme( panel.grid = element_blank(),
         panel.grid.major.x = element_line( linewidth = .5, color="#122623"),
         panel.background = element_rect(colour = NA,fill="transparent"),
         legend.position = "none", 
         axis.ticks.y = element_blank(),
         axis.title.y = element_blank(),
         axis.text.y = element_blank(),
         axis.ticks.x = element_blank(), 
         axis.title.x = element_blank(), 
         axis.text.x = element_text(size=20,face = 'bold'),
         plot.margin = margin(8, 6, 8, 6, "cm"),
         plot.background = element_rect(fill = "#ffffff", color = NA), 
         plot.title = element_text(size = 28, hjust = 0.5, face = 'bold', color = "#21FA90", vjust = 5,family="Archivo Black"), 
         plot.subtitle = element_text(size = 17.5, hjust = 0.5, face = "italic", color = "grey", vjust = 5,family="Archivo Black"),
         plot.caption = element_text(size = 12.5, color = "grey", vjust = 0,family="Archivo Black"),
  ) 
  
  anim <- gfb +
    transition_states(DATEMM, state_length = 0) + 
    view_follow(
      fixed_y = c(0, NA),
      fixed_x = TRUE,
    )+
    enter_grow() +
    exit_shrink() +
        ease_aes('linear') +
    labs( title = "Goal scored this season", 
          subtitle = 'Month: {closest_state}', 
          caption = 'Source: Hedgeflare calculation') 
anim
  animate(anim, fps = 10, duration =07, width = 500, height = 1000, renderer = gifski_renderer("gganim1.gif"))

Thanks a lot for your insight.


Solution

  • Here's a simplified adaptation. Today I learned from the help for ?zoo::yearmon that it is numeric data with a small added factor (I'm guessing to avoid floating point errors).

    The "yearmon" class is used to represent monthly data. Internally it holds the data as year plus 0 for January, 1/12 for February, 2/12 for March and so on in order that its internal representation is the same as ts class with frequency = 12. If x is not in this format it is rounded via floor(12*x + .0001)/12.

    This becomes relevant if we try to interpolate between yearmon values, since we need our new values to align to the existing ones in the data.

    Here, I prep the data to make each step in yearmon into 50 steps, with a linear interpolation of rank and goals for each. I also figure out how many frames that represents, so the output will align to those. (ie avoiding skipped or doubled frames)

    fp_interp = 50
    df3 <- df2 |>
      # Note -- ?yearmon help notes that months are stored numerically 
      #   as floor(12*x + .0001)/12. We need to align to those for interpolation
      mutate(DATEMM_num = as.numeric(floor(12*DATEMM + .0001) / 12)) |>
      complete(DATEMM_num = seq(min(DATEMM_num), max(DATEMM_num), (1/12)/fp_interp),
               nesting(League, image_file, Season)) |>
      arrange(League, DATEMM_num) |>
      mutate(across(c(goals,rank), 
                    ~approx(x = DATEMM_num, y = .x, xout = DATEMM_num)$y, 
                    .names = "{.col}_interp"),
             .by = League)
    
    nframes = df3 |>
      distinct(DATEMM_num) |>
      nrow()
    

    Then I can use the interpolated data for a simpler version of your plot.

    animate(ggplot(df3, aes(y = rank_interp,group=League,fill=factor(League,levels = colpal.order),
                         color = as.factor(League)))+
      geom_tile(aes(x=goals_interp/2,y=rank_interp,width=goals_interp,height = 0.5),alpha = 0.8, color = NA) +
      geom_text(aes(x=goals_interp, label= ifelse(goals_interp > 0, sprintf("%1.0f", goals_interp), "")),
                color="gray70",fontface = "bold", size = 8,vjust = 0.5, hjust = 0)+
      geom_text(aes(x=-10, label= League), hjust = 1) +
      scale_fill_manual(values = colpal, guide = "none")+
      scale_color_manual(values = colpal, guide = "none") +
      scale_x_continuous(labels = scales::comma) +
      scale_y_reverse() +
      coord_cartesian(clip = "off") +
      theme_void() +
      theme(plot.margin = unit(c(0,1,0,4), "cm")) +
      transition_states(DATEMM_num, state_length = 0) + 
      labs(subtitle = "Month: {paste(
                month.abb[(as.numeric(closest_state) %% 1)*12 + 1],
                floor(as.numeric(closest_state)))}") +
      view_follow(
        fixed_x = c(0, NA),
        fixed_y = TRUE,
      )+
      enter_grow() +
      exit_shrink() +
      ease_aes('linear'),
      renderer = gifski_renderer("gganim1.gif"), nframes = nframes, fps = 25, height = 300)
    

    enter image description here