Reported Canadian Wildfires

January to June 2025

data visualization
Author

A. Srikanth

Published

August 12, 2025

By the Numbers

This photograph was taken during my first visit to Jasper, Alberta, in June of this year.

I had heard about the wildfires before, but I quickly learned that hearing about them and seeing them are not the same thing.

Standing on the edge of a burned hillside made it real. Blackened trees stood like outlines against the sky. The ground was bare where I imagined there had once been lush greenery. The quiet felt heavy, as if the land was still holding its breath.

I took a few photos to remember the strength that was still there in the surrounding space and communities.

Back home, I obtained wildfire data for Canada covering the first half of 2025 from the Canadian Interagency Forest Fire Centre (CIFFC), which is the national agency responsible for collecting and coordinating wildfire information across jurisdictions. It detailed hectares burned by province and categorized fires by cause, size, and location. One hectare is 10,000 square metres, which works out to about four NBA courts side by side. Once I pictured it that way, the scale hit differently. Thousands of hectares meant thousands of packed arenas worth of hardwood. Millions meant enough courts to cover entire cities.

Mapping Its Footprint

The first visual is a map of every recorded wildfire in Alberta during the first half of 2025. Each circle marks a point where something irreversible happened. Standing in Jasper, I only saw one small part of it. Here, I can see how that moment fits into a much larger picture. I work with data often, but it’s rare to be able to place myself inside it like this.


Code
# ===============
# 1) Libraries and parameters
# ===============
suppressPackageStartupMessages({
  library(readr)
  library(dplyr)
  library(scales)
  library(plotly)
  library(htmltools)
})

bg_col      <- "#FAF8F1"
point_color <- "#751F2C"
font_family <- "Ramabhadra"

# Alberta rough bounding box (lat 49–60, lon -120–-110)
ab_lat_range <- c(49, 60.1)
ab_lon_range <- c(-120.1, -109.4)

# ===============
# 2) Load data from CSV (path for website deployment)
# ===============
fires <- read_csv(
  "data/CanadianWildfires_AlbertaMapping.csv",
  col_types = cols(
    size = col_double(),
    lat  = col_double(),
    long = col_double()
  )
)

# ===============
# 3) Clean, filter to Alberta, scale symbol sizes
# ===============
fires_ab <- fires %>%
  filter(
    !is.na(lat), !is.na(long),
    lat  >= ab_lat_range[1], lat  <= ab_lat_range[2],
    long >= ab_lon_range[1], long <= ab_lon_range[2]
  ) %>%
  mutate(
    size_plot = rescale(sqrt(pmax(size, 0)), to = c(4, 18)),
    hover = paste0(
      "<span style='color:#CC6F77;'><b>", toupper(cause), " FIRE</b></span><br>",
      "EST. SIZE: ", scales::comma(round(size, 2)), " ha<br>",
      "<span style='color:slategray;'>", "LATITUDE: ", round(lat, 4), " | LONGITUDE: ", round(long, 4), "</span>"
    )
  )

# ===============
# 4) Plotly scattergeo (no token required)
# ===============
fires_ab <- fires_ab %>%
  mutate(size_plot = rescale(sqrt(pmax(size, 0)), to = c(6, 22)))  # was c(4, 18)

fig <- plot_ly(
  fires_ab,
  type = "scattergeo",
  mode = "markers",
  lat  = ~lat,
  lon  = ~long,
  text = ~hover,
  hoverinfo = "text",
  marker = list(
    size = ~size_plot,
    color = point_color,
    line = list(width = 0),
    opacity = 0.9,
    sizemode = "diameter",
    sizemin = 4
  )
) %>%
  layout(
    autosize = TRUE,
    font = list(family = font_family, size = 14),
    hoverlabel = list(
      font = list(family = font_family, size = 14, color = "#313131"),
      bgcolor = "#FAF8F1",
      namelength = -1
    ),
    hovermode = "closest",
    paper_bgcolor = bg_col,
    plot_bgcolor  = bg_col,
    margin = list(l = 6, r = 6, t = 6, b = 6),
    geo = list(
      scope = "north america",
      lataxis = list(range = ab_lat_range),
      lonaxis = list(range = ab_lon_range),
      showland = TRUE,
      landcolor = "#FDEBED",
      subunitcolor = "#cccccc",
      countrycolor = "#cccccc",
      showsubunits = TRUE,
      showlakes = TRUE,
      lakecolor = "#e6f2ff",
      showcoastlines = TRUE,
      coastlinecolor = "slategray",
      coastlinewidth = 0.8,
      resolution = 50,
      fitbounds = "locations"
    ),
    dragmode = "pan"
  ) %>%
  config(
    responsive = TRUE,
    scrollZoom = FALSE,
    doubleClick = TRUE,
    displayModeBar = TRUE,
    displaylogo = FALSE,
    modeBarButtonsToRemove = list(
      "zoom2d", "pan2d", "select2d", "lasso2d",
      "zoomIn2d", "zoomOut2d",
      "toggleSpikelines", "toImage"
    )
  )

browsable(
  tagList(
    tags$style(HTML("
      #ab-map {
        border-radius: 6px;
        overflow: hidden;
        background: #FAF8F1;
        padding: 0.75rem;
        width: 100%;
        max-width: 790px;
        margin: 0 auto;
      }
      /* tighten padding & hover text on very small screens */
      @media (max-width: 790px) {
        #ab-map { padding: 0.5rem; }
      }
    ")),
    tags$div(id = "ab-map", fig)
  )
)


Understanding the Causes

The horizontal bar chart shifts the focus to cause: natural, human, or undetermined. The labels are simple, but each carries a complex story that can influence how we prevent the next fire. They matter to those who live on this land, and to those watching from afar.


Code
# ===============
# 1) Libraries and parameters
# ===============
suppressPackageStartupMessages({
  library(readr)
  library(dplyr)
  library(tidyr)
  library(plotly)
  library(htmltools)
  library(scales)
})

yr <- 2025
bg_col <- "#FAF8F1"
font_family <- "Ramabhadra"
show_modebar <- "hover"

cause_cols <- c(
  human        = "#CC6F77", # wine
  natural      = "#ECA8AD", # teal
  undetermined = "slategray" # soft gray
)

# ===============
# 2) Load data from CSV (path for website deployment)
# ===============
cause_wf <- read_csv(
  "data/CanadianWildfires_FireCause.csv",
  col_types = cols(
    .prov        = col_character(),
    human        = col_double(),
    natural      = col_double(),
    undetermined = col_double()
  )
)

# ===============
# 3) Light validation and normalization
# ===============
plot_df <- cause_wf %>%
  replace_na(list(human = 0, natural = 0, undetermined = 0)) %>%
  mutate(.prov = toupper(trimws(.prov)),
         total = human + natural + undetermined) %>%
  pivot_longer(c(human, natural, undetermined),
               names_to = "cause", values_to = "count") %>%
  mutate(
    cause = factor(cause, levels = c("human", "natural", "undetermined"))
  )

prov_order <- plot_df %>%
  distinct(.prov, total) %>%
  arrange(desc(total)) %>%
  pull(.prov)

plot_df <- plot_df %>%
  mutate(.prov = factor(.prov, levels = rev(prov_order)))

# ===============
# 4) Plotly stacked horizontal bar chart
# ===============
plot_df <- plot_df %>%
  mutate(
    cause_label = factor(
      toupper(as.character(cause)),
      levels = toupper(c("human","natural","undetermined"))
    )
  )

x_max    <- max(plot_df$count, na.rm = TRUE)
left_pad <- x_max * 0.045

cause_cols <- c(
  HUMAN = "#CC6F77",
  NATURAL = "#ECA8AD",
  UNDETERMINED = "slategray"
)

plot_df <- plot_df %>%
  mutate(
    prov_label = paste0("<span style='color:#751F2C;'><b>", name, " (", .prov, ")", "</b></span>"),
    hover_text = paste0(
      scales::comma(count), "<span style='color:slategray'> ", cause_label, " FIRES</b></span>"
    )
  )

fig <- plot_ly(
  plot_df,
  x = ~count,
  y = ~prov_label,
  color = ~cause_label,
  colors = cause_cols,
  type = "bar",
  orientation = "h",
  hovertemplate = "%{customdata}<extra></extra>",
  customdata = ~hover_text,
  marker = list(line = list(width = 0))
) %>%
  layout(
    barmode = "stack",
    font = list(family = font_family, size = 12),
    paper_bgcolor = bg_col,
    plot_bgcolor  = bg_col,
    height = 390,
    margin = list(l = 18, r = 18, t = 36, b = 36),
    legend = list(orientation = "v", x = 1.05, y = 1, xanchor = "left", yanchor = "top",
                  font = list(size = 14), title = list(text = "")),
    xaxis = list(
      title = list(text = "NUMBER OF WILDFIRES", standoff = 20),
      range = c(-left_pad, x_max * 1.05),
      tickmode = "linear",
      tick0 = 0,
      dtick = diff(pretty(c(0, x_max), n = 5))[1],
      tickformat = ",d",
      gridcolor = "#E8E8E8",
      fixedrange = TRUE
    ),
    yaxis = list(
      title = list(text = "PROVINCE", standoff = 20),
      tickfont = list(size = 16),
      automargin = TRUE,
      fixedrange = TRUE
    ),
    uniformtext = list(minsize = 14)
  ) %>%
  layout(
    hovermode = "y unified",
    hoverlabel = list(align = "left"),
    dragmode = FALSE
  ) %>%
  config(
    scrollZoom = FALSE,
    doubleClick = FALSE,
    modeBarButtonsToRemove = list(
      "zoom2d","pan2d","select2d","lasso2d",
      "zoomIn2d","zoomOut2d","autoScale2d","resetScale2d",
      "toggleSpikelines","toImage"
    ),
    displaylogo = FALSE,
    displayModeBar = TRUE
  )

browsable(
  tagList(
    tags$style(HTML("
      #causebars {
        border-radius: 6px;
        overflow: hidden;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        width: 100%;
        max-width: 790px;
        margin: 0 auto;
        background: #FAF8F1;
        padding: 1rem;
      }
      #causebars > div { min-width: 790px; }
    ")),
    tags$div(id = "causebars", fig)
  )
)


Grasping the Scale

The treemap is about scale. Each province’s space reflects how much was lost, and Saskatchewan’s share is impossible to miss. I try to picture that much burned land, but all I see are fragments. I’ve built charts like this before, in other contexts, but this feels different. Here, the numbers aren’t just measurements—they’re pieces of the landscape itself.


Code
# ===============
# 1) Libraries and parameters
# ===============
suppressPackageStartupMessages({
  library(readr)
  library(dplyr)
  library(tibble)
  library(plotly)
  library(htmltools)
  library(scales)
})

yr <- 2025
bg_col <- "#FAF8F1"
font_family <- "Ramabhadra"
show_modebar <- "hover"

# ===============
# 2) Load data from CSV (path for website deployment)
# ===============
prov_hect <- read_csv(
  "data/CanadianWildfires_AreaByProvince.csv",
  col_types = cols(
    .prov    = col_character(),
    name     = col_character(),
    region   = col_character(),
    hectares = col_double()
  )
)

# ===============
# 3) Light validation and normalization
# ===============
prov_hect <- prov_hect %>%
  mutate(
    name     = trimws(as.character(name)),
    region   = trimws(as.character(region)),
    hectares = as.numeric(hectares)
  ) %>%
  tidyr::replace_na(list(hectares = 0))

dup_names <- prov_hect$name[duplicated(prov_hect$.prov)]
if (length(dup_names)) {
  stop("Duplicate province/territory names found: ", paste(dup_names, collapse = ", "))
}

region_levels <- c("ATLANTIC", "CENTRAL", "PRAIRIES", "WEST", "NORTH")
prov_hect$region <- factor(prov_hect$region, levels = region_levels)

# ===============
# 4) Build hierarchy: CANADA > REGION > PROVINCE
# ===============
regions  <- prov_hect %>%
  group_by(region, .drop = FALSE) %>%
  summarise(hectares = sum(hectares), .groups = "drop") %>%
  mutate(region = as.character(region))

total_ha <- sum(regions$hectares, na.rm = TRUE)

df_treemap <- bind_rows(
  tibble(labels = "CANADA", parents = "",        values = total_ha),
  tibble(labels = regions$region, parents = "CANADA", values = regions$hectares),
  tibble(labels = prov_hect$name, parents = as.character(prov_hect$region), values = prov_hect$hectares)
)

wrap_label <- function(x, width = 18) {
  words <- strsplit(x, "\\s+")[[1]]
  out <- c(); line <- ""
  for (w in words) {
    nxt <- if (nzchar(line)) paste(line, w) else w
    if (nchar(nxt) > width) { out <- c(out, line); line <- w } else { line <- nxt }
  }
  paste(c(out, line), collapse = "<br>")
}

df_treemap <- df_treemap |>
  dplyr::mutate(
    label_wrapped = dplyr::case_when(
      labels %in% c("CANADA","ATLANTIC","CENTRAL","PRAIRIES","WEST","NORTH") ~ labels,
      TRUE ~ vapply(labels, wrap_label, character(1), width = 12)
    )
  )

# ===============
# 5) Plotly treemap
# ===============
regions_full <- prov_hect %>%
  group_by(region) %>%
  summarise(hectares = sum(hectares), .groups = "drop")

total_ha_full <- sum(regions_full$hectares)

top3_provs <- prov_hect %>%
  arrange(desc(hectares)) %>%
  slice_head(n = 3)

top3_by_region <- prov_hect %>%
  semi_join(top3_provs, by = c(".prov","name","region","hectares")) %>%
  select(region, name, hectares)

other_by_region <- prov_hect %>%
  anti_join(top3_provs, by = c(".prov","name","region","hectares")) %>%
  group_by(region) %>%
  summarise(hectares = sum(hectares), .groups = "drop") %>%
  mutate(name = paste0("<span style='color:#751F2C;'><b>Other (", region, ")</b></span>"))

children <- bind_rows(
  top3_by_region %>% transmute(parent = region, label = name, value = hectares),
  other_by_region %>% filter(hectares > 0) %>% transmute(parent = region, label = name, value = hectares)
)

df_treemap_top3_other_by_reg <- bind_rows(
  tibble(labels = "CANADA", parents = "",        values = total_ha_full),
  tibble(labels = regions_full$region, parents = "CANADA", values = regions_full$hectares),
  tibble(labels = children$label,     parents = children$parent, values = children$value)
) %>%
  mutate(label_wrapped = labels)

custom_colorscale <- list(
  list(0,       "#FFFFFF"), # white
  list(0.125,   "#FEF6F7"), # ultra light pink
  list(0.25,    "#FDEBED"), # very light pink
  list(0.375,   "#FAD9DC"), # pale rose
  list(0.5,     "#F5C2C7"), # muted rose
  list(0.625,   "#ECA8AD"), # warm medium-light
  list(0.75,    "#DF8E94"), # lighter medium
  list(0.875,   "#CC6F77"), # medium
  list(1,       "#B95763")  # softened dark rose
)

fig <- plot_ly(
  df_treemap_top3_other_by_reg,
  type         = "treemap",
  labels       = ~label_wrapped,
  parents      = ~parents,
  values       = ~values,
  branchvalues = "total",
  textinfo     = "none",
  texttemplate = "<span style='color:#751F2C'><b>%{label}</b></span><br>%{value:,.0f} ha<br><span style='color:slategray'>%{percentRoot:.2%}</span>",
  textposition = "top left",
  hoverinfo    = "skip",
  marker = list(
    colors = ~values,
    colorscale = custom_colorscale,
    line = list(width = 0)
  )
) %>%
  layout(
    font = list(family = font_family, size = 16),
    paper_bgcolor = bg_col,
    plot_bgcolor  = bg_col,
    margin = list(l = 18, r = 18, t = 36, b = 36),
    uniformtext = list(minsize = 18),
    width = 790,
    height = 390
  ) %>%
  config(
    modeBarButtonsToRemove = list("toImage"),
    displaylogo = FALSE,
    displayModeBar = show_modebar,
    responsive = FALSE
  )

# ===============
# Rounded, padded container for QMD
# ===============
browsable(
  tagList(
    tags$style(HTML("
      #treemap .slice, 
      #treemap .pathbar { pointer-events: none !important; }
      #treemap .slice path,
      #treemap .pathbar .bgrect,
      #treemap .pathbar path { stroke: none !important; }
      
      #treemap {
        border-radius: 6px;
        overflow: hidden;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        width: 100%;
        max-width: 790px;
        margin: 0 auto;
        background: #FAF8F1; /* matches bg_col */
        padding: 1rem;
      }
      #treemap > div { min-width: 790px; }
    ")),
    tags$div(id = "treemap", fig)
  )
)


Final Word

Looking at these visuals, I’m reminded that data isn’t just something to analyze and report—it’s a way of seeing. The maps and charts don’t replace the memory of standing in Jasper, but they give that moment context. They link a single hillside to an entire country’s story.

What I value most about my work is the chance to take something vast and overwhelming, break it into pieces, and share it in a way that helps someone see it more clearly. The numbers aren’t the whole story, but they can offer a small window into it. Living in Canada, with the chance to witness both its beauty and its fragility, is something I don’t take for granted.