EURO Group Tables and Knockout Performances

data viz ggplot2 football euro 2020

Everybody loves cinderella stories and fairy-tale runs.

July 3, 2021


It all began with a suggestion from my professor, @statsinthewild:

@qntkhvn I think something like this would be cool. And you could go back to past Euros to see what this would look like.

It's possible that THREE teams who finished third in their group will be in the final 8! And no one from group F advanced to final 8!

— Volume Tweeter (@StatsInTheWild) June 29, 2021

So I went back and looked at the group positions and knockout outcomes for the national teams that competed in the 6 most recent UEFA EURO tournaments - 1996, 2000, 2004, 2008, 2012, and 2016. These are competitions with 16 (1996-2012) or 24 (2016) teams, as prior to ’96, the EURO was either an 8- or 4-team tournament. I visited the Wikipedia page of each one those EUROs and quickly collected the desired data. After that, I utilized R packages ggplot2 and plotly to make a simple heatmap of how the national teams performed in past European championships.


As you can see, each observation in the heatmap below is represented by their group name (y-axis), group position (x-axis), tournament result (coded by different color values), and tournament year. The plot is also interactive, so you can actually reveal more information about the teams and their performances by simply moving your cursor inside the plot area. As always, you can find my code below. For this blog post, I’m not going to go over the details of the plotting process, rather I’m going to save it for a future tutorial blog post.

Show code

Team <- c(
  "England", "Netherlands", "Scotland", "Switzerland",
  "France", "Spain", "Bulgaria", "Romania",
  "Germany", "Czech Republic", "Italy", "Russia",
  "Portugal", "Croatia", "Denmark", "Turkey",
  "Portugal", "Romania", "England", "Germany",
  "Italy", "Turkey", "Belgium", "Sweden",
  "Spain", "Yugoslavia", "Norway", "Slovenia",
  "Netherlands", "France", "Czech Republic", "Denmark",
  "Portugal", "Greece", "Spain", "Russia",
  "France", "England", "Croatia", "Switzerland",
  "Sweden", "Denmark", "Italy", "Bulgaria",
  "Czech Republic", "Netherlands", "Germany", "Latvia",
  "Portugal", "Turkey", "Czech Republic", "Switzerland",
  "Croatia", "Germany", "Austria", "Poland",
  "Netherlands", "Italy", "Romania", "France",
  "Spain", "Russia", "Sweden", "Greece",
  "Czech Republic", "Greece", "Russia", "Poland",
  "Germany", "Portugal", "Denmark", "Netherlands",
  "Spain", "Italy", "Croatia", "Ireland",
  "England", "France", "Ukraine", "Sweden",
  "France", "Switzerland", "Albania", "Romania",
  "Wales", "England", "Slovakia", "Russia",
  "Germany", "Poland", "Northern Ireland", "Ukraine",
  "Croatia", "Spain", "Turkey", "Czech Republic",
  "Italy", "Belgium", "Ireland", "Sweden",
  "Hungary", "Iceland", "Portugal", "Austria"

Outcome <- c(
  "SF", "QF", "G", "G", "SF", "QF", "G", "G", "W", "F", "G", "G", "QF", "QF", "G", "G", # 96 
  "SF", "QF", "G", "G", "F", "QF", "G", "G", "QF", "QF", "G", "G", "SF", "W", "G", "G", # 00
  "F", "W", "G", "G", "QF", "QF", "G", "G", "QF", "QF", "G", "G", "SF", "SF", "G", "G", # 04
  "QF", "SF", "G", "G", "QF", "F", "G", "G", "QF", "QF", "G", "G", "W", "SF", "G", "G", # 08
  "QF", "QF", "G", "G", "SF", "SF", "G", "G", "W", "F", "G", "G", "QF", "QF", "G", "G", # 12
  "F", "R16", "G", "G", "SF", "R16", "R16", "G", "SF", "QF", "R16", "G", # 16 
  "R16", "R16", "G", "G", "QF", "QF", "R16", "G", "R16", "QF", "W", "G"

euro <- tibble(
  Year = c(rep(seq(1996, 2012, 4), each = 16), rep(2016, 24)),
  Group = c(rep(rep(c("A", "B", "C", "D"), each = 4), 6), 
            rep(c("E", "F"), each = 4)),
  Place = rep(1:4, 26),
) %>% 
  mutate(Outcome = factor(Outcome, levels = c("G", "R16", "QF", "SF", "F", "W")))

p <- euro %>% 
  mutate(Group = fct_rev(Group)) %>% 
  ggplot(aes(x = Place, y = Group, fill = Outcome, group = Team)) +
  geom_tile(color = "white") +
  facet_wrap(~ Year, scales = "free_y") +
  ggtitle("EURO Performances and Group Positions") +
  scale_fill_brewer(palette = "PRGn") +
  theme_minimal() +
    plot.title = element_text(hjust = 0.5),
    legend.title = element_blank(),
    axis.title = element_blank(),
    panel.grid.minor = element_blank(),
    panel.grid.major = element_blank()

p %>%
  ggplotly() %>%
  layout(legend = list(
    x = 0.25,
    y = -0.1,
    orientation = "h"


There are a couple of things I wanted to highlight here:

What’s Next?

There are many other situations where we could apply this type of plot to. In regard to sports, heatmap is a great visual tool when we have data on rankings, or seedings; for example, grand slam tennis, NCAA March Madness, or even weekly tables for football leagues. I will eventually write up a blog post on how to make heatmaps with ggplot2. Stay tuned!


For attribution, please cite this work as

qntkhvn (2021, July 3). The Q: EURO Group Tables and Knockout Performances. Retrieved from

BibTeX citation

  author = {qntkhvn, },
  title = {The Q: EURO Group Tables and Knockout Performances},
  url = {},
  year = {2021}