---
title: "Summer 2026 Wildfires in Europe: State of the Season"
subtitle: "EFFIS rapid perimeters, as of 3 July 2026"
author: "Pierre Beaucoral"
date: 2026-07-03
execute:
freeze: false
---
Every summer now brings the same headlines out of the Mediterranean, and the
same question underneath them: is this year actually worse, or does it just
feel that way? The 2026 fire season is about four weeks old, and this page is
my attempt at an answer: what has burned so far, where, and how that compares
with recent seasons at the same point in the calendar. It is a **state of the
season**, not a verdict.
A word on the data first. Satellites spot large fires, and Europe's EFFIS
service (part of the Copernicus programme) draws the outline of each burned
area it can map. I work with those outlines. I measure each fire's size
directly from its shape, using a map projection that keeps areas honest, so
hectares are comparable across countries. And I count only the parts that
fall inside Europe. The method is the same as in the [2025 season
analysis](../wildfires-europe-2025-local.html) this project grew out of; that
post remains the reference for the details.
::: {.callout-note title="Three things to know about these fire outlines"}
- **Only fires of roughly 30 to 50 hectares and up get mapped.** Smaller
fires are mostly invisible here, so totals sit below official statistics.
- **The outlines are rapid estimates.** They are revised as better satellite
imagery arrives. Every number on this page will move a little with each
weekly update, and that is by design.
- **I compare burned area, not fire counts.** The detection system changed in
2023 to 2024 and suddenly saw many more small fires. Counting fires across
years is therefore misleading; measuring area is much safer.
:::
```{r}
#| label: setup
# -------------------- Packages --------------------
library(sf)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)
library(lubridate)
library(forcats)
library(ggplot2)
library(scales)
library(patchwork)
library(janitor)
library(rnaturalearth)
library(leaflet)
library(htmlwidgets)
# 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, theme_burns()
source(file.path("R", "flags.R")) # flag_table()
source(file.path("R", "pipeline.R")) # season assembly (all cached)
source(file.path("R", "plots.R")) # plot_envelope() (shared with index)
source(file.path("scripts", "latest_snapshot.R"))
# -------------------- Season parameters --------------------
YEAR_CURRENT <- 2026L
HIST_YEARS <- 2017:2025 # archive effectively starts 2016; 2016 kept out of
# the band (partial-quality first year), see caveats
SEASON_START_MONTH <- 6L # season window: 1 Jun -
SEASON_END_MONTH <- 9L # 30 Sep
# Calendar heatmap DOES include 2016 (the archive's first, partial-quality
# year) -- unlike HIST_YEARS above, since a full-decade grid is the point of
# that chart; 2016's row is captioned as lower-confidence rather than dropped.
CALENDAR_YEARS <- 2016:2025
snap <- latest_snapshot()
stopifnot(!is.na(snap))
eu <- get_eu()
# -------------------- Core objects (all disk-cached) --------------------
envelope <- build_envelope(
hist_years = HIST_YEARS, year_current = YEAR_CURRENT,
snapshot_dir = snap, eu = eu,
start_month = SEASON_START_MONTH, end_month = SEASON_END_MONTH
)
tagged_2026_full <- get_tagged_full_year(YEAR_CURRENT, snap, eu)
as_of <- envelope$meta$as_of_date
as_of_lab <- paste(day(as_of), month.name[month(as_of)]) # English, locale-proof
# Season-window (Jun 1 - as-of) headline numbers
season_ha <- envelope$meta$current_cum_ha
season_n <- envelope$meta$current_n_fires
pct_vs_med <- envelope$meta$pct_vs_median
median_ha <- envelope$meta$median_to_date_ha
# Full-2026-to-date numbers (Jan 1 - as-of), used by maps and rankings
full_ha <- sum(tagged_2026_full$area_ha, na.rm = TRUE)
full_n <- nrow(tagged_2026_full)
# Land-cover machinery shared by several chunks
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) --------------------
cap_envelope <- sprintf(
paste(
"Cumulative burned area since 1 June: 2026 to date against the 2017–2025 range.",
"The shaded band spans the minimum and maximum cumulative trajectory of the nine",
"previous seasons on each day of the season; the dashed line is their median.",
"The 2026 line stops at its last mapped perimeter (%s)."
),
as_of_lab
)
cap_hero <- sprintf(
paste(
"Burn-scar perimeters mapped by EFFIS in 2026 to date (1 January – %s),",
"Europe-clipped. Perimeter outlines drawn on national borders; rapid mapping",
"covers fires of roughly 30–50 ha and above. Source: EFFIS rapid perimeters,",
"snapshot %s; geometry in EPSG:3035."
),
as_of_lab, basename(snap)
)
cap_facet <- sprintf(
paste(
"The same 2026 perimeters split by period: late winter and spring (January–May)",
"versus the summer window so far (1 June – %s). Panel labels report mapped",
"perimeters and Europe-clipped burned area per period. Source: EFFIS rapid",
"perimeters, snapshot %s."
),
as_of_lab, basename(snap)
)
cap_countries <- sprintf(
paste(
"Top-10 countries by Europe-clipped burned area, 1 January – %s 2026.",
"Left: total mapped burned area. Right: the same total as a share of national",
"land area (land area computed from the same reference polygons,",
"mainland-Europe crop). Source: EFFIS rapid perimeters, snapshot %s; areas from",
"geometry in EPSG:3035; countries tagged by maximum overlap."
),
as_of_lab, basename(snap)
)
cap_reburn <- sprintf(
paste(
"2026 perimeters (1 January – %s) split into first-time burns (orange-red) and",
"re-burns of ground already inside a 2017–2025 EFFIS perimeter (blue). Overlap",
"computed on ~100 m-simplified geometry in EPSG:3035. Source: EFFIS rapid",
"perimeters, snapshot %s."
),
as_of_lab, basename(snap)
)
cap_landcover <- sprintf(
paste(
"Land-cover composition of burned area in the 1 June – %s window: 2026 versus",
"the pooled (area-weighted) 2017–2025 average for the same window. Shares are",
"each perimeter's land-cover percentages weighted by its Europe-clipped area.",
"Source: EFFIS rapid perimeters, snapshot %s."
),
as_of_lab, basename(snap)
)
cap_calendar <- sprintf(
paste(
"Weekly Europe-clipped burned area by year, %d–%d, in ISO calendar weeks.",
"Fill on a square-root scale: many weeks burn exactly zero hectares, which a",
"log scale cannot display, and sqrt still compresses the July–August peaks",
"enough to keep smaller months visible. Grey cells are 2026 weeks that have",
"not happened yet; the dashed line marks the last mapped week so far (%s).",
"2016 is the archive's first year and its coverage is less complete than",
"later years. This chart uses burned area, not fire counts, so the",
"2023-to-2024 detection-system change (which makes counts incomparable",
"across years) mostly washes out. Source: EFFIS rapid perimeters,",
"snapshots covering %d–%d; areas from geometry in EPSG:3035, Europe-clipped."
),
min(CALENDAR_YEARS), YEAR_CURRENT, as_of_lab, min(CALENDAR_YEARS), YEAR_CURRENT
)
cap_natura_trend <- sprintf(
paste(
"Share of Europe-clipped burned area falling inside a Natura 2000 protected",
"site, 1 June – %s each year, computed as area-weighted PERCNA2K (EFFIS's own",
"field for the share of each perimeter's own area inside Natura 2000). The",
"window is identical every year, so the series is not distorted by how much",
"of the season had elapsed. 2026 is highlighted. Source: EFFIS rapid",
"perimeters, snapshots covering 2017–%d."
),
as_of_lab, YEAR_CURRENT
)
cap_natura_map <- sprintf(
paste(
"2026 perimeters (1 January – %s) colored by PERCNA2K, the share of each",
"fire's own perimeter that falls inside a Natura 2000 protected site. Grey:",
"0%% (no protected-area overlap). Bright yellow: perimeters that burned",
"almost entirely inside protected land. Source: EFFIS rapid perimeters,",
"snapshot %s."
),
as_of_lab, basename(snap)
)
```
Two numbers set the scene, and they cover different time windows. Since
**1 June**, the start of the summer season, satellites have mapped about
**`r comma(round(season_ha, -3))` hectares** of burned land in Europe, across
`r comma(season_n)` separate fire outlines. That is roughly
`r round(season_ha / 10500)` times the area of the city of Paris, and about
**`r if (is.na(pct_vs_med)) "n/a (too early in season)" else sprintf("%.0f%%", pct_vs_med)`** of what a typical recent season had
burned by this date (`r if (is.na(median_ha)) "n/a" else comma(round(median_ha, -3))` hectares). But 2026 did
not start in June. Counting from **1 January**, the total is already about
**`r comma(round(full_ha, -3))` hectares**, and an unusually busy late winter
and spring drove most of that gap, weeks before most people start watching
for fire season at all. That timing is this year's strangest thread so far,
and the calendar chart further down traces exactly when it happened. The
chart right below sticks to the summer window, so that seasons are compared
like for like; the maps after that show everything mapped in 2026 so far.
# Is this season bad so far?
```{r}
#| label: envelope
#| fig-width: 9.5
#| fig-height: 5.5
#| fig-cap: !expr cap_envelope
plot_envelope(envelope)
```
Read this like a race against the past. The grey band is the space between
the calmest and the most severe of the last nine seasons, day by day. The
dashed line is the middle of the pack. The orange line is 2026, and it stops
where the data stops, on `r as_of_lab`. One month in, 2026 is
`r if (is.na(pct_vs_med)) "not yet comparable to the median (too early in the season for a meaningful ratio)" else sprintf("running above the median (about %.1f times it)", pct_vs_med / 100)` but well
inside the band: several recent years, like 2017, 2022 and 2025, were further
along at this date. My honest reading is *elevated, not exceptional, so far*.
Most of a European fire season happens in July and August. This line has most
of its story still ahead of it, and I will not call a season in week four.
## Does Europe only burn in summer?
Not entirely, and 2026 makes the exception hard to miss. The chart below
stacks eleven years, calendar week by calendar week, so a season's full
rhythm is visible at once.
```{r}
#| label: calendar-heatmap
#| fig-width: 9.5
#| fig-height: 5.5
#| fig-cap: !expr cap_calendar
weekly_area <- build_weekly_area(
years = CALENDAR_YEARS, current_year = YEAR_CURRENT,
current_tagged = tagged_2026_full, snapshot_dir = snap, eu = eu, lc_cols = lc_cols
)
cal_grid <- prepare_calendar_grid(weekly_area, CALENDAR_YEARS, YEAR_CURRENT, as_of)
cal_cutoff_week <- min(lubridate::isoweek(as_of), 52L)
plot_calendar_heatmap(cal_grid, YEAR_CURRENT, cal_cutoff_week)
```
```{r}
#| label: calendar-numbers
peak_2026 <- cal_grid |>
dplyr::filter(year == YEAR_CURRENT, !is.na(area_ha)) |>
dplyr::slice_max(area_ha, n = 1, with_ties = FALSE)
peak_2026_date <- iso_week_start(YEAR_CURRENT, peak_2026$iso_week)
peak_2026_lab <- paste(day(peak_2026_date), month.abb[month(peak_2026_date)])
```
Two patterns jump out. First, most years share the same silhouette: a dark
block across July and August and comparatively little else. Second, 2026's
own row breaks that pattern before summer even starts, with a bright stretch
of late-winter and early-spring weeks that most other years do not show at
this intensity. That is the unusually busy January-to-May window from the
introduction, now visible on its own. The single busiest week of 2026 so far
was the week of `r peak_2026_lab`, when about
`r lab_si_ha(peak_2026$area_ha)` burned in that single week alone. The grey band
on the right marks weeks that have not happened yet: the row simply stops
there, rather than being padded with zeros.
::: {.callout-note title="Reading this chart honestly"}
- The fill is burned **area**, not fire counts. The detection system changed
in 2023 to 2024, which would make later years look artificially busier if
counted by number of fires; area is far less affected.
- **2016** is the archive's first year. Its coverage, especially outside
summer, is less complete than later years, so read that row with extra
caution.
- The color scale is a square root, not a log. Many weeks genuinely burn
zero hectares, and a log scale cannot show a true zero.
:::
# Where did it burn?
With the timing question answered, the next one is geographic: where, across
the continent, did all of this actually happen?
```{r}
#| label: hero
#| fig-width: 9
#| fig-height: 7.5
#| fig-cap: !expr cap_hero
p_base <- ggplot() +
geom_sf(data = eu$poly, fill = "grey95", color = "grey70", linewidth = 0.15) +
coord_sf()
p_base +
geom_sf(
data = tagged_2026_full,
fill = NA, color = "#D64A05", linewidth = 0.25, alpha = 0.8
) +
theme_burns(base_size = 11, map = TRUE)
```
```{r}
#| label: facet-period
#| fig-width: 9.5
#| fig-height: 5
#| fig-cap: !expr cap_facet
tagged_2026_full <- tagged_2026_full |>
mutate(period = factor(
if_else(ba_date < as.Date(sprintf("%d-06-01", YEAR_CURRENT)),
"January–May", paste("1 June –", as_of_lab)),
levels = c("January–May", paste("1 June –", as_of_lab))
))
period_stats <- tagged_2026_full |>
st_drop_geometry() |>
group_by(period) |>
summarise(n_fires = n(), burned_ha = sum(area_ha, na.rm = TRUE), .groups = "drop") |>
mutate(lab = paste0("Fires: ", comma(n_fires), "\nArea: ", lab_si_ha(burned_ha)))
bb <- st_bbox(eu$poly)
period_stats <- period_stats |>
mutate(
x = bb["xmin"] + 0.03 * (bb["xmax"] - bb["xmin"]),
y = bb["ymax"] - 0.03 * (bb["ymax"] - bb["ymin"])
)
p_base +
geom_sf(
data = tagged_2026_full,
fill = "#D64A05", alpha = 0.4, color = "#D64A05", linewidth = 0.08
) +
facet_wrap(~ period, ncol = 2) +
geom_label(
data = period_stats, aes(x = x, y = y, label = lab), inherit.aes = FALSE,
size = 3.1, label.size = 0, hjust = 0, vjust = 1,
fill = alpha("white", 0.85), colour = "grey20"
) +
theme_burns(base_size = 11, map = TRUE) +
theme(strip.text = element_text(face = "bold"))
```
The map makes two things obvious: first, the familiar geography is already
in place: the Iberian Peninsula and the Mediterranean arc dominate the map.
Second, here is that winter surge again, now on the map: about
`r comma(round(full_ha - season_ha, -3))` of the
`r comma(round(full_ha, -3))` hectares were mapped between January and May,
visible in the left panel above. Keep that in mind when reading the summer
chart at the top: it deliberately leaves those months out.
## Explore the fires yourself
```{r}
#| label: leaflet-data
# Computed here, ahead of the prose below, so the sliver-drop share it quotes
# is a real inline value rather than an asserted "about 1%". The cache key
# does not need the snapshot identifier: it already includes asof<date>, and
# tagged_2026_full (its only input) is itself built from a snapshot-aware key
# upstream in get_tagged_window().
leaflet_result <- cached(sprintf("leaflet_2026_asof%s", format(as_of, "%Y%m%d")), {
x <- tagged_2026_full |>
mutate(across(all_of(lc_cols), to_num))
lc_mat <- as.matrix(st_drop_geometry(x)[, lc_cols])
lc_mat[is.na(lc_mat)] <- 0
dom <- lc_labels[lc_cols[max.col(lc_mat, ties.method = "first")]]
dom[rowSums(lc_mat) <= 0] <- "n/a"
x$dominant_lc <- unname(dom)
total_ha_before <- sum(x$area_ha, na.rm = TRUE)
x <- x |>
st_simplify(dTolerance = 100) |> # ~100 m in EPSG:3035 (metric)
filter(!st_is_empty(geometry))
# leaflet only draws polygonal geometry: split GEOMETRYCOLLECTION rows
# (simplify/clip byproducts) into their polygon parts, drop line slivers,
# and cast everything to MULTIPOLYGON.
types <- st_geometry_type(x)
x_poly <- x[types %in% c("POLYGON", "MULTIPOLYGON"), ]
x_gc <- x[types == "GEOMETRYCOLLECTION", ]
if (nrow(x_gc) > 0L) {
x_poly <- rbind(x_poly, suppressWarnings(st_collection_extract(x_gc, "POLYGON")))
}
x_poly <- x_poly |>
filter(!st_is_empty(geometry)) |>
st_cast("MULTIPOLYGON") |>
st_transform(4326)
# Sliver-drop share quoted in prose: the area held by rows that degenerated
# into pure lines/points (or emptied out entirely) after simplification,
# and so never make it into x_poly, as a share of the pre-simplification
# total -- NOT the much smaller boundary shrinkage of polygons that ARE
# retained.
retained_ha <- sum(x_poly$area_ha, na.rm = TRUE)
sliver_pct <- 100 * (total_ha_before - retained_ha) / total_ha_before
list(sf = x_poly, sliver_pct = sliver_pct)
}, version = 3)
leaf_sf <- leaflet_result$sf
sliver_pct_lab <- sprintf("%.1f%%", leaflet_result$sliver_pct)
```
Every 2026 fire, on a map you can pan and zoom. Click an outline for its
date, size, country, and the vegetation that dominated the ground it burned.
I simplified the shapes slightly (about 100 m of tolerance) to keep the page
fast; the tiniest border slivers, about `r sliver_pct_lab` of the total area,
drop out in the process. For survey-grade outlines, use the official EFFIS
viewer.
```{r}
#| label: leaflet
pal_period <- c("#3E8EC4", "#D64A05") # winter-spring blue vs summer orange-red
leaflet(leaf_sf) |>
addProviderTiles("CartoDB.Positron") |>
addPolygons(
weight = 0.7,
color = ~if_else(period == "January–May", pal_period[1], pal_period[2]),
fillColor = ~if_else(period == "January–May", pal_period[1], pal_period[2]),
fillOpacity = 0.35,
popup = ~sprintf(
"<b>%s</b><br/>Date: %s<br/>Mapped area: %s ha<br/>Dominant land cover: %s",
name_long, format(ba_date, "%Y-%m-%d"), comma(round(area_ha)), dominant_lc
)
) |>
addLegend(
position = "bottomleft", colors = pal_period,
labels = c("January–May", paste("1 June –", as_of_lab)),
title = "2026 perimeters", opacity = 0.7
)
```
# What does a giant fire actually look like?
Scrolling through that map, numbers like "seven thousand hectares" are hard
to picture on their own. Below are the ten largest 2026 fires so far, each
drawn at the exact same scale, so size differences are visible rather than
just stated. A single grey circle, equal in area to the city of Paris, sits
alongside them for reference.
```{r}
#| label: gallery-data
# Built here (not in the shared setup chunk) because it only depends on
# tagged_2026_full, which the setup chunk already produced; the caption
# below needs gallery$half_side, so it is composed in this data chunk and
# only REFERENCED (via !expr) by the figure chunk that follows -- fig-cap
# options resolve before a chunk's own body runs, so a caption cannot depend
# on a value the same chunk is about to compute.
gallery <- build_gallery_scars(
tagged_2026_full, n = 10L, lc_cols = lc_cols, lc_labels = lc_labels
)
cap_gallery <- sprintf(
paste(
"The ten largest 2026 fires by Europe-clipped area, each drawn at the same",
"metric scale (a single shared window, about %.0f km across) so their true",
"relative size is directly comparable; fill shows each fire's dominant",
"land-cover class. The grey circle is not a fire: it is a circle of equal",
"area to Paris intra-muros (about 105 km2, 10,500 ha), drawn for scale",
"rather than traced from an official city boundary. Communes and country",
"codes from EFFIS attributes; rapid perimeters are provisional and",
"subject to revision. Source: EFFIS rapid perimeters, snapshot %s."
),
gallery$half_side * 2 / 1000, basename(snap)
)
```
```{r}
#| label: gallery-scars
#| fig-width: 10.5
#| fig-height: 8.5
#| fig-cap: !expr cap_gallery
plot_gallery_scars(gallery)
```
Fires like these are not typical of the season. Across all
`r comma(full_n)` mapped 2026 perimeters, the largest 1% alone hold about
`r sprintf("%.0f%%", gallery$top1_share * 100)` of the season's total burned
area. Most fires are small. A handful of monsters like the ones above do
most of the damage, and the biggest one on the sheet, in
`r gallery$panels$place_lab[[1]]`, is already bigger than the Paris-sized
circle next to it.
## See it for yourself: drop each fire onto Paris
A grey circle is one way to picture "the size of Paris," but the real city
is not a circle. What if you could see a fire's true, ragged outline sitting
directly on the streets you might actually know? The map below does exactly
that: it takes the true shape of each of the `r nrow(gallery$meta)` fires
above and slides it onto the real municipal boundary of Paris, one fire at a
time, so the comparison is against the actual city rather than a stand-in
shape.
```{r}
#| label: paris-comparison-data
# Reuses gallery$panels/gallery$meta (already computed for the static
# figure above) rather than re-selecting or re-simplifying the top-10 from
# scratch; build_paris_comparison() only adds a translation ("+ Paris
# centroid") on top of geometry build_gallery_scars() already produced.
# Cache key mirrors the leaflet-data chunk's convention above: asof<date>
# is enough because gallery (its only input besides the static Paris
# boundary asset) is itself built from snapshot-aware tagged_2026_full.
paris_cmp <- cached(
sprintf("paris_comparison_asof%s", format(as_of, "%Y%m%d")),
build_paris_comparison(gallery),
version = 1
)
paris_fires <- paris_cmp$fires_wgs84 |>
mutate(
fill_col = unname(pal_lc[dominant_lc]),
times_paris = area_ha / paris_cmp$paris_ha,
group_label = sprintf(
"%s. %s (%s), %s ha", rank, commune, iso_a2, comma(round(area_ha))
),
popup_html = sprintf(
paste(
"<b>%02d. %s</b><br/>Country: %s<br/>Date: %s<br/>",
"Mapped area: %s ha<br/><b>%.1f×</b> the area of Paris (%s ha)"
),
as.integer(rank), commune, name_long, format(ba_date, "%Y-%m-%d"),
comma(round(area_ha)), times_paris, comma(round(paris_cmp$paris_ha))
)
)
```
::: {.callout-note title="Read this map honestly"}
- These fires never happened anywhere near Paris. Their real locations are
on the maps earlier in this post; here, each outline is **moved** so its
size can be compared directly to a familiar shape.
- Paris's own outline is its real municipal boundary (INSEE code 75056,
not a circle this time), about
`r comma(round(paris_cmp$paris_ha))` hectares by this geometry (close to,
but not identical to, the round 10,500 ha used as a reference constant
elsewhere on this page). Boundary data:
[france-geojson](https://github.com/gregoiredavid/france-geojson)
(Etalab, Licence Ouverte).
- Pick a fire from the list on the right; only one outline is shown at a
time, so sizes are never visually stacked on top of each other.
:::
```{r}
#| label: paris-comparison-leaflet
# One dark, permanent outline (never toggles) for Paris itself, plus one
# leaflet GROUP per fire; addLayersControl(baseGroups = ...) makes group
# selection mutually exclusive (radio-style), and hideGroup() on every
# label but the first leaves fire 01 selected by default.
combined_geom <- c(sf::st_geometry(paris_cmp$paris_wgs84), sf::st_geometry(paris_fires))
bbox_all <- sf::st_bbox(combined_geom)
paris_map <- leaflet(width = "100%", height = 520) |>
addProviderTiles("CartoDB.Positron") |>
addPolygons(
data = paris_cmp$paris_wgs84,
color = "#1A1A1A", weight = 2.4, fill = FALSE, opacity = 0.9
) |>
fitBounds(
lng1 = unname(bbox_all["xmin"]), lat1 = unname(bbox_all["ymin"]),
lng2 = unname(bbox_all["xmax"]), lat2 = unname(bbox_all["ymax"])
)
paris_map <- purrr::reduce(seq_len(nrow(paris_fires)), function(map_acc, i) {
row_i <- paris_fires[i, ]
addPolygons(
map_acc,
data = row_i, group = row_i$group_label,
color = "grey20", weight = 1,
fillColor = row_i$fill_col, fillOpacity = 0.55,
popup = row_i$popup_html
)
}, .init = paris_map)
paris_map <- paris_map |>
addLayersControl(
baseGroups = paris_fires$group_label,
options = layersControlOptions(collapsed = FALSE),
position = "topright"
)
# Default selection: fire 01 (already the largest, per gallery's arrange()).
paris_map <- purrr::reduce(
paris_fires$group_label[-1],
function(map_acc, g) hideGroup(map_acc, g),
.init = paris_map
)
paris_map
```
# Which countries are hit hardest so far?
Individual fires are dramatic on their own; country totals show where the
season is landing hardest overall.
```{r}
#| label: countries
#| fig-width: 12
#| fig-height: 5.5
#| fig-cap: !expr cap_countries
ctry_2026 <- tagged_2026_full |>
st_drop_geometry() |>
group_by(name_long) |>
summarise(burned_ha = sum(area_ha, na.rm = TRUE), n_fires = n(), .groups = "drop") |>
left_join(country_land_area(eu), by = "name_long") |>
mutate(pct_land = 100 * burned_ha / land_area_ha)
top10 <- ctry_2026 |>
slice_max(burned_ha, n = 10)
flags <- flag_table(top10$name_long, eu$poly)
top10 <- top10 |>
left_join(flags, by = "name_long") |>
# factor AFTER the join: joining a factor to a character column silently
# coerces name_long back to character, which would both scramble the bar
# order (alphabetical) and break the numeric y positions for the flags
mutate(name_long = fct_reorder(name_long, burned_ha))
max_x <- max(top10$burned_ha)
# Flag PNGs drawn with base annotation_raster() (no extra image package
# needed): one raster layer per country, placed just right of its bar.
# Discrete y positions are the factor level codes (1..10).
flag_df <- top10 |>
filter(!is.na(flag_path)) |>
mutate(y_pos = as.integer(name_long))
flag_layers <- lapply(seq_len(nrow(flag_df)), function(i) {
x0 <- flag_df$burned_ha[i] + 0.05 * max_x
annotation_raster(
png::readPNG(flag_df$flag_path[i]),
xmin = x0, xmax = x0 + 0.08 * max_x,
ymin = flag_df$y_pos[i] - 0.28, ymax = flag_df$y_pos[i] + 0.28
)
})
p_raw <- ggplot(top10, aes(x = burned_ha, y = name_long)) +
geom_col(fill = "grey90") +
geom_point(aes(color = col), size = 3) +
scale_color_identity() +
flag_layers +
scale_x_continuous(labels = lab_si_ha, expand = expansion(mult = c(0, 0.18))) +
labs(x = "Burned area (ha)", y = NULL) +
theme_burns(base_size = 12) +
coord_cartesian(clip = "off")
p_pct <- ggplot(top10, aes(x = pct_land, y = name_long)) +
geom_segment(aes(x = 0, xend = pct_land, yend = name_long, color = col),
linewidth = 1.1, show.legend = FALSE) +
geom_point(aes(color = col), size = 3.2) +
scale_color_identity() +
scale_x_continuous(labels = label_percent(scale = 1),
expand = expansion(mult = c(0, 0.08))) +
labs(x = "Share of national land area", y = NULL) +
theme_burns(base_size = 12) +
theme(axis.text.y = element_blank())
p_raw + p_pct +
plot_layout(widths = c(3, 2))
```
`r top10$name_long[which.max(top10$burned_ha)]` leads the raw ranking with
about **`r comma(round(max(top10$burned_ha), -3))` hectares** so far. But raw
hectares flatter big countries. The right panel asks a fairer question: how
much of the country's *own* land has burned? Small countries can be hit much
harder relative to their size, and that is where the order reshuffles.
# Is fire returning to old scars?
Some land burns again and again. To see how much of 2026 falls on ground that
already burned recently, I merged all the fire outlines from 2017 to 2025
into one big historical footprint, then checked how much of this year's
burned area lands inside it.
```{r}
#| label: reburn
#| fig-width: 9
#| fig-height: 7.5
#| fig-cap: !expr cap_reburn
footprint <- get_historical_footprint(HIST_YEARS, snap, eu, tol_m = 100)
reburn <- cached(
sprintf("reburn_2026_asof%s_tol100", format(as_of, "%Y%m%d")),
compute_reburn(tagged_2026_full, footprint),
version = 1
)
reburn_layers <- rbind(
st_sf(kind = "First-time burn (not in 2017–2025 scars)", geometry = reburn$new_geom),
st_sf(kind = "Re-burn of a 2017–2025 scar", geometry = reburn$reburn_geom)
)
ggplot() +
geom_sf(data = eu$poly, fill = "grey95", color = "grey70", linewidth = 0.15) +
geom_sf(data = reburn_layers, aes(fill = kind, color = kind),
linewidth = 0.1, alpha = 0.65) +
scale_fill_manual(values = c(
"First-time burn (not in 2017–2025 scars)" = "#D64A05",
"Re-burn of a 2017–2025 scar" = "#1F78B4"
), name = NULL) +
scale_color_manual(values = c(
"First-time burn (not in 2017–2025 scars)" = "#D64A05",
"Re-burn of a 2017–2025 scar" = "#1F78B4"
), name = NULL) +
theme_burns(base_size = 11, map = TRUE) +
theme(legend.position = "bottom")
```
On the map, France's scars lean toward re-burns of familiar ground, while
Spain and Portugal's fires more often break new ground.
```{r}
#| label: aude-numbers
aude_rows <- tagged_2026_full |>
st_drop_geometry() |>
filter(!is.na(province), grepl("Aude", province, fixed = TRUE))
aude_n <- nrow(aude_rows)
aude_ha <- sum(aude_rows$area_ha, na.rm = TRUE)
fr_rows <- tagged_2026_full |> st_drop_geometry() |> filter(name_long == "France")
fr_n <- nrow(fr_rows)
fr_ha <- sum(fr_rows$area_ha, na.rm = TRUE)
```
The headline: about **`r sprintf("%.0f%%", 100 * reburn$reburn_share)`** of
2026's burned area (roughly `r comma(round(reburn$reburn_ha, -3))` of
`r comma(round(reburn$total_ha, -3))` hectares) sits on land that already
burned at least once since 2017. That is not automatically bad news.
Mediterranean shrubland and many pine forests are fire-adapted: they regrow
quickly after a burn, and that fresh growth is often exactly the kind of
fine, dry fuel that carries a fire well again a few years later. Repeated
burning on a short cycle has long been part of how these landscapes work.
What the share does not tell us is whether any single re-burn was more or
less severe than the fire before it, or whether the gap between fires is
shrinking: it is a description of overlap, not a verdict on land condition.
Treat the exact figure as an estimate too. Both layers are rapid mapping
products, and I simplified the shapes to keep the computation tractable.
I still check on Aude every season now, since it's where this whole project
started:
`r if (aude_n > 0) sprintf("the Aude département in France does show 2026 activity: %d mapped fire%s totalling about %s hectares so far.", aude_n, if (aude_n > 1) "s" else "", comma(round(aude_ha, -2))) else "the Aude département in France shows no mapped 2026 fire in this snapshot so far (a real zero, not a data gap)."`
France as a whole stands at about `r comma(round(fr_ha, -2))` hectares across
`r comma(fr_n)` mapped fires this year.
# What is actually burning?
Beyond where fire keeps coming back, there is a second question worth
asking: is this season eating into different vegetation than usual? To
compare fairly, I use identical calendar windows: what burned between
**1 June and
`r as_of_lab`** this year, against what burned in the very same window in the
nine previous seasons pooled together.
```{r}
#| label: landcover
#| fig-width: 9.5
#| fig-height: 3.6
#| fig-cap: !expr cap_landcover
cutoff_md <- format(as_of, "%m-%d")
lc_share <- function(df, label) {
df |>
mutate(across(all_of(lc_cols), to_num)) |>
summarise(across(all_of(lc_cols), ~ sum(.x / 100 * area_ha, na.rm = TRUE))) |>
pivot_longer(everything(), names_to = "class", values_to = "lc_ha") |>
mutate(share = lc_ha / sum(lc_ha), group = label)
}
lc_hist <- map_dfr(HIST_YEARS, function(y) {
get_tagged_window(
y, snap, eu,
start_date = as.Date(sprintf("%d-06-01", y)),
end_date = as.Date(paste0(y, "-", cutoff_md))
) |>
st_drop_geometry() |>
mutate(year = y)
})
lc_2026 <- get_tagged_summer(YEAR_CURRENT, snap, eu,
SEASON_START_MONTH, SEASON_END_MONTH) |>
st_drop_geometry() |>
filter(ba_date <= as_of)
shares_2026 <- lc_share(lc_2026, sprintf("2026 (Jun 1 – %s %d)", month.abb[month(as_of)], day(as_of)))
shares_hist <- lc_share(lc_hist, "2017–2025 average\n(same window)")
lc_comp <- bind_rows(shares_2026, shares_hist) |>
mutate(
class = factor(lc_labels[class], levels = names(pal_lc)),
group = fct_rev(factor(group, levels = unique(group)))
)
# Largest 2026-vs-history composition gap, quoted (stale-proof) in the prose
lc_gap <- shares_2026 |>
select(class, share_cur = share) |>
left_join(shares_hist |> select(class, share_hist = share), by = "class") |>
mutate(class_label = lc_labels[class], gap = share_cur - share_hist) |>
slice_max(abs(gap), n = 1)
ggplot(lc_comp, aes(x = share, y = group, fill = class)) +
geom_col(width = 0.62, color = NA) +
geom_text(
aes(
label = if_else(share >= 0.07, label_percent(accuracy = 1)(share), ""),
group = class
),
position = position_stack(vjust = 0.5),
color = "white", size = 3.3, fontface = "bold"
) +
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.01))) +
labs(
x = "Share of burned area", y = NULL,
caption = "Pooled 2017–2025 shares are area-weighted across all nine years' same-window fires."
) +
theme_burns(base_size = 12)
```
Compare the two bars: where they differ is where this season deviates from
the recent norm. Right now the biggest gap is **`r lc_gap$class_label`**, at
`r label_percent(accuracy = 1)(lc_gap$share_cur)` of this season's burned
area versus `r label_percent(accuracy = 1)(lc_gap$share_hist)` in a typical
early season. Read the bars as *what burned*, not as what burns easily: they
describe the landscape the fires happened to cross.
::: {.callout-note title="What the land-cover classes mean" collapse="true"}
- **Broad-leaved forest**: deciduous trees (oaks, beeches).
- **Coniferous forest**: needle-leaf trees (pines, spruces, firs), including
plantations.
- **Mixed forest**: broad-leaved and coniferous stands together.
- **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 listed above (heath, sparse
vegetation, dunes, rocky ground).
- **Other LC**: residual or unknown classes.
:::
# How much of this is happening in protected nature?
One more lens on what burned: how much of it sits inside land specifically
protected for nature?
::: {.callout-note title="What is Natura 2000?"}
Natura 2000 is the European Union's network of protected natural sites,
covering habitats and species the EU has agreed are worth safeguarding. It
is not a fire ban. Many Natura 2000 landscapes, especially Mediterranean
scrub and some forest types, are fire-adapted ecosystems where occasional
burning is part of their natural cycle. A fire inside a protected site is
not automatically a disaster, but tracking how much burning happens there is
still useful context.
:::
EFFIS tags every perimeter with the share of its own area that falls inside
a Natura 2000 site. The chart below turns that into a season-by-season
series, using the same 1 June cutoff every year so the comparison is fair.
```{r}
#| label: natura-trend
#| fig-width: 8
#| fig-height: 4.2
#| fig-cap: !expr cap_natura_trend
natura_trend <- build_natura_trend(
hist_years = HIST_YEARS, year_current = YEAR_CURRENT,
current_tagged = tagged_2026_full, snapshot_dir = snap, eu = eu, cutoff_md = cutoff_md
)
plot_natura_trend(natura_trend, YEAR_CURRENT)
```
```{r}
#| label: natura-numbers
natura_2026 <- natura_trend |> dplyr::filter(year == YEAR_CURRENT)
natura_hist_median <- stats::median(natura_trend$share[natura_trend$year != YEAR_CURRENT], na.rm = TRUE)
```
Weighting by area, about `r sprintf("%.0f%%", natura_2026$share * 100)` of
2026's burned area in this window sits inside a Natura 2000 site, against a
`r sprintf("%.0f%%", natura_hist_median * 100)` median for 2017-2025 by the
same date. The map below shows exactly which 2026 scars carry a high
protected-area share; grey outlines mark fires with none.
```{r}
#| label: natura-map
#| fig-width: 8
#| fig-height: 6.5
#| fig-cap: !expr cap_natura_map
plot_natura_map(tagged_2026_full, eu)
```
That is the season through `r as_of_lab`. The real test is still ahead: most
of a typical European fire season happens in July and August, and this page
updates weekly as new EFFIS outlines come in. Watch, above all, whether the
elevated pace from this first month holds, eases, or turns into something
worse.
::: {.callout-note title="A final word of caution"}
Everything on this page is a rapid satellite estimate, not an official
statistic. Full methods and caveats are on the [About](../about.qmd) page.
:::