Maps

# The explorer filters display copies and exports the matching source fields.
# The original CSVs and their coordinates are never changed by map interactions.
resource_map <- build_explorer(map_data)
resource_map

What changed in November 2026?

Varroa mites are ectoparasites of honey bees native to Asia. V. destructor and V. jacobsoni have independently switched from the Eastern honey bee, Apis cerana, to the Western honey bee, A. mellifera. The map brings together mitochondrial labels reported for mites collected on these hosts. Collection host does not by itself establish successful reproduction.

Australia’s former freedom from V. destructor ended with detection in sentinel colonies at the Port of Newcastle in June 2022. The Australian addition represents five anonymised sites within 20 km of the port as one approximate regional observation. McFarlane et al. (2024)

Open the original map before this update to compare it with this page. The original HTML is preserved exactly as it appeared in the baseline checkout. The New additions button isolates the newly added evidence; the three original host-specific views are also retained below.

The targeted literature addition focuses on Vietnam, China, Taiwan, Thailand and South Korea, with selected comparative records and the Australian incursion. It adds 108 point evidence records at 85 coordinate pairs and five country records. Of the point additions, 98 have a mitochondrial assignment and 10 are reserved for the optional gray genome-wide layer. There are 48 published-site records and 60 approximate-locality records. The full database now holds 704 point records and 429 country records. These are evidence records, not independent mites, unique samples or prevalence estimates.

How precise are the locations? Filled symbols indicate published site coordinates. Hollow symbols indicate an approximate locality, which may be a town, administrative district or anonymised study area. Numbered rings identify records sharing a coordinate; inspect each record’s precision in its popup. Country shading indicates occurrence within the named region, not throughout the country. In the main atlas, circles identify V. destructor, diamonds V. jacobsoni, squares V. underwoodi, triangles V. rindereri, and stars unresolved species. Numbered shared markers list their species in the popup. Dotted gold halos mark the 2026 additions; their collection dates may be much older. The bundled borders and locator work offline; OpenStreetMap is optional.

Can I reuse the figure? Export figure downloads an editable SVG of the current map extent, with its legend, evidence counts, source credit and filter metadata. Exported counts describe all matching records; the current viewport may show only a subset of their locations. Equal segments in shared-coordinate rings show which groups are present, never their sample proportions. The muted atlas palette is a display choice; the original host-specific maps below retain their historical colors. The species view follows the red/blue distinction used in Techer et al. (2019), Figure 1.

What do the filters mean? The year filter uses collection years and overlapping collection intervals. Unavailable years can be kept or excluded explicitly. The genome-wide option adds unresolved mitochondrial records in gray and marks WGS/RAD evidence with a gray outline; resolved records retain their mitochondrial color. A study filter can also find separately attributed archive annotations. The results table and CSV contain the same matching records. Country evidence can be hidden from the map while remaining in the table and download.

What happens when studies share a marker? A shared popup lists every matching evidence record at that coordinate, including different hosts when both are selected. Expand each study to see its collection year, host, reported mitochondrial label, accession or run IDs and source evidence. Shared coordinates do not establish reuse of the same mite. Broad K colors include study-specific K1-like or mixed-variant calls, which remain explicit in the popup.

Distribution of Varroa spp. mtDNA lineages on Apis mellifera

This full map retains V. destructor, V. jacobsoni and the unresolved Varroa species records on the Western honey bee. Study-specific J-type labels remain available alongside J1 and K-group records.

mellifera_map <- build_map(map_data, "A. mellifera",
  c("V. destructor", "V. jacobsoni", "Varroa sp."))
mellifera_map

Distribution of Varroa destructor mtDNA lineages on Apis cerana

The Eastern honey bee map includes Taiwan T, Lin’s J2-1 and C4-1 labels, and the archived Nepal2 record. The two Fuzhou WGS records both have A. cerana as their collection host. AcFZ8 grouped with the A. mellifera-associated nuclear clade, whereas the other seven family pools grouped with the A. cerana-associated clade. Their mitochondrial colors remain K-group colors. Thong Nhat SG2 has no resolved mitochondrial call and appears only in the gray genome-wide layer.

cerana_destructor_map <- build_map(map_data, "A. cerana", "V. destructor", center = c(105, 23, 4))
cerana_destructor_map

Distribution of Varroa jacobsoni mtDNA lineages on Apis cerana

The existing V. jacobsoni records and lineage views are retained. This targeted update does not add the reclassified Vietnamese accessions to the V. destructor layers.

cerana_jacobsoni_map <- build_map(map_data, "A. cerana", "V. jacobsoni", center = c(108, 12, 4))
cerana_jacobsoni_map

What can these records tell us?

The additions support occurrence at the reported sites. They do not support comparisons of prevalence between countries: surveys differ in their dates, sampling units, sequencing methods and coverage. Lin’s mixed-variant counts describe whole site–host groups. Zheng’s WGS libraries are brood-cell family pools, not individual mites. COI and RAD counts in Nguyen’s study can refer to different individuals. Wang et al. (2020) provides complementary brood-cell evidence at overlapping Chinese sites.

Vietnamese accessions MW599134, MW599138 and MW599139 are excluded from the V. destructor additions following Nguyen’s reassignment to V. jacobsoni. The short COI alignment does not resolve MW599142–MW599144 unambiguously, and MW599126 also lacks an unambiguous named call in the reanalysis. Those records retain the original report and nuclear evidence in the gray layer. At Ia Ko, the Chiu records carry the later Nguyen accessions with possible specimen overlap explicitly unresolved. Chiu’s Taiwan and Gia Lai years remain unavailable.

Taiwan’s two surveys remain distinct: Hua reported 168 J calls on A. mellifera and 15 T calls on A. cerana, while Chiu’s 13 RAD-sequenced Taiwanese mites were all collected on A. mellifera. Antu’s 74 A. cerana mites carry Lin’s J2-1 call. AcFZ8’s collection year remains unavailable because its alias lacks a matching public ENA run.

Fourteen archived Techer site–host–COI records are retained in the workbook and separately attributed in matching legacy-locality popups. They do not create new point rows. Nine other archived records are among the 108 additions. These 1994–2003 collections do not imply recent spread. VNV475_1 and JPV025_1 retain A. cerana as the collection host; A. mellifera host reads indicate recent feeding, not a change to the collection record. Disagreement in coordinates, dates or lineage calls remains visible for review, and specimen reuse is not inferred from a shared locality.

The bundled borders have no polygon named “Yugoslavia”. That historical country-only record remains in the database without being reassigned to a present-day country. Korean spelling variants and trailing whitespace are matched only for display.

Data and original review

The updated workbook contains 127 literature-sheet additions. Both workbooks are included in this page, so these downloads also work from a saved local copy.

Download the 2026 workbook · Download the archival 2021 workbook

The original four sheets and the dated GenBank snapshot are retained. Subsequent assignments are recorded in the literature sheet rather than rewriting the original sequence entries. The repository also contains the curated handoff and accession crosswalk.

The following description documents the original review, not the extent of the targeted 2026 literature pass.

The original review covered 68 articles from 1995 to 2020 using either RAPD, mtDNA analysis (PCR-RFLP, sequencing) and/or nuclear microsatellites on Varroa mites or environmental honey DNA29. We collected distribution data about Varroa species identity, mtDNA haplogroup, date of sampling, geographical localization, honey bee host. For geographical localization, three cases occurred: a) exact coordinates were available, b) city or locality was available and c) no localization was available outside of the country level. In the second case, the original review approximated the geographical position as the center of the city/locality or placed it to the nearest Agricultural Institute or Academic Center/University as some past sampling was known to be carried in experimental research apiaries..

In addition, 485 mitochondrial sequences were downloaded from NCBI Genbank (last update on the 01 February 2020) for which for the same information if not included in the previous papers list. That snapshot contained three mitogenomes, 387 partial COX1 sequences (length ranging from 188 to 1088bp), 36 partial COX3 sequences (ranging from 323 to 436 bp), 36 partial ATP6 sequences (ranging from 287 to 339 bp), 23 partial CYTB sequences (ranging from 899 to 985 bp). All COX1 sequences were blasted and aligned to 44 reference mtDNA haplotypes (see Table S3)1,4,6,26,27,30–33. Despite highly variable length and inconsistent sequence overlapping due to the usage of variable primers and high sequences trimming, we expanded the known COX1 haplogroups . Following the nomenclature rules we found that V. destructor is the most diverse with 31 haplogroups (including 22 K-like), followed by V. jacobsoni with 19, five for V. underwoodi, for undetermined Varroa sp. and one for V. rindereri. The original review used these haplogroups to build an interactive distribution map of the Varroa mites on their honey bee hosts mikheyevlab.github.io/varroa-mtDNA-world-distrib/.

Reproduce this page

Map preparation code

library(leaflet)
library(htmltools)
library(sf)
# Inputs stay unchanged; these helpers prepare labels, evidence layers and popups.
read_evidence <- function(path) {
  read.csv(path, fileEncoding = "UTF-8-BOM", colClasses = "character",
           na.strings = character(), check.names = FALSE, stringsAsFactors = FALSE)
}
row_key <- function(x, columns) apply(x[, columns, drop = FALSE], 1, paste, collapse = "\034")
escape <- function(x) as.character(htmltools::htmlEscape(x))
genome_study <- function(description) {
  # Both the cited study and its explicit assay must support genome-wide evidence.
  grepl("(Chiu et al. \\(2023\\)|Nguyen et al. \\(2025\\)|Zheng et al. \\(2023\\)|Techer et al. \\(2022\\))", description) &
    grepl("\\bWGS\\b|RAD-seq", description)
}

load_map_data <- function(apply_reviews = TRUE) {
  u <- "Data/updates/2026-11-05"
  points <- read_evidence("Data/mappingpoints_Varroareview05022020.csv")
  countries <- read_evidence("Data/mappingcountry_Varroareview05022020.csv")
  additions <- list(P = read_evidence(file.path(u, "point_additions.csv")),
                    C = read_evidence(file.path(u, "country_additions.csv")))
  crosswalk <- read_evidence(file.path(u, "workbook_crosswalk.csv"))
  prepare <- function(data, kind) {
    # Match entire supplied rows, so study labels never leak between coincident records.
    match_row <- match(row_key(data, names(data)), row_key(additions[[kind]], names(data)))
    input <- paste0(kind, match_row)
    lookup <- match(input, crosswalk$input)
    data$record_id <- paste0(kind, seq_len(nrow(data)))
    data$is_new <- !is.na(match_row)
    data$input_id <- ifelse(data$is_new, input, "")
    data$reported_mt <- data$Haplogroup
    data$reported_mt[!is.na(lookup)] <- crosswalk$reported_mt[lookup[!is.na(lookup)]]
    data$reference <- sub(".*Ref:", "", data$Description)
    data$reference[!is.na(lookup)] <- crosswalk$reference[lookup[!is.na(lookup)]]
    data$nuclear <- genome_study(data$Description)
    data$archive_note <- ""
    if (kind == "C") {
      data$Region <- sub("\\. .*", "", data$Description)
      data$Region[!is.na(lookup)] <- crosswalk$region[lookup[!is.na(lookup)]]
    }
    data
  }
  points <- prepare(points, "P")
  countries <- prepare(countries, "C")
  archives <- read_evidence(file.path(u, "archive_annotations.csv"))
  links <- read_evidence(file.path(u, "archive_legacy_links.csv"))
  for (i in seq_len(nrow(links))) {
    link <- links[i, ]
    j <- as.integer(link$point_data_row)
    archive <- archives[as.integer(sub("A", "", link$archive_input)), ]
    # A reviewed locality/host association is not evidence of the same individual mite.
    stopifnot(points$Description[j] == link$legacy_description,
              points$Region[j] == link$legacy_region, points$Host[j] == link$host)
    note <- paste0("<div class='archive-note'><b>Separately attributed archived WGS evidence (",
      link$archive_input, "; legacy S2 row ", link$s2_row, ")</b><br>",
      escape(archive$Description), "<br><b>Archive reference coordinates:</b> ",
      archive$coord.X, " N, ", archive$coord.Y, " E (approximate).<br><b>Review:</b> ",
      escape(link$review), ". Locality and collection host matched; individual reuse is unresolved.</div>")
    points$archive_note[j] <- paste0(points$archive_note[j], note)
    points$nuclear[j] <- TRUE
  }
  points$coord.X <- as.numeric(points$coord.X) # X is latitude in the historical files.
  points$coord.Y <- as.numeric(points$coord.Y) # Y is longitude, despite the usual GIS convention.
  stopifnot(all(is.finite(points$coord.X)), all(abs(points$coord.X) <= 90),
            all(is.finite(points$coord.Y)), all(abs(points$coord.Y) <= 180))
  points$coordinate_key <- paste(points$coord.X, points$coord.Y, sep = ",")
  # This lookup affects polygon display only; the archival country spelling is retained.
  aliases <- c("Republic of Korea" = "Korea, Republic of", "South Korea" = "Korea, Republic of")
  countries$border_name <- trimws(countries$Location)
  matched <- countries$border_name %in% names(aliases)
  countries$border_name[matched] <- unname(aliases[countries$border_name[matched]])
  world <- sf::st_read("Raster_borders/TM_WORLD_BORDERS_SIMPL-0.3.shp", quiet = TRUE)
  world <- sf::st_transform(world, 4326)
  # Manual decisions are exported from the review log by build_resource.py.
  add_review <- function(rows) {
    rows$verification_status <- "not_verified"
    rows$verified_by <- rows$verified_at <- rows$verification_note <- rows$fingerprint <- ""
    rows$publication_status <- "published"
    if (apply_reviews && file.exists("Data/curation/map_review.json")) {
      reviews <- jsonlite::fromJSON("Data/curation/map_review.json")
      index <- match(rows$record_id, reviews$record_id)
      for (key in setdiff(names(reviews), "record_id")) {
        rows[[key]][!is.na(index)] <- reviews[[key]][index[!is.na(index)]]
      }
      binding_path <- "Data/curation/map_review_inputs.json"
      current <- FALSE
      if (file.exists(binding_path)) {
        binding <- jsonlite::fromJSON(binding_path)
        current <- all(vapply(names(binding), function(path) {
          file.exists(path) && digest::digest(file = path, algo = "sha256") == binding[[path]]
        }, logical(1)))
      }
      if (!current) {
        rows$verification_status[rows$verification_status == "verified"] <- "needs_recheck"
        rows$fingerprint <- ""
        warning("Map review metadata is stale. Rebuild with scripts/build_site.sh before publishing.")
      }
      rows <- rows[rows$publication_status == "published", ]
    }
    rows
  }
  points <- add_review(points)
  countries <- add_review(countries)
  list(points = points, countries = countries, world = world, archives = archives, links = links)
}

# Stable colors retain the original mitochondrial families; study-specific labels stay distinct.
group_colors <- c(
  VD_K_type = "#e41a1c", VD_Japan_J1 = "#0066ff", VD_J_type = "#3875b9",
  KoreaJapan = "#9707e7", VD_Taiwan_T = "#008b8b", VD_J2_Lin2021 = "#6b58a5",
  VD_C4_Lin2021 = "#986545", VD_China_C1 = "#ff9933", VD_China_C2 = "#b800e6",
  VD_China_C3 = "#ff4dff", VD_China_C4 = "#663300", VD_Nepal_1 = "#993300",
  VD_Nepal_2 = "#af7183", VD_South_India_1 = "#b29800", VD_Sri_Lanka_1 = "#669999",
  VD_Viet_Nam_V1 = "#009900", VJ_Malaysia_1 = "#4dffff", VJ_Malaysia_2 = "#0099ff",
  VJ_Java_1 = "#0000e6", VJ_Ambon_1 = "#000000", VJ_Lombok_1 = "#00aa00",
  VJ_Bali_1 = "#006600", VJ_Bengaluru_1 = "#006666", VJ_Sumbawa_1 = "#ff8000",
  VJ_Sumatra_1 = "#d1a17b", VJ_Flores_1 = "#c5b600", VJ_Samui_1 = "#a64dff",
  VJ_Samui_2 = "#bda1cf", VJ_Borneo_1 = "#663300", VJ_Borneo_2 = "#896a4b",
  VJ_North_Thailand_1 = "#888888", VJ_North_Thailand_2 = "#666666",
  VJ_North_Thailand_3 = "#4d4d4d", VJ_Laos_L1 = "#cc0066", VJ_Papua_New_Guinea_1 = "#345c2c",
  VSP_Los_Banos_1 = "#333333", VSP_Luzon_1 = "#111111", VSP_Luzon_2 = "#555555", VSP_Mindanao = "#777777")
group_label <- function(group) {
  labels <- c(VD_K_type = "V. destructor K group", VD_Japan_J1 = "V. destructor J1",
    VD_J_type = "V. destructor J type (Hua 2023)", VD_Taiwan_T = "V. destructor T (Hua 2023)",
    VD_J2_Lin2021 = "V. destructor J2-1 (Lin 2021)", VD_C4_Lin2021 = "V. destructor C4-1 (Lin 2021)",
    VD_Nepal_2 = "V. destructor Nepal2 (Techer 2022)", KoreaJapan = "V. destructor K and J")
  fallback <- gsub("_", " ", sub("^VJ_", "V. jacobsoni ", sub("^VD_", "V. destructor ", group)))
  fallback <- sub("^VU ", "V. underwoodi ", sub("^VR ", "V. rindereri ", sub("^VSP ", "Varroa sp. ", fallback)))
  ifelse(group %in% names(labels), unname(labels[group]), fallback)
}

evidence_html <- function(row, country = FALSE) {
  year <- ifelse(row$Year %in% c("", "NA"), "unavailable", row$Year)
  unresolved <- row$GROUP == "VD_nuclear_evidence" || !nzchar(row$Haplogroup)
  mt <- if (unresolved) "unresolved / assignment withheld" else row$reported_mt
  precision <- if (country) "country polygon; named region only" else
    if (row$Coord.type == "exact") "published site coordinates" else "approximate locality"
  stamp <- if (row$is_new) "Added 2026" else "2021 baseline"
  paste0("<details class='evidence-record'><summary>", escape(paste(row$record_id, stamp, row$Host,
      year, row$reference, sep = " · ")), "</summary><p><b>", escape(row$Species), "</b><br>",
      "<b>Collection host:</b> ", escape(row$Host), "<br><b>Collection year:</b> ", escape(year),
      "<br><b>Locality:</b> ", escape(paste(row$Region, row$Location, sep = ", ")),
      "<br><b>Geographic precision:</b> ", precision,
      "<br><b>Reported mt label:</b> ", escape(mt),
      "<br><b>Manual verification:</b> ",
      if (row$verification_status == "verified") paste0("Manually verified by ", escape(row$verified_by), " · ", escape(row$verified_at)) else
        if (row$verification_status == "needs_recheck") "Needs recheck after a record change" else "Not yet verified",
      if (unresolved && nzchar(row$reported_mt)) paste0("<br><b>Unresolved COI report:</b> ", escape(row$reported_mt)) else "",
      "<br><b>Study and source evidence:</b> ", escape(row$Description), "</p>",
      row$archive_note, "</details>")
}

shared_popup <- function(data, country = FALSE) {
  # Every coincident record, including other hosts and studies, remains separately expandable.
  data <- data[order(!data$is_new), ] # Put this update first, with archival context still available.
  title <- if (country) "Country evidence" else "Evidence at this coordinate"
  paste0("<div class='evidence-popup'><b>", title, ": ", nrow(data), " records</b>",
    if (country) "<p>A polygon indicates occurrence in the named region, not uniform distribution across the country.</p>" else
      "<p>Open each study below. Shared coordinates do not establish shared specimens.</p>",
    paste(vapply(seq_len(nrow(data)), function(i) evidence_html(data[i, ], country), character(1)), collapse = ""), "</div>")
}
build_explorer <- function(data) {
  # Export display metadata alongside the untouched original fields for browser-side filtering.
  records <- c(lapply(seq_len(nrow(data$points)), function(i) {
    row <- as.list(data$points[i, ])
    row$evidence_level <- "point"
    row
  }), lapply(seq_len(nrow(data$countries)), function(i) {
    row <- as.list(data$countries[i, ])
    row$evidence_level <- "country"
    row
  }))
  geojson <- tempfile(fileext = ".geojson")
  sf::st_write(data$world[, "NAME", drop = FALSE], geojson, driver = "GeoJSON", quiet = TRUE)
  world <- jsonlite::fromJSON(geojson, simplifyVector = FALSE)
  references <- read_evidence("Data/updates/2026-11-05/reference_links.csv")
  groups <- sort(unique(c(data$points$GROUP, data$countries$GROUP)))
  colors <- group_colors[groups]
  # Less common species and unresolved labels still have a visible, neutral display color.
  colors[is.na(colors)] <- "#52646d"
  names(colors) <- groups
  labels <- setNames(group_label(groups), groups)
  # A restrained palette for the atlas; archival host maps keep their original colors.
  atlas_colors <- c(VD_K_type = "#c34b47", VD_Japan_J1 = "#386ca3", VD_J_type = "#598bb4",
    KoreaJapan = "#8b609b", VD_Taiwan_T = "#168c86", VD_J2_Lin2021 = "#7463a4",
    VD_C4_Lin2021 = "#9b7048", VD_China_C1 = "#dc943e", VD_China_C2 = "#bd6091",
    VD_China_C3 = "#ce8aa9", VD_China_C4 = "#805b43", VD_Viet_Nam_V1 = "#4c9060",
    VD_Nepal_1 = "#a56a44", VD_Nepal_2 = "#b87d87", VD_South_India_1 = "#a39135",
    VJ_Malaysia_1 = "#46a5ac", VJ_Malaysia_2 = "#4089b9", VJ_Java_1 = "#425c9b",
    VJ_Lombok_1 = "#559761", VJ_Bali_1 = "#367950", VJ_Sumbawa_1 = "#d38b42",
    VJ_Samui_1 = "#9673ac", VJ_Laos_L1 = "#b75c83")
  colors[names(atlas_colors)] <- atlas_colors
  leaflet::leaflet(height = 560, options = leaflet::leafletOptions(
      worldCopyJump = TRUE, zoomSnap = 0.25, zoomDelta = 0.5, minZoom = 0)) |>
    leaflet::setView(lng = 108, lat = 24, zoom = 3) |>
    htmlwidgets::onRender(paste(readLines("scripts/resource_explorer.js", warn = FALSE), collapse = "\n"),
      data = list(records = records, world = world, colors = as.list(colors), labels = as.list(labels),
        references = lapply(seq_len(nrow(references)), function(i) as.list(references[i, ]))))
}

build_map <- function(data, hosts, species, new_only = FALSE, center = c(0, 15, 2)) {
  points <- subset(data$points, Host %in% hosts & Species %in% species)
  countries <- subset(data$countries, Host %in% hosts & Species %in% species)
  if (new_only) {
    points <- subset(points, is_new)
    countries <- subset(countries, is_new)
  }
  mt_points <- subset(points, GROUP != "VD_nuclear_evidence" & nzchar(Haplogroup))
  mt_countries <- subset(countries, GROUP != "VD_nuclear_evidence" & nzchar(Haplogroup))
  groups <- sort(unique(c(mt_points$GROUP, mt_countries$GROUP)))
  stopifnot(all(groups %in% names(group_colors)))
  point_popups <- lapply(split(data$points, data$points$coordinate_key), shared_popup)
  country_popups <- lapply(split(data$countries, data$countries$border_name), shared_popup, country = TRUE)
  m <- leaflet::leaflet(options = leaflet::leafletOptions(preferCanvas = FALSE), height = 620) |>
    # Bundled borders provide a usable base even when external tile services change.
    leaflet::addPolygons(data = data$world, group = "Country outlines (offline)",
      color = "#bbc6c8", weight = 0.6, fillColor = "#f7f5ef", fillOpacity = 1,
      options = leaflet::pathOptions(interactive = FALSE)) |>
    leaflet::addTiles(group = "OpenStreetMap") |>
    leaflet::setView(lng = center[1], lat = center[2], zoom = center[3])
  add_points <- function(map, rows, color, layer) {
    if (!nrow(rows)) return(map)
    for (precision in unique(rows$Coord.type)) {
      q <- rows[rows$Coord.type == precision, ]
      q <- q[!duplicated(q$coordinate_key), ]
      # Filled circles distinguish published coordinates; hollow circles mark locality proxies.
      map <- leaflet::addCircleMarkers(map, data = q, lng = ~coord.Y, lat = ~coord.X,
        radius = 5.5, color = color, weight = 2, fill = TRUE,
        fillColor = if (precision == "exact") color else "white",
        fillOpacity = if (precision == "exact") 0.85 else 0.55,
        popup = unname(vapply(q$coordinate_key, function(k) point_popups[[k]], character(1))),
        label = paste(q$Region, q$Host, sep = " · "), group = layer,
        popupOptions = leaflet::popupOptions(maxWidth = 530, maxHeight = 360))
    }
    map
  }
  add_countries <- function(map, rows, color, layer, nuclear = FALSE) {
    if (!nrow(rows)) return(map)
    polygons <- data$world[data$world$NAME %in% rows$border_name, ]
    if (!nrow(polygons)) return(map)
    leaflet::addPolygons(map, data = polygons, group = layer, color = color,
      weight = if (nuclear) 3 else 1, opacity = 0.75,
      dashArray = if (nuclear) "5,5" else NULL, fillColor = color,
      fillOpacity = if (nuclear) 0.05 else 0.12,
      popup = unname(vapply(polygons$NAME, function(k) country_popups[[k]], character(1))),
      label = polygons$NAME, popupOptions = leaflet::popupOptions(maxWidth = 530, maxHeight = 360))
  }
  point_layers <- country_layers <- character()
  for (g in groups) {
    point_layer <- paste0("Points: ", group_label(g))
    country_layer <- paste0("Countries: ", group_label(g))
    p <- mt_points[mt_points$GROUP == g, ]
    c <- mt_countries[mt_countries$GROUP == g, ]
    m <- add_countries(m, c, group_colors[[g]], country_layer)
    m <- add_points(m, p, group_colors[[g]], point_layer)
    if (nrow(p)) point_layers <- c(point_layers, point_layer)
    if (nrow(c)) country_layers <- c(country_layers, country_layer)
  }
  nuclear_layer <- "Genome-wide evidence (WGS / RAD-seq)"
  m <- add_countries(m, subset(countries, nuclear), "#5b626b", nuclear_layer, TRUE)
  m <- add_points(m, subset(points, nuclear), "#5b626b", nuclear_layer)
  m <- leaflet::addLayersControl(m, position = "topright",
    baseGroups = c("Country outlines (offline)", "OpenStreetMap"),
    overlayGroups = c(point_layers, country_layers, nuclear_layer),
    options = leaflet::layersControlOptions(collapsed = TRUE)) |>
    leaflet::hideGroup("OpenStreetMap") |>
    leaflet::hideGroup(nuclear_layer)
  legend <- paste0("<details class='map-legend'><summary><b>Lineages and symbols</b></summary>",
    "<div>● published site coordinates<br>○ approximate locality<br>Shading: country evidence<br>",
    "Gray: optional genome-wide evidence</div><hr>",
    paste0("<div><span style='color:", unname(group_colors[groups]), "'>●</span> ",
      escape(group_label(groups)), "</div>", collapse = ""), "</details>")
  m <- leaflet::addControl(m, html = legend, position = "bottomleft")
  if (new_only) {
    # A record finder makes all additions accessible, including coincident points and country-only evidence.
    records <- lapply(seq_len(nrow(points)), function(i) {
      p <- points[i, ]
      list(id = p$input_id, label = paste(p$Location, p$Region, p$Host, p$reference, p$input_id, sep = " · "),
        layer = if (p$GROUP == "VD_nuclear_evidence") nuclear_layer else paste0("Points: ", group_label(p$GROUP)),
        lat = p$coord.X, lng = p$coord.Y, popup = point_popups[[p$coordinate_key]])
    })
    for (i in seq_len(nrow(countries))) {
      p <- countries[i, ]
      bbox <- sf::st_bbox(data$world[data$world$NAME == p$border_name, ])
      records[[length(records) + 1]] <- list(id = p$input_id,
        label = paste(p$Location, p$Region, p$Host, p$reference, "country evidence", sep = " · "),
        layer = paste0("Countries: ", group_label(p$GROUP)),
        bounds = list(c(unname(bbox["ymin"]), unname(bbox["xmin"])), c(unname(bbox["ymax"]), unname(bbox["xmax"]))),
        popup = country_popups[[p$border_name]])
    }
    choices <- paste(vapply(records, function(r) paste0("<option value='", r$id, "'>", escape(r$label), "</option>"), character(1)), collapse = "")
    m <- leaflet::addControl(m, html = paste0("<label class='record-finder'>Find added evidence<select aria-label='Find added evidence'>",
      "<option value=''>Choose a study record</option>", choices, "</select></label>"), position = "topleft")
    m <- htmlwidgets::onRender(m, "function(el, x, data) {
      var map = this, select = el.querySelector('.record-finder select');
      L.DomEvent.disableClickPropagation(select);
      L.DomEvent.disableScrollPropagation(select);
      select.addEventListener('change', function() {
        var record = data.records.find(function(r) { return r.id === select.value; });
        if (!record) return;
        map.layerManager.getLayerGroup(record.layer, true).addTo(map);
        if (record.bounds) map.fitBounds(record.bounds);
        else map.setView([record.lat, record.lng], 7);
        L.popup({maxWidth: 530, maxHeight: 360}).setLatLng(record.bounds ? map.getCenter() : [record.lat, record.lng])
          .setContent(record.popup).openOn(map);
      });
    }", data = list(records = records))
  }
  m
}
# Load tables, display labels and separately attributed archive notes.
map_data <- load_map_data()

Run the following from the repository root after installing rmarkdown, rmdformats, knitr, leaflet, htmltools, htmlwidgets, jsonlite, base64enc and sf, with Pandoc available. The map preparation code is shown in the Code panels above and stored in scripts/map_helpers.R. The searchable explorer is implemented in scripts/resource_explorer.js. Source data retain coord.Y as longitude and coord.X as latitude.

# Append only exact full rows that have not already been applied.
python3 scripts/apply_curated_update.py
# Build the interactive page from project-relative data and country borders.
PYTHON=.venv-resource/bin/python bash scripts/build_site.sh

Questions, corrections or new records are welcome: Maeva Techer.