Introduction

In this document, we will use the fitzroy package to get the ladder positions of each team, after each round, in the 2025 AFL season. We will then create an animated plot showing how each team’s position changed across the season.

# Install packages if required

#install.packages("fitzRoy")
#install.packages("dplyr")
#install.packages("ggplot2")
#install.packages("gganimate")
#install.packages("gifski")

# Open packages ready for data collection and visualisation
library(fitzRoy)
library(dplyr)
library(ggplot2)
library(gganimate)
library(gifski)

Load in the ladder from the 2025 AFL season

Ladder_2025 <- fetch_ladder_afltables(season = 2025)
## No round number specified, trying to return most recent ladder for specified
## season
Ladder_2025
## # A tibble: 18 × 8
##    Season Team     Round.Number Season.Points Score.For Score.Against Percentage
##     <dbl> <chr>           <int>         <dbl>     <dbl>         <dbl>      <dbl>
##  1   2025 Adelaide           25            72      2278          1635      1.39 
##  2   2025 Geelong            25            68      2425          1714      1.41 
##  3   2025 Brisban…           25            66      2061          1804      1.14 
##  4   2025 Colling…           25            64      1991          1627      1.22 
##  5   2025 GWS                25            64      2114          1834      1.15 
##  6   2025 Fremant…           25            64      1978          1815      1.09 
##  7   2025 Gold Co…           25            60      2173          1740      1.25 
##  8   2025 Hawthorn           25            60      2045          1691      1.21 
##  9   2025 Footscr…           25            56      2493          1820      1.37 
## 10   2025 Sydney             25            48      1845          1902      0.970
## 11   2025 Carlton            25            36      1799          1861      0.967
## 12   2025 St Kilda           25            36      1839          2077      0.885
## 13   2025 Port Ad…           25            36      1705          2136      0.798
## 14   2025 Melbour…           25            28      1902          2038      0.933
## 15   2025 Essendon           25            24      1535          2209      0.695
## 16   2025 North M…           25            22      1805          2365      0.763
## 17   2025 Richmond           25            20      1449          2197      0.660
## 18   2025 West Co…           25             4      1466          2438      0.601
## # ℹ 1 more variable: Ladder.Position <int>

Note this is the final ladder, from the end of the season.

We want the ladder position after every round. To do this we will need to create a for loop.

# Set max round number for the 2025 season
maxround <- 25

# Create an empty list to store ladder results from each round
ladder_by_round <- list()

# Loop through each round of the season and collect the ladder data
for (i in 1:maxround){
  # Temporarily store ladder for round i
  ladder <- fetch_ladder_afltables(season = 2025, round = i)
  
  # Add ladder for each round into empty list
  ladder_by_round[[i]] <- ladder
}

# Combine all rounds into one dataframe
ladder_all_rounds <- bind_rows(ladder_by_round)

# Rename Western Bulldogs
ladder_all_rounds <- ladder_all_rounds %>%
  mutate(Team = recode(Team, "Footscray" = "Western Bulldogs"))

# Check dataframe head and tail to ensure all rounds have worked
head(ladder_all_rounds)
## # A tibble: 6 × 8
##   Season Team      Round.Number Season.Points Score.For Score.Against Percentage
##    <dbl> <chr>            <int>         <dbl>     <dbl>         <dbl>      <dbl>
## 1   2025 Gold Coa…            1             4       153            58      2.64 
## 2   2025 GWS                  1             4       104            52      2    
## 3   2025 Hawthorn             1             4        96            76      1.26 
## 4   2025 Brisbane…            1             4        70            61      1.15 
## 5   2025 Geelong              1             0        61            70      0.871
## 6   2025 Sydney               1             0        76            96      0.792
## # ℹ 1 more variable: Ladder.Position <int>
tail(ladder_all_rounds)
## # A tibble: 6 × 8
##   Season Team      Round.Number Season.Points Score.For Score.Against Percentage
##    <dbl> <chr>            <int>         <dbl>     <dbl>         <dbl>      <dbl>
## 1   2025 Port Ade…           25            36      1705          2136      0.798
## 2   2025 Melbourne           25            28      1902          2038      0.933
## 3   2025 Essendon            25            24      1535          2209      0.695
## 4   2025 North Me…           25            22      1805          2365      0.763
## 5   2025 Richmond            25            20      1449          2197      0.660
## 6   2025 West Coa…           25             4      1466          2438      0.601
## # ℹ 1 more variable: Ladder.Position <int>

Only require Round number, Team name and ladder position so the dataframe can be simplified

ladder_all_rounds <- ladder_all_rounds %>% 
  select(Round.Number, Team, Ladder.Position)

head(ladder_all_rounds)
## # A tibble: 6 × 3
##   Round.Number Team           Ladder.Position
##          <int> <chr>                    <int>
## 1            1 Gold Coast                   1
## 2            1 GWS                          2
## 3            1 Hawthorn                     3
## 4            1 Brisbane Lions               4
## 5            1 Geelong                      5
## 6            1 Sydney                       6

Plot a single round

library(ggplot2)
library(dplyr)

# Filter to Round 2, (Round 1 only had 4 games)
ladder_round2 <- ladder_all_rounds %>%
  filter(Round.Number == 2) %>%
  mutate(Ladder.Position = as.numeric(Ladder.Position))

# Vertical ladder plot
ggplot(ladder_round2, aes(x = 2, y = Ladder.Position, color = Team)) +
  geom_point(size = 4) +               # dot for each team
  scale_y_reverse(breaks = 1:18) +     # 1 at top
  labs(
    title = "AFL 2025 Ladder - Round 2",
    x = "Round 2",
    y = "Ladder Position",
    color = "Team"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_blank(),        # For single round dont need full x axis
    axis.ticks.x = element_blank(),
    legend.key.size = unit(0.5, "cm"),    # shrink legend boxes
    legend.text = element_text(size = 8)  # smaller legend text
  )

Hard to identify team easily just from a range of colours, so we will add an abbreviation to each team’s circle.

library(ggplot2)
library(dplyr)

# Add team abbreviation to round 2 to add the the plot
ladder_round2 <- ladder_round2 %>%
  mutate(TeamAbbr = case_when(
    Team == "St Kilda" ~ "STK",
    Team == "West Coast" ~ "WCE",
    Team == "Western Bulldogs" ~ "WBD",
    TRUE ~ substr(Team, 1, 3)  # Every other team just select first 3 letters
  ))


# Vertical ladder plot with labels
ggplot(ladder_round2, aes(x = 2, y = Ladder.Position, color = Team)) +
  geom_point(size = 5) +               # larger dots to fit text
  geom_text(aes(label = TeamAbbr), color = "black", size = 2) + # label inside dot
  scale_y_reverse(breaks = 1:18) +     # 1 at top
  labs(
    title = "AFL 2025 Ladder - Round 2",
    x = "Round 2",
    y = "Ladder Position",
    color = "Team"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_blank(),        # For single round dont need full x axis
    axis.ticks.x = element_blank(),
    legend.key.size = unit(0.5, "cm"),    # shrink legend boxes
    legend.text = element_text(size = 8)  # smaller legend text
  )

Plot ladder position across the whole season

library(ggplot2)
library(dplyr)

# Make sure Ladder.Position is numeric
ladder_all_rounds <- ladder_all_rounds %>%
  mutate(Ladder.Position = as.numeric(Ladder.Position))

# Add team abbreviation
ladder_all_rounds <- ladder_all_rounds %>%
  group_by(Team) %>% 
  mutate(TeamAbbr = case_when(
    Team == "St Kilda" ~ "STK",
    Team == "West Coast" ~ "WCE",
    Team == "Western Bulldogs" ~ "WBD",
    TRUE ~ substr(Team, 1, 3)  # Every other team just select first 3 letters
  ))

# Ladder position vs round plot
ladder_plot <- ggplot(ladder_all_rounds, aes(x = Round.Number, y = Ladder.Position, group = Team, color = Team)) +
  geom_line(size = 1, alpha = 0.7) +  # lines showing ladder progression
  geom_point(size = 4) +              # dots at each round
  geom_text(aes(label = TeamAbbr), color = "black", size = 2) + # label inside dot
  
  scale_y_reverse(limits = c(18, 1), # 1 is top, 18 is bottom
                  breaks = 1:18, 
                  expand = expansion(add = c(1, 1))) +  # x-axis spacing, needed for animation to fit
  
  scale_x_continuous(breaks = 1:max(ladder_all_rounds$Round.Number)) +
  labs(
    title = "AFL 2025 Ladder Progression",
    x = "Round",
    y = "Ladder Position",
  ) +
  theme_minimal(base_size = 10) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "none",                 
    plot.margin = unit(c(1, 1, 2, 1), "cm"))  # extra space around plot for animation

ladder_plot

Animation

As we have the round by round ladder for each team, we can turn this plot into an animation


# Only run this in RStudio

# Animate round-by-round
animated_ladder <- ladder_plot +
  transition_reveal(Round.Number) +
  ease_aes("linear")

# Save the animation as a .gif file
anim_save(
  filename = "ladder_anim.gif",
  animation = animated_ladder,
  fps = 8,                   
  width = 800,               
  height = 600,               
  res = 150,  
  renderer = gifski_renderer(loop = TRUE),
  end_pause = 20
)
# Display the saved file
knitr::include_graphics("ladder_anim.gif")