---
title: "Summer 2026 fires: interactive explorer"
subtitle: "Every mapped fire of 2026, plus a drop-it-on-Paris size comparison"
author: "Pierre Beaucoral"
date: 2026-10-02
execute:
freeze: false
---
This page is the hands-on companion to the [2026 season
review](2026.qmd). The review tells the story in charts; here you can pan,
zoom and click through the fires yourself. It sits on its own page because the
maps carry the outline of every fire, which is heavy, and the review should
load fast.
```{r}
#| label: setup
#| include: false
library(sf)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)
library(lubridate)
library(forcats)
library(scales)
library(leaflet)
library(htmlwidgets)
# GDAL caps single GeoJSON features at ~200 MB; see scripts/fetch_effis.R.
Sys.setenv(OGR_GEOJSON_MAX_OBJ_SIZE = "0")
source(file.path("R", "helpers.R"))
source(file.path("R", "geo.R"))
source(file.path("R", "cache.R"))
source(file.path("R", "theme.R"))
source(file.path("R", "flags.R"))
source(file.path("R", "pipeline.R"))
source(file.path("R", "plots.R"))
source(file.path("R", "retro.R"))
source(file.path("R", "maps.R"))
source(file.path("scripts", "latest_snapshot.R"))
# Same parameters as posts/2026.qmd, so every cache key below is identical and
# nothing is recomputed.
YEAR_CURRENT <- 2026L
HIST_YEARS <- 2017:2025
SEASON_START_MONTH <- 6L
SEASON_END_MONTH <- 9L
snap <- latest_snapshot()
stopifnot(!is.na(snap))
eu <- get_eu()
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)])
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"
)
# The leaflet cache below was built from data that already carries `period`
# (added in the review's period-map chunk), so add it here before the call.
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))
))
```
# Every fire of 2026
Zoomed out, each fire is a circle: the bigger the circle, the more ground
burned. Zoom in a few steps and the circles give way to the real outlines.
Click a fire for its date, size, country, and the vegetation that dominated the
ground it burned. Blue is January to May, orange is the summer season.
```{r}
#| label: leaflet-data
# Same cache key and body as posts/2026.qmd (asof<date> is enough: the only
# input, tagged_2026_full, is built from a snapshot-aware key upstream).
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))
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)
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)
# One centroid per fire for the zoomed-out view (centroids are computed on the
# sphere here, which is fine for a marker position).
leaf_pts <- suppressWarnings(st_centroid(st_geometry(leaf_sf))) |>
st_coordinates() |>
as.data.frame() |>
setNames(c("lng", "lat"))
leaf_df <- bind_cols(st_drop_geometry(leaf_sf), leaf_pts) |>
mutate(
col = if_else(period == "January–May", "#3E8EC4", "#D64A05"),
# radius in pixels grows with sqrt(area): circle area ~ burned area
radius = 2 + 0.07 * sqrt(area_ha),
popup = sprintf(
paste0("<b>%s</b><br/>EFFIS fire date: %s<br/>Mapped area: %s ha (%s km<sup>2</sup>)",
"<br/>Dominant land cover: %s"),
name_long, format(ba_date, "%Y-%m-%d"), comma(round(area_ha)),
comma(round(area_ha / 100, 1)), dominant_lc
)
) |>
arrange(desc(area_ha)) # big circles underneath small ones
leaf_sf$popup <- sprintf(
paste0("<b>%s</b><br/>EFFIS fire date: %s<br/>Mapped area: %s ha (%s km<sup>2</sup>)",
"<br/>Dominant land cover: %s"),
leaf_sf$name_long, format(leaf_sf$ba_date, "%Y-%m-%d"), comma(round(leaf_sf$area_ha)),
comma(round(leaf_sf$area_ha / 100, 1)), leaf_sf$dominant_lc
)
# Round vertices to 4 decimals (~8 m) to roughly halve the embedded JSON.
sf::st_geometry(leaf_sf) <- sf::st_sfc(
lapply(sf::st_geometry(leaf_sf), function(g) {
g[] <- lapply(g, function(poly) lapply(poly, round, 4))
g
}),
crs = 4326
)
leaf_sf$col <- if_else(leaf_sf$period == "January–May", "#3E8EC4", "#D64A05")
```
```{r}
#| label: leaflet
# Esri World Light Gray tiles. CARTO's Positron (the previous basemap) now
# answers keyless requests with "API KEY REQUIRED" watermark tiles
# (checked 2026-10-02), so this page uses a light, keyless Esri canvas instead.
# Shared by both maps on this page.
add_basemap <- function(map) {
addTiles(
map,
urlTemplate = paste0(
"https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/",
"World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}"
),
attribution = "Tiles © Esri; sources: Esri, HERE, Garmin, OpenStreetMap contributors",
options = tileOptions(maxZoom = 16)
)
}
pal_period <- c("#3E8EC4", "#D64A05") # winter-spring blue vs summer orange-red
ZOOM_SWITCH <- 7L # outlines from this zoom level upward
leaflet(options = leafletOptions(minZoom = 3)) |>
add_basemap() |>
addCircleMarkers(
data = leaf_df, lng = ~lng, lat = ~lat, group = "circles",
radius = ~radius, weight = 0.6, color = ~col, fillColor = ~col,
fillOpacity = 0.4, popup = ~popup
) |>
addPolygons(
data = leaf_sf, group = "outlines",
weight = 0.7, color = ~col, fillColor = ~col, fillOpacity = 0.35,
popup = ~popup
) |>
groupOptions("circles", zoomLevels = 3:(ZOOM_SWITCH - 1L)) |>
groupOptions("outlines", zoomLevels = ZOOM_SWITCH:18) |>
setView(lng = 12, lat = 47, zoom = 4) |>
addLegend(
position = "bottomleft", colors = pal_period,
labels = c("January–May", paste("1 June –", as_of_lab)),
title = "2026 fires", opacity = 0.7
)
```
Two things to know. Circle size is proportional to burned area, so a few giants
stand out against thousands of small fires. And the dates are the **EFFIS fire
date**, the day the satellite service attributes to the fire, not an official
start or end date. I simplified the outlines slightly (about 100 m of tolerance)
to keep the page responsive; 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.
# How big is a fire, really? Drop it on Paris
Numbers like "forty thousand hectares" are hard to picture. So here is a
comparison with a city you may know: the map below takes the true shape of each
of the ten largest 2026 fires and slides it onto the real municipal boundary of
Paris, one fire at a time.
```{r}
#| label: paris-comparison-data
gallery <- build_gallery_scars(
tagged_2026_full, n = 10L, lc_cols = lc_cols, lc_labels = lc_labels
)
# version 2: gallery place names now fall back to province/country when EFFIS
# has no commune ("N.A."), so v1 caches carry stale labels.
paris_cmp <- cached(
sprintf("paris_comparison_asof%s", format(as_of, "%Y%m%d")),
build_paris_comparison(gallery),
version = 2
)
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/>EFFIS fire date: %s<br/>",
"Mapped area: %s ha (%s km<sup>2</sup>)<br/>",
"<b>%.1f×</b> the area of Paris (%s ha, %s km<sup>2</sup>)"
),
as.integer(rank), commune, name_long, format(ba_date, "%Y-%m-%d"),
comma(round(area_ha)), comma(round(area_ha / 100)), times_paris,
comma(round(paris_cmp$paris_ha)), comma(round(paris_cmp$paris_ha / 100))
)
)
```
::: {.callout-note title="Read this map honestly"}
- These fires never happened anywhere near Paris. Their real locations are
on the maps in the [season review](2026.qmd); 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),
about `r comma(round(paris_cmp$paris_ha))` hectares
(`r comma(round(paris_cmp$paris_ha / 100))` km<sup>2</sup>) by this geometry.
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 stacked on top of each other.
:::
```{r}
#| label: paris-comparison-leaflet
# One dark, permanent outline for Paris, plus one leaflet GROUP per fire;
# baseGroups makes the selection mutually exclusive, 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) |>
add_basemap() |>
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"
)
paris_map <- purrr::reduce(
paris_fires$group_label[-1],
function(map_acc, g) hideGroup(map_acc, g),
.init = paris_map
)
paris_map
```
*Source: EFFIS rapid perimeters, snapshot `r basename(snap)`; areas from
geometry in EPSG:3035. The outlines are rapid estimates and are revised as
better imagery arrives.*