Milano Cortina 2026 Winter Olympics

A heatmap using {ggplot2}, {paletteer} and {ggtext}

#TidyTuesday
Author

Aditya Dahiya

Published

February 5, 2026

About the Data

The 2026 Winter Olympics dataset contains comprehensive scheduling information for all 1,866 Olympic events taking place in Milan-Cortina, Italy. Curated by Daniel Chen from Posit PBC and the University of British Columbia, this dataset is part of the weekly #TidyTuesday social data project in the R for Data Science online learning community. The data includes both competition and training sessions across various winter sport disciplines, with detailed information about start and end times in both local and UTC timezones, venue details, and metadata indicating whether events award medals. Participants can access the dataset using R with the tidytuesdayR package, Python with pandas, or Julia, and are encouraged to create visualizations, models, Quarto reports, Shiny apps, or Quarto dashboards to explore questions about event distribution, scheduling patterns, and venue utilization. The complete dataset and example code are available in the GitHub repository, by Daniel Chen.

Figure 1: This calendar heatmap shows the daily distribution of medal events at the Milano Cortina 2026 Winter Olympics. Each tile represents a competition day, with deeper shades indicating a higher number of medal events. The schedule reveals how medal intensity builds across the Games, highlighting peak competition days and quieter transitions, offering a clear visual rhythm of the Olympic fortnight.

How I Made This Graphic

Loading required libraries

Code
pacman::p_load(
  tidyverse, # All things tidy

  scales, # Nice Scales for ggplot2
  fontawesome, # Icons display in ggplot2
  ggtext, # Markdown text support for ggplot2
  showtext, # Display fonts in ggplot2
  colorspace, # Lighten and Darken colours
  sf, # Spatial Features

  patchwork,  # Composing Plots
  packcircles, # for hierarchichal packing circles
  colorspace, # Modify and play with colours, extract dominant colours
  magick  # Playing with images
)

schedule <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-02-10/schedule.csv')

Visualization Parameters

Code
# Font for titles
font_add_google("Saira",
  family = "title_font"
)

# Font for the caption
font_add_google("Saira Condensed",
  family = "body_font"
)

# Font for plot text
font_add_google("Saira Extra Condensed",
  family = "caption_font"
)

showtext_auto()

# A base Colour
bg_col <- "white"
seecolor::print_color(bg_col)

# Colour for highlighted text
text_hil <- "grey20"
seecolor::print_color(text_hil)

# Colour for the text
text_col <- "grey10"
seecolor::print_color(text_col)

# Define Base Text Size
bts <- 120

# Caption stuff for the plot
sysfonts::font_add(
  family = "Font Awesome 6 Brands",
  regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf")
)
github <- "&#xf09b"
github_username <- "aditya-dahiya"
xtwitter <- "&#xe61b"
xtwitter_username <- "@adityadahiyaias"
social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_hil}'>{github_username}  </span>")
social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")
plot_caption <- paste0(
  "**Data:**  Daniel Chen",
  "   |  **Code:** ",
  social_caption_1,
  " |  **Graphics:** ",
  social_caption_2
)
rm(
  github, github_username, xtwitter,
  xtwitter_username, social_caption_1,
  social_caption_2
)

plot_title <- "tidy_winter_olympics"

plot_subtitle <- "tidy_winter_olympics" |> 
  str_wrap(110)

Exploratory Data Analysis and Wrangling

Code
schedule |> 
  summarytools::dfSummary() |> 
  summarytools::view()

# Prepare calendar heatmap data
calendar_data <- schedule |> 
  mutate(
    date = date(end_datetime_local)
  ) |> 
  group_by(discipline_name, date) |> 
  summarise(
    medal_events = sum(is_medal_event, na.rm = TRUE),
    medal_events = as.numeric(medal_events),
    .groups = "drop"
  )

The Plot

Code
# Define Base Text Size for the plot
bts <- 90

# Plot true calendar heatmap
g <- calendar_data |> 
  ggplot(
    mapping = aes(
      x = date, 
      y = discipline_name, 
      fill = medal_events
      )
  ) +
  
  geom_tile(
    color = bg_col, 
    linewidth = 0.4
    ) +
  
  paletteer::scale_fill_paletteer_c(
    "grDevices::Reds 2",
    direction = -1,
    breaks = seq(0, 8, 2)
  ) +
  
  labs(
    title = "Milano Cortina 2026 Winter Olympics",
    subtitle = "Calendar Heatmap of Daily Medal Events",
    x = NULL, y = NULL,
    caption = plot_caption,
    fill = "Number of Medal Events"
  ) +
  
  scale_x_date(
    date_breaks = "2 days",
    date_labels = "%d %b",
    expand = c(0, 0)
  ) +
  theme_minimal(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    text = element_text(
      colour = text_hil, 
      margin = margin(0,0,0,0, "mm")
    ),
    legend.position = "bottom",
    legend.margin = margin(-20,0,0,0, "mm"),
    legend.title = element_text(
      margin = margin(0,5,2,0, "mm")
    ),
    legend.text = element_text(
      margin = margin(2,0,0,0, "mm")
    ),
    legend.key.width = unit(30, "mm"),
    
    # Axis ticks
    axis.ticks.x.bottom = element_line(
      linewidth = 0.3,
      colour = text_hil
    ),
    axis.ticks.y.left = element_blank(),
    axis.ticks.length.x.bottom = unit(4, "mm"),
    axis.ticks.length.y.left = unit(0, "mm"),
    
    # Grid
    panel.grid = element_blank(),
    
    # Axis lines
    axis.line.x = element_line(
      arrow = arrow(
        length = unit(5, "mm")
      ),
      linewidth = 0.5,
      colour = text_hil
    ),
    axis.line.y = element_blank(),
    
    # Text elements
    plot.title = element_text(
      margin = margin(10, 0, 5, 0, "mm"),
      hjust = 0.5,
      size = bts * 2.3,
      face = "bold",
      colour = text_hil,
      lineheight = 0.3
    ),
    plot.subtitle = element_text(
      margin = margin(0, 0, 18, 0, "mm"),
      hjust = 0.5,
      size = bts * 1.5,
      colour = text_hil,
      lineheight = 0.35
    ),
    plot.caption = element_textbox(
      hjust = 0.5,
      family = "caption_font",
      size = bts * 0.8,
      colour = text_hil,
      lineheight = 0.4,
      margin = margin(5, 0, 0, 0, "mm")
    ),
    
    # Axis text - reduced margins to minimize white space
    axis.text.x.bottom = element_text(
      margin = margin(2, 0, 0, 0, "mm"),
      lineheight = 0.3,
      colour = text_col,
      angle = 90,
      hjust = 1,
      vjust = 0.5,
      size = bts * 1.1
    ),
    axis.text.y = element_text(
      margin = margin(0, 2, 0, 0, "mm"),
      lineheight = 0.3,
      colour = text_col
    ),
    
    # Axis titles - reduced margins
    axis.title.x = element_blank(),
    axis.title.y = element_blank(),
    
    # Plot background
    plot.background = element_rect(
      fill = bg_col, 
      colour = NA
    ),
    
    # Plot margins - reduced to minimize white space
    plot.margin = margin(5, 5, 5, 5, "mm"),
    
    plot.title.position = "plot",
    plot.caption.position = "plot"
  )

# Save the plot
ggsave(
  filename = here::here(
    "data_vizs",
    "tidy_winter_olympics.png"
  ),
  plot = g,
  width = 400,
  height = 500,
  units = "mm",
  bg = bg_col
)

Savings the thumbnail for the webpage

Code
# Saving a thumbnail

library(magick)

# Saving a thumbnail for the webpage
image_read(
  here::here(
    "data_vizs",
    "tidy_winter_olympics.png"
    )
  ) |>
  image_resize(geometry = "x400") |>
  image_write(
    here::here(
      "data_vizs",
      "thumbnails",
      "tidy_winter_olympics.png"
    )
  )

Session Info

Code
pacman::p_load(
  tidyverse, # All things tidy

  scales, # Nice Scales for ggplot2
  fontawesome, # Icons display in ggplot2
  ggtext, # Markdown text support for ggplot2
  showtext, # Display fonts in ggplot2
  colorspace # Lighten and Darken colours
)

sessioninfo::session_info()$packages |>
  as_tibble() |>
  
  # The attached column is TRUE for packages that were 
  # explicitly loaded with library()
  dplyr::filter(attached == TRUE) |>
  dplyr::select(package,
    version = loadedversion,
    date, source
  ) |>
  dplyr::arrange(package) |>
  janitor::clean_names(
    case = "title"
  ) |>
  gt::gt() |>
  gt::opt_interactive(
    use_search = TRUE
  ) |>
  gtExtras::gt_theme_espn()
Table 1: R Packages and their versions used in the creation of this page and graphics

Links