Summer 2025 Wildfires in Europe: Where Did It Burn?

EFFIS rapid perimeters, 1 June – 30 September 2025

Author

Pierre Beaucoral

Published

September 9, 2025

I had been wanting to understand Europe’s wildfire season better for a while. In 2025 it hit closer to home: I am French, and the fires in Aude stuck with me, along with the big outbreaks in Spain. Rather than skim headlines, I wanted a grounded view of where things burned, how much, when, and what actually burned.

So I built a small, reproducible dataset and a handful of maps and charts for summer 2025 across Europe. This page is the result, and it is the analysis the rest of this site grew out of. The 2026 tracker applies the same method to the season now underway.

NoteWhat this page is, and what changed since it was first published

This post was originally written in September 2025 against a local snapshot of EFFIS perimeters, and it defined summer as June to August. It has since been rebuilt on the project’s shared pipeline: the same dated EFFIS snapshots, country tagging, and equal-area geometry that produce every other page here. Two things changed as a result, and both are improvements:

  • The season window is now 1 June to 30 September, matching the tracker. September 2025 was quiet, so this adds little area, but it makes 2025 directly comparable with every other year on the site.
  • The perimeters are the current, revised ones. EFFIS corrects its rapid mapping over time, and sharper Sentinel-2 imagery now maps far smaller fires than the older MODIS-era product did. Totals here are therefore higher than in the original September 2025 version, and a few of that version’s country-level comparisons no longer hold. Where that happens, the text below reflects what the current data shows, not what the original said.
NoteThree things to know about these fire outlines
  • Most mapped fires are small, not big. The original version of this post repeated the common line that EFFIS maps fires of roughly 30 hectares and up. On the current data that is not true: most 2025 summer perimeters are under 30 hectares (the exact figure is below). The very smallest fires are still missed, so totals stay below official statistics, but the “30-hectare floor” framing is wrong and I have dropped it.
  • The outlines are rapid estimates, revised as better imagery arrives.
  • I compare burned area, not fire counts. The detection system changed between 2023 and 2024 and suddenly saw many more small fires, so counting fires across years is misleading; measuring area is much safer.
Show code
# -------------------- Packages --------------------
library(sf)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)
library(lubridate)
library(forcats)
library(ggplot2)
library(scales)
library(janitor)
library(rnaturalearth)
library(ggimage)
library(ggrepel)
library(slider)
library(maptiles)
library(terra)
library(tidyterra)
library(ggspatial)
library(gganimate)

# GDAL caps single GeoJSON features at ~200 MB; the 2020 snapshot has one
# giant multipolygon above that (documented in scripts/fetch_effis.R).
Sys.setenv(OGR_GEOJSON_MAX_OBJ_SIZE = "0")

# -------------------- Shared helper library --------------------
source(file.path("R", "helpers.R"))   # to_num, parse_date_any, lab_si_ha
source(file.path("R", "geo.R"))       # Europe polygons, read_effis, tagging
source(file.path("R", "cache.R"))     # cached()
source(file.path("R", "theme.R"))     # pal_lc, burns_brand, theme_burns()
source(file.path("R", "flags.R"))     # flag_table()
source(file.path("R", "pipeline.R"))  # season assembly (all cached)
source(file.path("scripts", "latest_snapshot.R"))

# -------------------- Season parameters --------------------
YEAR_POST          <- 2025L
SEASON_START_MONTH <- 6L    # season window: 1 Jun -
SEASON_END_MONTH   <- 9L    # 30 Sep
# Benchmark years kept from the original post: 2017 as the historical
# reference season, 2022 and 2023 as the other recent crisis years.
COMPARE_YEARS <- c(2017L, 2022L, 2023L, YEAR_POST)

snap <- latest_snapshot()
stopifnot(!is.na(snap))
eu <- get_eu()

# -------------------- Core objects (all disk-cached) --------------------
ba_2025 <- get_tagged_summer(
  YEAR_POST, snap, eu,
  start_month = SEASON_START_MONTH, end_month = SEASON_END_MONTH
)

# English month labels: lubridate's label = TRUE is locale-dependent and would
# render "juin"/"août" on a French machine, so build the factor explicitly.
month_levels <- month.name[SEASON_START_MONTH:SEASON_END_MONTH]
ba_2025 <- ba_2025 |>
  mutate(month = factor(month.name[month(ba_date)], levels = month_levels))

flat_2025 <- ba_2025 |> st_drop_geometry()

# -------------------- Headline numbers (all used inline below) -----------
season_ha    <- sum(flat_2025$area_ha, na.rm = TRUE)
season_n     <- nrow(flat_2025)
pct_under_30 <- 100 * mean(flat_2025$area_ha < 30, na.rm = TRUE)
median_ha    <- median(flat_2025$area_ha, na.rm = TRUE)

by_month <- flat_2025 |>
  group_by(month) |>
  summarise(ha = sum(area_ha, na.rm = TRUE), n = n(), .groups = "drop")
aug_share <- 100 * by_month$ha[by_month$month == "August"] / season_ha

# -------------------- Land-cover machinery ------------------------------
lc_cols <- c("broadlea", "conifer", "mixed", "scleroph", "transit",
             "agriareas", "artifsurf", "othernatlc", "otherlc")
lc_labels <- c(
  broadlea = "Broad-leaved forest", conifer = "Coniferous forest",
  mixed = "Mixed forest", scleroph = "Sclerophyllous veg.",
  transit = "Transitional woodland-shrub", agriareas = "Agricultural areas",
  artifsurf = "Artificial surfaces", othernatlc = "Other natural LC",
  otherlc = "Other LC"
)

# -------------------- Figure captions (referenced via !expr) ------------
snap_lab <- basename(snap)

cap_landcover <- sprintf(
  paste(
    "Land-cover composition of burned area in the top-10 countries by total",
    "burned area, 1 June – 30 September 2025. Each bar is one country; segment",
    "widths are that country's burned hectares in each EFFIS land-cover class,",
    "expressed as a share of its 2025 total. Read it as \"what kind of ground",
    "burned here\", not \"how much burned here\". Classes are EFFIS's own",
    "per-perimeter percentages, weighted by each perimeter's Europe-clipped",
    "area. Source: EFFIS rapid perimeters, snapshot %s; geometry in EPSG:3035."
  ),
  snap_lab
)
cap_hero <- sprintf(
  paste(
    "Burn-scar perimeters mapped by EFFIS between 1 June and 30 September 2025,",
    "clipped to Europe and drawn as outlines on national borders. Outlines",
    "rather than fills, because at this scale filled polygons of very different",
    "sizes read as a single smear. Every visible mark is one mapped fire.",
    "Source: EFFIS rapid perimeters, snapshot %s; geometry in EPSG:3035."
  ),
  snap_lab
)
cap_facet <- sprintf(
  paste(
    "The same 2025 perimeters split by month of the season, on Esri World",
    "Imagery. Panel labels report the number of mapped perimeters and the",
    "Europe-clipped burned area for that month. The panels share one map",
    "extent and one scale, so panel-to-panel differences are real differences",
    "in burned area, not differences in zoom. Source: EFFIS rapid perimeters,",
    "snapshot %s; basemap Esri World Imagery."
  ),
  snap_lab
)
cap_daily <- sprintf(
  paste(
    "Daily count of newly mapped perimeters, 1 June – 30 September 2025. Grey",
    "bars: perimeters first mapped that day. Orange line: 7-day centred mean.",
    "Blue line: cumulative count since 1 June, read on the right axis. The",
    "three busiest days are labelled. This chart counts perimeters, not",
    "hectares, so a day with many small fires can outrank a day with one huge",
    "one. Source: EFFIS rapid perimeters, snapshot %s."
  ),
  snap_lab
)
cap_anim_daily <- sprintf(
  paste(
    "Animated version of the daily-count chart, revealed left to right through",
    "the season. Bars: perimeters mapped per day; line: 7-day centred mean.",
    "Source: EFFIS rapid perimeters, snapshot %s."
  ),
  snap_lab
)
cap_anim_new <- sprintf(
  paste(
    "Day-by-day animation of newly mapped perimeters: each frame shows only the",
    "fires first mapped on that date, so the map goes dark on quiet days.",
    "Geometry simplified to ~2 km for rendering speed, which is why small",
    "perimeters look coarser here than on the static maps. Source: EFFIS rapid",
    "perimeters, snapshot %s."
  ),
  snap_lab
)
cap_anim_cum <- sprintf(
  paste(
    "The same animation with the season accumulating: each frame adds that",
    "day's perimeters to a faded trail of everything mapped before it, so the",
    "final frame is the whole season. Geometry simplified to ~2 km for",
    "rendering speed. Source: EFFIS rapid perimeters, snapshot %s."
  ),
  snap_lab
)
cap_countries <- sprintf(
  paste(
    "Top-10 countries by Europe-clipped burned area, 1 June – 30 September",
    "2025. Bar length is total burned hectares; the coloured dot at each bar's",
    "end and the flag to its right identify the country. Countries are tagged",
    "by maximum overlap of each perimeter with national borders. Source: EFFIS",
    "rapid perimeters, snapshot %s; areas from geometry in EPSG:3035."
  ),
  snap_lab
)
cap_dumbbell <- sprintf(
  paste(
    "Burned area in summer 2025 versus summer 2017, for the 20 countries with",
    "the most burned area in 2025. Each row is one country: the small dot is",
    "its 2017 total, the large dot its 2025 total, and the connecting line the",
    "change between them. A line running left to right means 2025 was worse",
    "than 2017. Both years use the identical 1 June – 30 September window.",
    "Source: EFFIS rapid perimeters, snapshot %s; areas from geometry in",
    "EPSG:3035."
  ),
  snap_lab
)
cap_multiyear <- sprintf(
  paste(
    "Summer burned area across four crisis seasons (2017, 2022, 2023, 2025)",
    "for the ten countries worst hit in 2025, ordered by their 2025 total.",
    "Grouped bars share one axis, so heights are directly comparable across",
    "both years and countries; the 2025 bar is highlighted and its value",
    "labelled. All four years use the identical 1 June – 30 September window.",
    "Source: EFFIS rapid perimeters, snapshot %s; areas from geometry in",
    "EPSG:3035."
  ),
  snap_lab
)

Two numbers set the scene. Between 1 June and 30 September 2025, EFFIS mapped about 1,025,000 hectares of burned land in Europe, across 4,348 separate perimeters. That is roughly 98 times the area of the city of Paris. And it was overwhelmingly an August season: that single month accounts for 70% of the total.

The size distribution is worth pausing on, because it contradicts what the first version of this post claimed. 63% of the mapped perimeters are smaller than 30 hectares, and the median perimeter is about 15 hectares. EFFIS is not a 30-hectare-and-up product any more. What remains true is that the hectares are concentrated in a handful of very large fires, which is what the rest of this page is really about.

What has burned: land-cover composition

Show code
present_lc <- intersect(lc_cols, names(flat_2025))

lc_shares <- flat_2025 |>
  mutate(across(all_of(present_lc), \(x) to_num(x) / 100)) |>
  mutate(across(all_of(present_lc), \(x) x * area_ha, .names = "{.col}_ha")) |>
  group_by(name_long) |>
  summarise(
    across(c(area_ha, ends_with("_ha")), \(x) sum(x, na.rm = TRUE)),
    .groups = "drop"
  )

top10_lc <- lc_shares |> slice_max(area_ha, n = 10) |> pull(name_long)

comp_long <- lc_shares |>
  filter(name_long %in% top10_lc) |>
  mutate(across(ends_with("_ha") & !matches("^area_ha$"), \(x) x / area_ha)) |>
  select(name_long, area_ha, ends_with("_ha"), -area_ha) |>
  pivot_longer(-name_long, names_to = "class", values_to = "share") |>
  mutate(
    class = lc_labels[sub("_ha$", "", class)],
    class = factor(class, levels = names(pal_lc))
  ) |>
  left_join(lc_shares |> select(name_long, total_ha = area_ha), by = "name_long") |>
  mutate(name_long = fct_reorder(name_long, total_ha))

ggplot(comp_long, aes(x = share, y = name_long, fill = class)) +
  geom_col(width = 0.72, color = NA) +
  scale_fill_manual(values = pal_lc, breaks = names(pal_lc), name = "Land cover") +
  scale_x_continuous(labels = label_percent(), expand = expansion(mult = c(0, 0.02))) +
  labs(x = "Share of the country's burned area", y = NULL) +
  theme_burns(base_size = 12) +
  theme(legend.position = "right")

Land-cover composition of burned area in the top-10 countries by total burned area, 1 June – 30 September 2025. Each bar is one country; segment widths are that country’s burned hectares in each EFFIS land-cover class, expressed as a share of its 2025 total. Read it as “what kind of ground burned here”, not “how much burned here”. Classes are EFFIS’s own per-perimeter percentages, weighted by each perimeter’s Europe-clipped area. Source: EFFIS rapid perimeters, snapshot 2026-07-27; geometry in EPSG:3035.

Land-cover composition of burned area in the top-10 countries by total burned area, 1 June – 30 September 2025. Each bar is one country; segment widths are that country’s burned hectares in each EFFIS land-cover class, expressed as a share of its 2025 total. Read it as “what kind of ground burned here”, not “how much burned here”. Classes are EFFIS’s own per-perimeter percentages, weighted by each perimeter’s Europe-clipped area. Source: EFFIS rapid perimeters, snapshot 2026-07-27; geometry in EPSG:3035.
  • Broad-leaved forest: deciduous trees (oaks, beeches).
  • Coniferous forest: needle-leaf trees (pines, spruces, firs), including plantations.
  • Mixed forest: a mixture of broad-leaved and coniferous stands.
  • Sclerophyllous vegetation: fire-adapted Mediterranean shrubland (maquis, garrigue); evergreen, waxy leaves.
  • Transitional woodland-shrub: open, regenerating or degraded woodland, shrub mosaics, young stands.
  • Agricultural areas: arable land, permanent crops, pastures.
  • Artificial surfaces: urban and built-up areas, infrastructure.
  • Other natural LC: natural cover not in the above (heath, sparse vegetation, dunes, rock).
  • Other LC: residual, unknown, or rare classes.

No single land cover dominates. Several types burn together, which is the point: these are not tidy forest fires but mixed landscapes of scrub, forest, and abandoned or marginal farmland burning as one.

Geographic distribution

It is reasonable to expect these events to concentrate in Southern Europe, with Spain and Portugal especially exposed, and France increasingly so.

Show code
ggplot() +
  geom_sf(data = eu$poly, fill = "grey95", color = "grey70", linewidth = 0.15) +
  geom_sf(data = ba_2025, fill = NA, color = burns_brand$ember,
          linewidth = 0.2, alpha = 0.7) +
  coord_sf() +
  theme_burns(base_size = 11, map = TRUE)

Burn-scar perimeters mapped by EFFIS between 1 June and 30 September 2025, clipped to Europe and drawn as outlines on national borders. Outlines rather than fills, because at this scale filled polygons of very different sizes read as a single smear. Every visible mark is one mapped fire. Source: EFFIS rapid perimeters, snapshot 2026-07-27; geometry in EPSG:3035.

Burn-scar perimeters mapped by EFFIS between 1 June and 30 September 2025, clipped to Europe and drawn as outlines on national borders. Outlines rather than fills, because at this scale filled polygons of very different sizes read as a single smear. Every visible mark is one mapped fire. Source: EFFIS rapid perimeters, snapshot 2026-07-27; geometry in EPSG:3035.

The spatial distribution confirms that wildfires are far from evenly spread across Europe. They cluster heavily in Southern and Eastern Europe, with striking densities in Portugal, Spain, southern Italy, and the Balkans. These regions are recurrent hotspots, shaped by Mediterranean climatic stressors, land-use legacies such as agricultural abandonment, and the structural vulnerability of the wildland–urban interface.

France, particularly the south, also shows notable scars, reinforcing the idea that wildfire risk is no longer confined to Iberia and the Mediterranean islands. By contrast, Central and Northern Europe show only scattered events.

This pattern supports the view that Southern and Eastern Europe are systematically more exposed to large fires, for climatic and socio-environmental reasons alike. At the same time, the spread into France and across the Balkans suggests wildfire vulnerability is becoming a pan-European concern rather than one limited to the Mediterranean fringe.

When did it burn?

Fire risk might be expected to peak late in summer, once the heat has dragged on for weeks, water is short, and the landscape is at its driest.

Month by month

Show code
monthly_stats <- flat_2025 |>
  group_by(month) |>
  summarise(n_fires = n(), burned_ha = sum(area_ha, na.rm = TRUE), .groups = "drop") |>
  complete(month = factor(month_levels, levels = month_levels),
           fill = list(n_fires = 0, burned_ha = 0)) |>
  mutate(lab = paste0("Fires: ", comma(n_fires), "\nArea: ", lab_si_ha(burned_ha)))

# Satellite basemap, cached on disk after the first fetch.
bb_ll <- st_bbox(st_transform(eu$poly, 4326))
sat <- maptiles::get_tiles(
  x = st_as_sfc(bb_ll), provider = "Esri.WorldImagery",
  zoom = 4, cachedir = file.path("DATA", "cache")
)
sat_3035 <- terra::project(sat, "EPSG:3035")

bb <- st_bbox(eu$poly)
stats_pos <- monthly_stats |>
  mutate(x = bb["xmin"] + 0.03 * (bb["xmax"] - bb["xmin"]),
         y = bb["ymax"] - 0.03 * (bb["ymax"] - bb["ymin"]))

ggplot() +
  tidyterra::geom_spatraster_rgb(data = sat_3035, maxcell = 3e6, interpolate = TRUE) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf,
           fill = alpha("white", 0.06), colour = NA) +
  geom_sf(data = eu$poly, fill = NA, color = "white", linewidth = 0.18, alpha = 0.9) +
  geom_sf(data = ba_2025, fill = burns_brand$ember, alpha = 0.35,
          color = burns_brand$ember, linewidth = 0.06, inherit.aes = FALSE) +
  facet_wrap(~ month, ncol = 4, drop = FALSE) +
  coord_sf(expand = FALSE,
           xlim = c(bb["xmin"], bb["xmax"]), ylim = c(bb["ymin"], bb["ymax"])) +
  geom_label(data = stats_pos, aes(x = x, y = y, label = lab), inherit.aes = FALSE,
             size = 3.1, label.size = 0, label.padding = grid::unit(3, "pt"),
             fill = alpha("white", 0.85), colour = "grey20") +
  ggspatial::annotation_scale(location = "bl", width_hint = 0.25,
                              text_cex = 0.6, line_width = 0.2) +
  theme_burns(base_size = 11, map = TRUE) +
  theme(panel.spacing = grid::unit(8, "pt"),
        strip.text = element_text(face = "bold"),
        legend.position = "none")

The same 2025 perimeters split by month of the season, on Esri World Imagery. Panel labels report the number of mapped perimeters and the Europe-clipped burned area for that month. The panels share one map extent and one scale, so panel-to-panel differences are real differences in burned area, not differences in zoom. Source: EFFIS rapid perimeters, snapshot 2026-07-27; basemap Esri World Imagery.

The same 2025 perimeters split by month of the season, on Esri World Imagery. Panel labels report the number of mapped perimeters and the Europe-clipped burned area for that month. The panels share one map extent and one scale, so panel-to-panel differences are real differences in burned area, not differences in zoom. Source: EFFIS rapid perimeters, snapshot 2026-07-27; basemap Esri World Imagery.

August is not merely the worst month, it is most of the season: 70% of the year’s burned area. This supports the idea that the longer ecosystems endure sustained summer heat, the more vulnerable they become. Late summer is the tipping point: vegetation is driest, soils are depleted, and firefighting resources are stretched thin after months of activity.

Day by day

Show code
season_start <- as.Date(sprintf("%d-%02d-01", YEAR_POST, SEASON_START_MONTH))
season_end   <- as.Date(sprintf("%d-%02d-%02d", YEAR_POST, SEASON_END_MONTH,
                                days_in_month(as.Date(sprintf("%d-%02d-01", YEAR_POST,
                                                              SEASON_END_MONTH)))))

daily <- flat_2025 |>
  count(ba_date, name = "fires") |>
  complete(ba_date = seq(season_start, season_end, by = "day"),
           fill = list(fires = 0)) |>
  arrange(ba_date) |>
  mutate(roll7 = slider::slide_dbl(fires, mean, .before = 3, .after = 3, .complete = FALSE),
         cum_fires = cumsum(fires))

month_bands <- daily |>
  mutate(m = floor_date(ba_date, "month")) |>
  group_by(m) |>
  summarise(xmin = min(ba_date), xmax = max(ba_date) + 1, .groups = "drop") |>
  mutate(even = row_number() %% 2 == 0)

max_daily <- max(daily$fires, na.rm = TRUE)
sfactor   <- max(daily$cum_fires, na.rm = TRUE) / (max_daily * 1.15)
daily     <- daily |> mutate(cum_scaled = cum_fires / sfactor)
peaks     <- daily |> slice_max(fires, n = 3, with_ties = FALSE)

ggplot(daily, aes(x = ba_date)) +
  geom_rect(data = month_bands,
            aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = even),
            inherit.aes = FALSE, alpha = 0.28, show.legend = FALSE) +
  scale_fill_manual(values = c("TRUE" = "white", "FALSE" = "grey98")) +
  geom_col(aes(y = fires), width = 0.9, fill = "grey85",
           color = "grey80", linewidth = 0.15) +
  geom_ribbon(aes(ymin = pmax(0, roll7 - 0.5), ymax = roll7 + 0.5),
              fill = alpha(burns_brand$ember, 0.12), color = NA) +
  geom_line(aes(y = roll7), linewidth = 0.9, color = burns_brand$ember) +
  geom_line(aes(y = cum_scaled), linewidth = 0.8, color = burns_brand$reburn_blue) +
  ggrepel::geom_text_repel(data = peaks, aes(y = fires, label = comma(fires)),
                           nudge_y = 4, size = 3.5, color = "grey20",
                           segment.color = "grey70", seed = 1,
                           box.padding = 0.2, min.segment.length = 0) +
  scale_y_continuous(name = "Perimeters per day",
                     sec.axis = sec_axis(~ . * sfactor,
                                         name = "Cumulative since 1 June")) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b") +
  labs(x = NULL) +
  theme_burns(base_size = 12) +
  theme(panel.grid.major.x = element_blank(),
        axis.text.x = element_text(margin = margin(t = 4)))

Daily count of newly mapped perimeters, 1 June – 30 September 2025. Grey bars: perimeters first mapped that day. Orange line: 7-day centred mean. Blue line: cumulative count since 1 June, read on the right axis. The three busiest days are labelled. This chart counts perimeters, not hectares, so a day with many small fires can outrank a day with one huge one. Source: EFFIS rapid perimeters, snapshot 2026-07-27.

Daily count of newly mapped perimeters, 1 June – 30 September 2025. Grey bars: perimeters first mapped that day. Orange line: 7-day centred mean. Blue line: cumulative count since 1 June, read on the right axis. The three busiest days are labelled. This chart counts perimeters, not hectares, so a day with many small fires can outrank a day with one huge one. Source: EFFIS rapid perimeters, snapshot 2026-07-27.

Wildfire activity in 2025 did not escalate smoothly. It arrived in waves: a first surge in early July, a second in late July, and a third, far more intense, in early August. The pattern points to an interaction between cumulative vulnerability, as ecosystems dried out under sustained heat, and short-term triggers such as heatwaves and wind episodes. Each wave left the landscape more fragile, so that by early August conditions were primed for the largest outbreaks. Late summer remains the most dangerous period, but the timing of the peaks depends heavily on weather extremes layered on a background of progressive drying.

The season in motion

The charts above compress four months into one frame. These animations play the season back at its own pace, which makes the August cluster much harder to miss.

Show code
p_daily_anim <- ggplot(daily, aes(x = ba_date)) +
  geom_col(aes(y = fires), fill = "grey85", color = "grey80", linewidth = 0.15) +
  geom_line(aes(y = roll7), linewidth = 0.8, color = burns_brand$ember) +
  scale_x_date(date_breaks = "2 weeks", date_labels = "%d %b") +
  labs(x = NULL, y = "Perimeters per day") +
  theme_burns(base_size = 12) +
  transition_reveal(along = ba_date)

# Two non-obvious requirements for every animation on this page:
#
# 1. units = "px" is mandatory. gganimate::animate() forwards width/height to
#    the device, which defaults to INCHES -- omitting units asks for a 900-inch
#    frame (~99,000 px at res = 110), ~95 MB per frame, and never finishes.
#
# 2. The GIF must be written to an explicit path. The project renders with
#    execute-dir: project, so knitr's own figure path for a gganimate GIF
#    resolves against the project root and the file lands in ./2025_files/
#    instead of posts/2025_files/, leaving a broken image in the HTML. Writing
#    to ANIM_DIR and referencing it by a site-absolute path below sidesteps
#    that entirely.
ANIM_DIR <- file.path("assets", "anim")
dir.create(ANIM_DIR, recursive = TRUE, showWarnings = FALSE)

anim_daily <- animate(p_daily_anim, nframes = 100, fps = 12, width = 900, height = 500,
        units = "px", res = 110,
        renderer = gifski_renderer(file.path(ANIM_DIR, "daily-counts.gif")))
Animated bar chart of daily wildfire perimeter counts across summer 2025, revealed left to right.
Figure 1: Animated version of the daily-count chart, revealed left to right through the season. Bars: perimeters mapped per day; line: 7-day centred mean. Source: EFFIS rapid perimeters, snapshot 2026-07-27.
Show code
# Simplify geometry before animating: the season has thousands of perimeters
# and every frame re-draws them, so full-resolution outlines make the render
# time explode for no visible gain at continental zoom.
# clean_polygons() is required, not cosmetic: a handful of perimeters come back
# from the simplify/intersect chain as GEOMETRYCOLLECTIONs, and transformr
# (which tweens the sf layer between frames) aborts on those with
# "Unsupported geometry type". clean_polygons() keeps only the polygonal part.
ba_anim <- ba_2025 |> select(ba_date, area_ha)
st_geometry(ba_anim) <- clean_polygons(
  st_simplify(st_geometry(ba_anim), dTolerance = 2000, preserveTopology = TRUE)
)
ba_anim <- ba_anim |>
  filter(!st_is_empty(geometry)) |>
  mutate(gid = row_number())

bb_anim <- st_bbox(ba_anim)

p_map_new <- ggplot() +
  geom_sf(data = eu$poly, inherit.aes = FALSE,
          fill = "grey95", color = "grey70", linewidth = 0.2) +
  geom_sf(data = ba_anim, aes(group = gid), fill = burns_brand$ember,
          alpha = 0.55, color = burns_brand$ember, linewidth = 0.05,
          inherit.aes = FALSE) +
  coord_sf(xlim = c(bb_anim["xmin"], bb_anim["xmax"]),
           ylim = c(bb_anim["ymin"], bb_anim["ymax"]), expand = FALSE) +
  labs(subtitle = "New perimeters mapped on {closest_state}") +
  theme_burns(base_size = 12, map = TRUE) +
  transition_states(ba_date, transition_length = 0, state_length = 1, wrap = FALSE) +
  enter_fade() + exit_fade() + ease_aes("linear")

anim_map_new <- animate(p_map_new, nframes = length(unique(ba_anim$ba_date)), fps = 10,
        width = 800, height = 700, units = "px", res = 110,
        renderer = gifski_renderer(file.path(ANIM_DIR, "map-new.gif")))
Animated map of Europe showing only the wildfire perimeters newly mapped on each day of summer 2025.
Figure 2: Day-by-day animation of newly mapped perimeters: each frame shows only the fires first mapped on that date, so the map goes dark on quiet days. Geometry simplified to ~2 km for rendering speed, which is why small perimeters look coarser here than on the static maps. Source: EFFIS rapid perimeters, snapshot 2026-07-27.
Show code
p_map_cum <- ggplot() +
  geom_sf(data = eu$poly, inherit.aes = FALSE,
          fill = "grey95", color = "grey70", linewidth = 0.2) +
  geom_sf(data = ba_anim, aes(group = gid), fill = burns_brand$ember,
          alpha = 0.55, color = burns_brand$ember, linewidth = 0.05,
          inherit.aes = FALSE) +
  coord_sf(xlim = c(bb_anim["xmin"], bb_anim["xmax"]),
           ylim = c(bb_anim["ymin"], bb_anim["ymax"]), expand = FALSE) +
  labs(subtitle = "Season to date, as of {closest_state}") +
  theme_burns(base_size = 12, map = TRUE) +
  transition_states(ba_date, transition_length = 0, state_length = 1, wrap = FALSE) +
  shadow_mark(past = TRUE, future = FALSE, alpha = 0.15, size = 0.02) +
  enter_fade() + exit_fade() + ease_aes("linear")

anim_map_cum <- animate(p_map_cum, nframes = length(unique(ba_anim$ba_date)), fps = 10,
        width = 800, height = 700, units = "px", res = 110,
        renderer = gifski_renderer(file.path(ANIM_DIR, "map-cumulative.gif")))
Animated map of Europe accumulating wildfire perimeters day by day through summer 2025, ending with the full season.
Figure 3: The same animation with the season accumulating: each frame adds that day’s perimeters to a faded trail of everything mapped before it, so the final frame is the whole season. Geometry simplified to ~2 km for rendering speed. Source: EFFIS rapid perimeters, snapshot 2026-07-27.

How 2025 compares with earlier seasons

To put 2025 in perspective, start with which countries were worst hit, then place the season against 2017, long treated as the benchmark for a catastrophic European fire year, and finally against 2022 and 2023, the other recent crisis seasons. Together they show whether 2025 was an anomaly or part of a pattern.

Worst-hit countries in 2025

Show code
ctry_2025 <- flat_2025 |>
  group_by(name_long) |>
  summarise(burned_ha = sum(area_ha, na.rm = TRUE), n_fires = n(), .groups = "drop") |>
  slice_max(burned_ha, n = 10) |>
  mutate(name_long = fct_reorder(name_long, burned_ha))

flags_10 <- flag_table(as.character(ctry_2025$name_long), eu$poly)

bars_df <- ctry_2025 |>
  mutate(name_long_chr = as.character(name_long)) |>
  left_join(flags_10, by = c("name_long_chr" = "name_long")) |>
  mutate(col = coalesce(col, "#444444"),
         x_flag = burned_ha + 0.10 * max(burned_ha, na.rm = TRUE))

ggplot(bars_df, aes(x = burned_ha, y = name_long)) +
  geom_col(fill = "grey90") +
  geom_point(aes(color = col), size = 3) +
  scale_color_identity() +
  ggimage::geom_image(data = filter(bars_df, !is.na(flag_path)),
                      aes(x = x_flag, y = name_long, image = flag_path),
                      inherit.aes = FALSE, size = 0.065, asp = 2/3) +
  scale_x_continuous(labels = lab_si_ha, expand = expansion(mult = c(0, 0.30))) +
  labs(x = "Burned area (ha)", y = NULL) +
  theme_burns(base_size = 11) +
  theme(plot.margin = margin(5.5, 90, 10, 5.5, "pt")) +
  coord_cartesian(clip = "off")

Top-10 countries by Europe-clipped burned area, 1 June – 30 September 2025. Bar length is total burned hectares; the coloured dot at each bar’s end and the flag to its right identify the country. Countries are tagged by maximum overlap of each perimeter with national borders. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

Top-10 countries by Europe-clipped burned area, 1 June – 30 September 2025. Bar length is total burned hectares; the coloured dot at each bar’s end and the flag to its right identify the country. Countries are tagged by maximum overlap of each perimeter with national borders. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

Spain and Portugal, as expected, and by a wide margin: between them they account for most of the European total. France comes 11th, with about 17,900 hectares. The fires that made the French news were real and locally severe, but on a continental scale 2025 was an Iberian season.

2025 against the 2017 benchmark

Show code
ctry_multi <- map_dfr(COMPARE_YEARS, function(y) {
  get_tagged_summer(y, snap, eu, SEASON_START_MONTH, SEASON_END_MONTH) |>
    st_drop_geometry() |>
    group_by(name_long) |>
    summarise(burned_ha = sum(area_ha, na.rm = TRUE), .groups = "drop") |>
    mutate(year = y)
})

dd <- ctry_multi |>
  filter(year %in% c(2017L, YEAR_POST)) |>
  pivot_wider(names_from = year, values_from = burned_ha, values_fill = 0) |>
  slice_max(`2025`, n = 20) |>
  mutate(name_long = fct_reorder(name_long, `2025`))

flags_20 <- flag_table(as.character(dd$name_long), eu$poly)
dd_plot <- dd |>
  mutate(name_long_chr = as.character(name_long)) |>
  left_join(flags_20, by = c("name_long_chr" = "name_long")) |>
  mutate(col = coalesce(col, "#444444"))

max_x  <- max(c(dd_plot$`2017`, dd_plot$`2025`), na.rm = TRUE)
x_left <- -0.25 * max_x
x_name <- -0.05 * max_x

ggplot(dd_plot) +
  geom_segment(aes(x = `2017`, xend = `2025`, y = name_long, yend = name_long,
                   colour = col),
               linewidth = 0.9, alpha = 0.9, lineend = "round", show.legend = FALSE) +
  geom_point(aes(x = `2017`, y = name_long, colour = col), size = 2.4, show.legend = FALSE) +
  geom_point(aes(x = `2025`, y = name_long, colour = col), size = 3.1, show.legend = FALSE) +
  ggimage::geom_image(aes(y = name_long, x = x_left, image = flag_path),
                      size = 0.1, asp = 2/3, inherit.aes = FALSE, na.rm = TRUE) +
  geom_text(aes(y = name_long, x = x_name, label = name_long),
            size = 3.7, hjust = 1, inherit.aes = FALSE) +
  scale_colour_identity() +
  scale_x_continuous(labels = lab_si_ha, expand = expansion(mult = c(0, 0.03))) +
  labs(x = "Burned area (ha)", y = NULL) +
  theme_burns(base_size = 12) +
  theme(axis.text.y = element_blank(), axis.ticks.y = element_blank(),
        plot.margin = margin(5.5, 5.5, 10, 110, "pt")) +
  coord_cartesian(xlim = c(-0.18 * max_x, NA), clip = "off")

Burned area in summer 2025 versus summer 2017, for the 20 countries with the most burned area in 2025. Each row is one country: the small dot is its 2017 total, the large dot its 2025 total, and the connecting line the change between them. A line running left to right means 2025 was worse than 2017. Both years use the identical 1 June – 30 September window. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

Burned area in summer 2025 versus summer 2017, for the 20 countries with the most burned area in 2025. Each row is one country: the small dot is its 2017 total, the large dot its 2025 total, and the connecting line the change between them. A line running left to right means 2025 was worse than 2017. Both years use the identical 1 June – 30 September window. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

This is where the rebuilt data departs most sharply from the original version of this post. On the current perimeters, Spain is the dominant case: about 388,300 hectares in 2025 against roughly 58,500 in 2017, a sixfold increase. The first version of this post described Spain as having “already suffered catastrophic losses in 2017”; on the revised EFFIS record its 2017 summer was comparatively moderate, and 2025 is the outlier.

Portugal is the other correction. The original text had Portugal recording much smaller losses in 2025 than in its emblematic 2017 crisis. The current data shows the opposite: about 272,000 hectares in 2025 against 231,800 in 2017. Portugal did not escape 2025; it had a worse summer than the one that reshaped its fire policy.

Italy still burned more in 2017 than in 2025 (122,600 against 79,500 hectares), and several Balkan countries appear in both years, confirming the region’s structural exposure. France, not a major outlier in 2017, sits higher in 2025.

Overall, the Mediterranean South remains the epicentre, but the relative weight of countries within it shifts markedly from year to year. That instability, more than any single ranking, is the durable finding.

2025 against 2017, 2022, and 2023

Show code
top10_multi <- ctry_multi |>
  filter(year == YEAR_POST) |>
  slice_max(burned_ha, n = 10) |>
  pull(name_long)

order_2025 <- ctry_multi |>
  filter(year == YEAR_POST, name_long %in% top10_multi) |>
  arrange(desc(burned_ha)) |>
  pull(name_long)

df_plot <- ctry_multi |>
  filter(name_long %in% top10_multi) |>
  mutate(name_long = fct_relevel(name_long, rev(order_2025)),
         year = factor(year))

max_x <- max(df_plot$burned_ha, na.rm = TRUE)
layout_df <- df_plot |>
  group_by(name_long) |>
  summarise(x_max = max(burned_ha, na.rm = TRUE), .groups = "drop") |>
  left_join(df_plot |> filter(year == as.character(YEAR_POST)) |>
              select(name_long, val_cur = burned_ha), by = "name_long") |>
  mutate(name_long_chr = as.character(name_long)) |>
  left_join(flag_table(as.character(unique(df_plot$name_long)), eu$poly),
            by = c("name_long_chr" = "name_long")) |>
  mutate(x_label   = x_max + 0.10 * max_x,
         x_flag    = x_label + 0.06 * max_x,
         x_country = x_flag  + 0.03 * max_x,
         label_txt = lab_si_ha(val_cur))

col_map <- c(`2017` = "grey80", `2022` = "grey65", `2023` = "grey55",
             `2025` = burns_brand$ember)

ggplot(df_plot, aes(y = name_long, x = burned_ha, fill = year)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.65) +
  scale_fill_manual(values = col_map, name = "Year") +
  scale_x_continuous(labels = lab_si_ha, expand = expansion(mult = c(0, 0.55))) +
  geom_text(data = layout_df, aes(y = name_long, x = x_label, label = label_txt),
            inherit.aes = FALSE, hjust = 1, vjust = 0.5, size = 3.7,
            colour = col_map[["2025"]]) +
  ggimage::geom_image(data = filter(layout_df, !is.na(flag_path)),
                      aes(y = name_long, x = x_flag, image = flag_path),
                      inherit.aes = FALSE, size = 0.07, asp = 2/3) +
  geom_text(data = layout_df, aes(y = name_long, x = x_country, label = name_long),
            inherit.aes = FALSE, hjust = 0, vjust = 0.5, size = 4) +
  labs(x = "Burned area (ha)", y = NULL) +
  theme_burns(base_size = 13) +
  theme(legend.position = "right",
        axis.text.y = element_blank(), axis.ticks.y = element_blank(),
        plot.margin = margin(5.5, 140, 14, 5.5, "pt")) +
  coord_cartesian(clip = "off")

Summer burned area across four crisis seasons (2017, 2022, 2023, 2025) for the ten countries worst hit in 2025, ordered by their 2025 total. Grouped bars share one axis, so heights are directly comparable across both years and countries; the 2025 bar is highlighted and its value labelled. All four years use the identical 1 June – 30 September window. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

Summer burned area across four crisis seasons (2017, 2022, 2023, 2025) for the ten countries worst hit in 2025, ordered by their 2025 total. Grouped bars share one axis, so heights are directly comparable across both years and countries; the 2025 bar is highlighted and its value labelled. All four years use the identical 1 June – 30 September window. Source: EFFIS rapid perimeters, snapshot 2026-07-27; areas from geometry in EPSG:3035.

This situates 2025 within a broader multi-year picture, against the other recent crisis years.

Spain dominates: its 2025 burned area clearly surpasses its own earlier peaks and every other national total in the comparison. Portugal, the emblematic case of 2017, records its highest figure of the four years in 2025. Greece stands out differently: its worst year here is 2023, driven overwhelmingly by the Dadia fire in Évros, which alone burned close to 96,000 hectares and remains the largest single perimeter in the European record.

What is striking is not only the magnitude of 2025 for Iberia, but the geographic rotation of high-impact years. Different countries surface at different times: Portugal in 2017, Greece in 2023, Spain and Portugal together in 2025. Extreme fire seasons are not confined to one geography; they move within the Mediterranean and Balkan regions depending on where the heat, wind, and drought line up.

By setting 2025 alongside 2017, 2022, and 2023, the comparison suggests 2025 was not an isolated anomaly but part of a recurring pattern of severe pan-European fire years, each with its own epicentre.

Notes and caveats

The figures and maps here are based on EFFIS rapid perimeter data, which relies on satellite detection and automated mapping. This gives timely, consistent, continent-wide coverage, but it differs from official national fire statistics, which use different thresholds, definitions of burned area, and reporting criteria. Treat the numbers as estimates, not exact totals.

A note on revision: EFFIS corrects its rapid mapping as better imagery arrives, so these figures are a snapshot (2026-07-27) rather than a final accounting. This is also why the numbers here differ from the version of this post published in September 2025.

On temporal framing, this analysis defines the season as 1 June to 30 September. The original version used June to August. The wider window improves comparability with the rest of this site and captures early-autumn outbreaks, which matter in some regions, but fire agencies use their own seasonal boundaries and totals will differ accordingly.

Country attribution uses maximum spatial overlap with national borders, which can under- or over-attribute in border areas. Areas are computed directly from perimeter geometry in an equal-area projection (EPSG:3035).

Finally, these results are for analytical and exploratory purposes. They offer a comparative overview of European wildfire dynamics but are not official statistics. For policy, management, or operational decisions, consult authoritative national and European reporting.