explodemap generates hierarchical exploded-view maps for
dense administrative boundary data. It applies rigid-body translations
to polygon geometries so that features are visually separated while each
feature’s internal geometry is preserved exactly.
The basic two-level workflow is:
For the two-level core described in the paper, the package is designed to satisfy three key properties:
alpha_r + alpha_l metres.explodemap expects an sf object with:
Geographic coordinates such as EPSG:4326 (longitude/latitude) must be transformed before use. For U.S. work, a state plane, UTM, or Albers-type projected CRS is usually appropriate.
We create a small synthetic dataset with four square polygons in two regions.
sq <- function(xmin, ymin, size = 1000) {
st_polygon(list(matrix(
c(xmin, ymin,
xmin + size, ymin,
xmin + size, ymin + size,
xmin, ymin + size,
xmin, ymin),
ncol = 2,
byrow = TRUE
)))
}
geom <- st_sfc(
sq(0, 0), sq(3000, 0), # Region A
sq(12000, 0), sq(15000, 0), # Region B
crs = 3857
)
x <- st_sf(
id = c("a1", "a2", "b1", "b2"),
region = c("A", "A", "B", "B"),
geometry = geom
)
x
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POLYGON
#> Dimension: XY
#> Bounding box: xmin: 0 ymin: 0 xmax: 16000 ymax: 1000
#> Projected CRS: WGS 84 / Pseudo-Mercator
#> id region geometry
#> 1 a1 A POLYGON ((0 0, 1000 0, 1000...
#> 2 a2 A POLYGON ((3000 0, 4000 0, 4...
#> 3 b1 B POLYGON ((12000 0, 13000 0,...
#> 4 b2 B POLYGON ((15000 0, 16000 0,...The simplest entry point is explode_sf(). Pass your sf
object and the name of the grouping column:
In Shiny or other non-interactive pipelines, add
quiet = TRUE to suppress progress messages:
quiet_result <- explode_sf(x, region_col = "region", plot = FALSE, quiet = TRUE)
class(quiet_result)
#> [1] "exploded_map" "list"The returned object is an S3 object of class
exploded_map:
class(result)
#> [1] "exploded_map" "list"
names(result)
#> [1] "sf_orig" "sf_exp" "sf_exp_wgs" "stats"
#> [5] "params" "gamma_r_implied" "gamma_l_implied" "plots"
#> [9] "refinement" "diagnostics"It contains the original and exploded geometries, a WGS84 export-ready version, derived statistics and parameters, plots, and diagnostics.
print() shows geometry statistics and derived
parameters:
print(result)
#>
#> -- Custom Dataset ----------------------------------------
#> n units : 4
#> n regions : 2
#> w_bar : 1.1 km
#> R_local : 1.5 km
#> n_bar : 2
#> R_local/w : 1.33
#> alpha_r : 1.7 km
#> alpha_l : 2.4 km
#> p : 1.25
#> max ||t|| : 4.1 km (Proposition 3 bound)summary() adds implied gamma coefficients that are
useful for calibration work:
summary(result)
#>
#> Exploded Map Summary
#> ====================
#> Dataset: Custom Dataset
#> Units: 4
#> Regions: 2
#> Grouped by: region
#>
#> Geometry Statistics
#> Characteristic diameter (w_bar): 1.1 km
#> Regional radius (R_local): 1.5 km
#> Median units/region (n_bar): 2
#> Tightness ratio (R_local/w_bar): 1.33
#>
#> Parameters
#> alpha_r: 1.7 km (regional separation)
#> alpha_l: 2.4 km (local expansion)
#> p: 1.25
#>
#> Implied Gamma Coefficients
#> gamma_r: 3
#> gamma_l: 1.136You can also view both original and exploded layouts side by side:
calibration_row() returns a one-row data frame suitable
for combining across datasets when building a cross-dataset calibration
table:
By default, explodemap derives displacement parameters
analytically from dataset geometry using the paper’s two closed-form
results. You can also supply parameters directly. Overrides may be
supplied independently, so you can tune regional separation without
changing local expansion, or vice versa:
manual <- explode_sf(
x,
region_col = "region",
alpha_r = 100,
alpha_l = 200,
plot = FALSE
)
#> Using manual alpha_r = 100 m
#> Using manual alpha_l = 200 m
manual$params
#> $alpha_r
#> [1] 100
#>
#> $alpha_l
#> [1] 200
#>
#> $p
#> [1] 1.25
#>
#> $gamma_r
#> [1] NA
#>
#> $gamma_l
#> [1] NA
#>
#> $refine
#> [1] FALSEmore_region_gap <- explode_sf(
x,
region_col = "region",
alpha_r = result$params$alpha_r * 1.5,
plot = FALSE
)
#> Using manual alpha_r = 2538.8531259649 m
more_region_gap$params
#> $alpha_r
#> [1] 2538.853
#>
#> $alpha_l
#> [1] 2409.82
#>
#> $p
#> [1] 1.25
#>
#> $gamma_r
#> [1] NA
#>
#> $gamma_l
#> [1] 1.136
#>
#> $refine
#> [1] FALSEThe two-level algorithm is the clean paper model. For dense municipal cores, you can add a bounded refinement pass that nudges close same-region neighbors apart after the analytical displacement:
refined <- explode_sf(
x,
region_col = "region",
refine = TRUE,
refine_min_gap = 0.15,
refine_max_shift = 0.10,
plot = FALSE
)
refined$refinement#> Collision refinement: corrected 0 pair-visits; max shift = 0.0 m.
#> $enabled
#> [1] TRUE
#>
#> $min_gap
#> [1] 0.15
#>
#> $max_shift
#> [1] 0.1
#>
#> $max_iter
#> [1] 20
#>
#> $step
#> [1] 0.5
#>
#> $within
#> [1] "region"
#>
#> $iterations
#> [1] 1
#>
#> $corrected_pairs
#> [1] 0
#>
#> $active_pairs_last
#> [1] 0
#>
#> $max_shift_observed
#> [1] 0
refine_max_shift caps the extra correction per feature,
so the refinement remains a small display adjustment rather than a
replacement for the displacement model. Use
refine_within = "all" when the remaining crowding crosses
region boundaries.
For irregular or multipart polygons, "point_on_surface"
may be preferable to the default geometric centroid:
explode_state() downloads U.S. Census TIGER/Line
boundaries automatically and groups municipalities by a county-to-region
mapping:
nj <- explode_state(
state_fips = "34", crs = 32118,
region_map = list(
North = c("Bergen", "Essex", "Hudson", "Morris",
"Passaic", "Sussex", "Union", "Warren"),
Central = c("Hunterdon", "Mercer", "Middlesex",
"Monmouth", "Somerset"),
South = c("Atlantic", "Burlington", "Camden", "Cape May",
"Cumberland", "Gloucester", "Ocean", "Salem")
),
label = "New Jersey"
)Downloaded data is cached locally so subsequent runs are faster.
Use quiet = TRUE in app code if downloads and region
assignment happen inside a reactive expression.
explode_sf_with_lookup() joins an external lookup table
to your sf object before exploding:
groups <- read.csv("region_assignments.csv")
result <- explode_sf_with_lookup(
my_sf,
join_col = "GEOID",
lookup = groups,
lookup_key = "geoid",
region_col = "region"
)Unmatched units are labelled "Other". This is useful
when a lookup table is incomplete. You can include or exclude those
units using allow_other.
The export parameter supports three modes:
NULL (default): no exportTRUE: auto-named GeoJSON filefocus_map() renders raw sf,
exploded_map, or grouped_exploded_map objects
as an interactive htmlwidget. Click a polygon to zoom and lift it into
focus; right-click or press Escape to reset. Information cards can show
selected fields without blocking the map.
focus_map(
result,
label_col = "id",
id_col = "id",
group_col = "region",
group_palette = c(A = "#4C78A8", B = "#F58518", C = "#54A24B"),
info_cols = c("id", "region"),
info_card_scale = 0.95
)In Shiny, use focusmapOutput() and
renderFocusmap(). The widget emits
input$<outputId>_selected, which includes the
selected feature ID, label, group, and properties.
ui <- shiny::fluidPage(
focusmapOutput("map", height = "650px"),
shiny::verbatimTextOutput("selected")
)
server <- function(input, output, session) {
exploded <- explode_sf(x, "region", plot = FALSE, quiet = TRUE)
output$map <- renderFocusmap({
focus_map(
exploded,
label_col = "id",
id_col = "id",
group_col = "region",
group_palette = c(A = "#4C78A8", B = "#F58518", C = "#54A24B")
)
})
output$selected <- shiny::renderPrint(input$map_selected)
}For section drill-downs, use explode_section() before
rendering. It explodes the selected section and marks the rest of the
layer as context:
focused <- explode_section(
x,
section_col = "region",
section = "A",
region_col = "county",
alpha_r = 900,
alpha_l = 600,
plot = FALSE,
quiet = TRUE
)
focus_map(
focused,
label_col = "id",
context_col = ".explodemap_role",
context_mode = "fade"
)Small municipal features can use adaptive focus sizing so the selected polygon is not left visually distant after the camera moves:
focus_map(
focused,
label_col = "id",
context_col = ".explodemap_role",
context_mode = "fade",
min_focus_width = 115,
min_focus_height = 88,
tiny_feature_threshold = 52,
tiny_feature_boost = 1.45,
origin_context = "inset",
origin_context_position = "bottom-left",
focus_context_opacity = 0.14,
show_drag_zoom = TRUE
)See vignette("grouped-layouts") for the three-level
extension using explode_grouped(), which adds region-block
anchor placement for larger multi-region or national layouts.