knitr::opts_chunk$set( collapse = TRUE, comment = "#>", dev = "svglite", fig.ext = "svg", fig.width = 7, fig.height = 5 ) # Save par settings and restore on exit (CRAN policy) oldpar <- par(no.readonly = TRUE) knitr::knit_hooks$set(document = function(x) { par(oldpar); x })
The areaOfEffect package classifies spatial points by their position relative to a region's boundary—without requiring sf expertise.
Dataframe in → dataframe out.
Points are classified as:
Core: inside the original support
Halo: outside the original but inside the area of effect
Pruned: outside the AoE entirely (removed)
By default, halos are defined as equal area to the core—a proportion-based definition that enables consistent cross-region comparisons.
library(areaOfEffect) library(sf)
The simplest usage: pass a dataframe with coordinates and a country name.
# Your occurrence data observations <- data.frame( species = c("Oak", "Beech", "Pine", "Spruce"), lon = c(14.5, 15.2, 16.8, 20.0), lat = c(47.5, 48.1, 47.2, 48.5) ) # Classify relative to Austria result <- aoe(observations, "Austria") result$aoe_class #> [1] "core" "core" "halo"
The package auto-detects coordinate columns (lon/lat, x/y, longitude/latitude, etc.).
sf objects work directly:
result <- aoe(pts_sf, "AT")
# Get Austria and transform to equal-area projection austria <- get_country("AT") austria_ea <- st_transform(austria, "ESRI:54009") # Create a point inside Austria dummy_pt <- st_centroid(austria_ea) # Run aoe() to get geometries (uses buffer method by default) result <- aoe(dummy_pt, austria_ea) geoms <- aoe_geometry(result, "both") # Extract geometries austria_geom <- geoms[geoms$type == "original", ] aoe_geom <- geoms[geoms$type == "aoe", ] # Plot par(mar = c(1, 1, 1, 1), bty = "n") plot(st_geometry(aoe_geom), border = "steelblue", lty = 2, lwd = 1.5) plot(st_geometry(austria_geom), border = "black", lwd = 2, add = TRUE) legend("topright", legend = c("Austria (core)", "Area of Effect"), col = c("black", "steelblue"), lty = c(1, 2), lwd = c(2, 1.5), inset = 0.02)
support <- st_as_sf( data.frame(id = 1), geometry = st_sfc(st_polygon(list( cbind(c(0, 100, 100, 0, 0), c(0, 0, 100, 100, 0)) ))), crs = 32631 )
Create observation points:
pts <- st_as_sf( data.frame( id = 1:5, value = c(10, 20, 15, 25, 30) ), geometry = st_sfc( st_point(c(50, 50)), # center st_point(c(10, 10)), # near corner st_point(c(95, 50)), # near edge st_point(c(120, 50)), # outside, in halo st_point(c(250, 250)) # far outside ), crs = 32631 )
Apply the area of effect:
result <- aoe(pts, support) print(result)
The result contains only points inside the AoE, with their classification:
result$aoe_class
Process multiple regions at once:
# Two adjacent regions supports <- st_as_sf( data.frame(region = c("A", "B")), geometry = st_sfc( st_polygon(list(cbind(c(0, 50, 50, 0, 0), c(0, 0, 100, 100, 0)))), st_polygon(list(cbind(c(50, 100, 100, 50, 50), c(0, 0, 100, 100, 0)))) ), crs = 32631 ) # Points that may fall in overlapping AoEs pts_multi <- st_as_sf( data.frame(id = 1:3), geometry = st_sfc( st_point(c(25, 50)), # inside A st_point(c(50, 50)), # on boundary st_point(c(75, 50)) # inside B ), crs = 32631 ) result_multi <- aoe(pts_multi, supports) print(result_multi)
Points can appear multiple times (once per support whose AoE contains them).
For coastal regions, sea is a hard boundary. Provide a mask to constrain the AoE:
# Create a coastal support support_coast <- st_as_sf( data.frame(id = 1), geometry = st_sfc(st_polygon(list( cbind(c(40, 80, 80, 40, 40), c(20, 20, 60, 60, 20)) ))), crs = 32631 ) # Create land mask (irregular coastline) land_mask <- st_as_sf( data.frame(id = 1), geometry = st_sfc(st_polygon(list(cbind( c(0, 100, 100, 70, 50, 30, 0, 0), c(0, 0, 50, 60, 55, 70, 60, 0) )))), crs = 32631 ) # Create some points pts_coast <- st_as_sf( data.frame(id = 1:4), geometry = st_sfc( st_point(c(60, 40)), # core st_point(c(50, 30)), # core st_point(c(30, 40)), # halo (on land) st_point(c(90, 70)) # would be halo but in sea ), crs = 32631 ) # Apply with mask result_coast <- aoe(pts_coast, support_coast, mask = land_mask) # Get geometries for visualization aoe_masked <- aoe_geometry(result_coast, "aoe") support_geom <- aoe_geometry(result_coast, "original") par(mar = c(1, 1, 1, 1), bty = "n") plot(st_geometry(land_mask), col = NA, border = "steelblue", lwd = 2, xlim = c(-10, 110), ylim = c(-10, 90)) plot(st_geometry(aoe_masked), col = rgb(0.5, 0.5, 0.5, 0.3), border = "steelblue", lty = 2, add = TRUE) plot(st_geometry(support_geom), border = "black", lwd = 2, add = TRUE) # Add points with colors cols <- ifelse(result_coast$aoe_class == "core", "forestgreen", "darkorange") plot(st_geometry(result_coast), col = cols, pch = 16, cex = 1.5, add = TRUE) # Show pruned point plot(st_geometry(pts_coast)[4], col = "gray60", pch = 4, cex = 1.2, add = TRUE) text(85, 75, "SEA", col = "steelblue", font = 2, cex = 1.2) legend("topleft", legend = c("Support", "AoE (masked)", "Coastline", "Core", "Halo", "Pruned"), col = c("black", "steelblue", "steelblue", "forestgreen", "darkorange", "gray60"), lty = c(1, 2, 1, NA, NA, NA), lwd = c(2, 1, 2, NA, NA, NA), pch = c(NA, NA, NA, 16, 16, 4), pt.cex = c(NA, NA, NA, 1.5, 1.5, 1.2), inset = 0.02)
The package includes bundled country boundaries and a global land mask. Use mask = "land"
to clip AoE to coastlines:
# Create a point inside Portugal (approximate center of mainland) dummy <- st_as_sf( data.frame(id = 1), geometry = st_sfc(st_point(c(-8, 39.5))), crs = 4326 ) # Without mask result_no_mask <- aoe(dummy, "PT") aoe_no_mask <- aoe_geometry(result_no_mask, "aoe") # With mask + area=1 for equal land area result_masked <- aoe(dummy, "PT", mask = "land", area = 1) aoe_masked <- aoe_geometry(result_masked, "aoe") # Get support geometry support_geom <- aoe_geometry(result_masked, "original") # Transform to equal area for plotting crs_ea <- st_crs("+proj=laea +lat_0=39.5 +lon_0=-8 +datum=WGS84") aoe_no_mask_ea <- st_transform(aoe_no_mask, crs_ea) aoe_masked_ea <- st_transform(aoe_masked, crs_ea) support_ea <- st_transform(support_geom, crs_ea) # Plot - expand xlim for legend, crop bottom margin bbox <- st_bbox(aoe_no_mask_ea) x_range <- bbox[3] - bbox[1] y_range <- bbox[4] - bbox[2] par(mar = c(1, 1, 1, 1), bty = "n") plot(st_geometry(aoe_no_mask_ea), border = "gray50", lty = 2, lwd = 1.5, xlim = c(bbox[1], bbox[3]), ylim = c(bbox[2] + y_range * 0.25, bbox[4]), axes = FALSE, xaxt = "n", yaxt = "n") plot(st_geometry(aoe_masked_ea), col = rgb(0.3, 0.5, 0.7, 0.3), border = "steelblue", lty = 2, lwd = 1.5, add = TRUE) plot(st_geometry(support_ea), border = "black", lwd = 2, add = TRUE) legend("topright", legend = c("Portugal", "AoE (unmasked)", "AoE (land only)"), col = c("black", "gray50", "steelblue"), lty = c(1, 2, 2), lwd = c(2, 1.5, 1.5), bty = "n", inset = 0.05)
The area = 1 parameter ensures the halo has equal land area to the core, even after
the ocean is masked out. Without this, coastline clipping would reduce the effective halo area.
The scale parameter controls halo size as a proportion of core area.
# Default: equal core/halo areas (scale = sqrt(2) - 1) result_default <- aoe(pts, support) # Scale = 1: larger halo (3:1 area ratio) result_large <- aoe(pts, support, scale = 1)
| Scale | Halo:Core Area |
|-------|----------------|
| sqrt(2) - 1 (default) | 1:1 |
| 1 | 3:1 |
| 0.5 | 1.25:1 |
Sometimes you need a specific halo area regardless of masking. The area parameter
specifies the target halo area as a proportion of the original support:
# Halo area = original area (same as scale = sqrt(2) - 1 without mask) result <- aoe(pts, support, area = 1) # Halo area = half of original result <- aoe(pts, support, area = 0.5)
Unlike scale, area accounts for masking: the function finds the scale that
produces the target halo area after mask intersection. This is useful for
coastal regions where scale alone would produce inconsistent effective areas.
# Target area = 1 means halo = original, even after coastline clipping result <- aoe(pts, support, area = 1, mask = "land")
aoe_expand()When some supports have too few points at baseline AoE, aoe_expand() finds the
minimum scale needed to capture a target number of points:
# Create sparse data set.seed(42) pts_sparse <- st_as_sf( data.frame(id = 1:15), geometry = st_sfc(c( lapply(1:5, function(i) st_point(c(runif(1, 20, 80), runif(1, 20, 80)))), lapply(1:10, function(i) st_point(c(runif(1, -50, 150), runif(1, -50, 150)))) )), crs = 32631 ) # Expand until at least 10 points are captured result_expand <- aoe_expand(pts_sparse, support, min_points = 10)
Two safety caps prevent unreasonable expansion:
max_area = 2 (default): halo area cannot exceed 2× the original
max_dist: maximum expansion distance in CRS units
# Strict caps result <- aoe_expand(pts, support, min_points = 50, max_area = 1.5, # halo ≤ 1.5× original max_dist = 500) # max 500m expansion
Check expansion details:
info <- attr(result_expand, "expansion_info") info
aoe_sample()Core regions often dominate due to point density. aoe_sample() provides
stratified sampling to balance core/halo representation:
# Create imbalanced data (many core, few halo) set.seed(42) pts_imbal <- st_as_sf( data.frame(id = 1:60), geometry = st_sfc(c( lapply(1:50, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90)))), lapply(1:10, function(i) st_point(c(runif(1, 110, 140), runif(1, 10, 90)))) )), crs = 32631 ) result_imbal <- aoe(pts_imbal, support, scale = 1) # Default: balance core/halo (downsamples core to match halo) set.seed(123) balanced <- aoe_sample(result_imbal) table(balanced$aoe_class)
Custom ratios and fixed sample sizes:
# Fixed n with 70/30 split set.seed(123) sampled <- aoe_sample(result_imbal, n = 20, ratio = c(core = 0.7, halo = 0.3)) table(sampled$aoe_class)
For multiple supports, use by = "support" to sample within each:
sampled <- aoe_sample(result_multi, by = "support")
aoe_border()When your study involves a boundary line rather than a polygon (e.g., a river, mountain
range, or political border), use aoe_border() to classify points by their distance from
and side of the border.
# Create a diagonal border line border_line <- st_as_sf( data.frame(id = 1), geometry = st_sfc(st_linestring(matrix( c(0, 0, 100, 100), ncol = 2, byrow = TRUE ))), crs = 32631 ) # Create points on both sides set.seed(42) pts_border <- st_as_sf( data.frame(id = 1:30), geometry = st_sfc(c( # Points on side 1 (above the line) lapply(1:15, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90) + 20))), # Points on side 2 (below the line) lapply(1:15, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90) - 20))) )), crs = 32631 ) # Classify by distance from border result_border <- aoe_border( pts_border, border_line, width = 30, side_names = c("north", "south") ) # Built-in plot method plot(result_border)
The aoe_border() function:
Creates symmetric buffer zones on both sides of the border
Classifies points as "core" (near border) or "halo" (farther away)
Assigns each point to a side based on position relative to the line
Use the area parameter to specify target zone areas instead of fixed widths:
# Each side's core zone has area 5000 (in CRS units²) result <- aoe_border(pts, border, area = 5000)
aoe_sample() also works with border results, allowing stratification by side or class:
# Balance by side (equal north/south) set.seed(123) balanced_side <- aoe_sample(result_border, ratio = c(north = 0.5, south = 0.5)) table(balanced_side$side) # Balance by distance class set.seed(123) balanced_class <- aoe_sample(result_border, by = "class") table(balanced_class$aoe_class)
aoe_summary(result)
aoe() classifies points as "core" or "halo"
Works with dataframes or sf objects
Pass country codes directly: aoe(df, "AT")
Area-based halos enable consistent cross-country comparisons
Use mask for coastlines and hard boundaries
Use area parameter for target halo area (accounts for masking)
Use aoe_expand() for adaptive expansion to capture minimum points
Use aoe_sample() for balanced core/halo sampling
Use aoe_border() for border/line-based classification
Use aoe_summary() for diagnostics
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.