The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
Shared grids across datasets, polygon generation, resolution selection, and GIS export.
Define a grid once, reuse it everywhere. Same grid object = guaranteed spatial alignment across datasets.
You often have:
Several independent datasets (observations, sensors, surveys)
All in longitude/latitude coordinates
Collected at different times or from different sources
You want to:
Put everything on one common global grid
Be sure the grids actually match
Combine results later without subtle errors
Once data are hexified, longitude/latitude no longer matter for
analysis. The cell_id becomes the shared spatial key:
# Extract data frames with cell IDs
birds_df <- as.data.frame(birds)
birds_df$cell_id <- birds@cell_id
mammals_df <- as.data.frame(mammals)
mammals_df$cell_id <- mammals@cell_id
climate_df <- as.data.frame(climate)
climate_df$cell_id <- climate@cell_id
# Aggregate each dataset by cell
bird_richness <- aggregate(
species ~ cell_id,
data = birds_df,
FUN = function(x) length(unique(x))
)
names(bird_richness)[2] <- "bird_species"
mammal_richness <- aggregate(
species ~ cell_id,
data = mammals_df,
FUN = function(x) length(unique(x))
)
names(mammal_richness)[2] <- "mammal_species"
mean_temp <- aggregate(
temp_c ~ cell_id,
data = climate_df,
FUN = mean
)
names(mean_temp)[2] <- "mean_temp"
# Join datasets by cell_id - guaranteed to align because same grid
combined <- merge(bird_richness, mammal_richness, by = "cell_id", all = TRUE)
combined <- merge(combined, mean_temp, by = "cell_id", all = TRUE)
head(combined)
#> cell_id bird_species mammal_species mean_temp
#> 1 6634 NA 1 12.18894
#> 2 6635 1 NA NA
#> 3 6716 NA 1 NA
#> 4 6718 1 NA NA
#> 5 6800 NA 1 NA
#> 6 6882 NA 1 NAhexify provides functions to generate grid polygons over regions for visualization and analysis.
# Create grid specification
grid <- hex_grid(area_km2 = 5000)
# Generate hexagons over Western Europe
europe_hexes <- grid_rect(c(-10, 35, 25, 60), grid)
# Get European countries for context
europe <- hexify_world[hexify_world$continent == "Europe", ]
ggplot() +
geom_sf(data = europe, fill = "gray95", color = "gray60") +
geom_sf(data = europe_hexes, fill = NA, color = "steelblue", linewidth = 0.4) +
coord_sf(xlim = c(-10, 25), ylim = c(35, 60)) +
labs(title = sprintf("Hexagonal Grid (~%.0f km² cells)", grid@area_km2)) +
theme_minimal(base_size = FIG_BASE_SIZE)Clip a grid to any sf polygon boundary:
# Get France boundary
france <- hexify_world[hexify_world$name == "France", ]
# Generate grid covering mainland France
grid <- hex_grid(area_km2 = 2000)
france_grid <- grid_rect(c(-5, 41, 10, 52), grid)
# Clip grid to France boundary
france_grid_clipped <- st_intersection(france_grid, st_geometry(france))
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries
ggplot() +
geom_sf(data = france, fill = "gray95", color = "gray40", linewidth = 0.5) +
geom_sf(data = france_grid_clipped, fill = alpha("steelblue", 0.3),
color = "steelblue", linewidth = 0.3) +
coord_sf(xlim = c(-5, 10), ylim = c(41, 52)) +
labs(title = "Hexagonal Grid Clipped to France",
subtitle = sprintf("~%.0f km² cells", grid@area_km2)) +
theme_minimal(base_size = FIG_BASE_SIZE)# Coarse global grid (be careful with fine grids - many cells!)
grid <- hex_grid(area_km2 = 500000)
global_hexes <- grid_global(grid)
ggplot() +
geom_sf(data = hexify_world, fill = "gray90", color = "gray70", linewidth = 0.2) +
geom_sf(data = global_hexes, fill = NA, color = "darkgreen", linewidth = 0.3) +
labs(title = sprintf("Global Hexagonal Grid (~%.0f km² cells)", grid@area_km2)) +
theme_minimal(base_size = FIG_BASE_SIZE) +
theme(axis.text = element_blank(), axis.ticks = element_blank())Analyze data at multiple spatial scales using different target areas.
# Sample data
set.seed(42)
observations <- data.frame(
species = sample(c("Species A", "Species B", "Species C"), 100, replace = TRUE),
lon = runif(100, -10, 30),
lat = runif(100, 35, 60)
)
# Fine resolution (~1000 km² cells)
grid_fine <- hex_grid(area_km2 = 1000)
obs_fine <- hexify(observations, lon = "lon", lat = "lat", grid = grid_fine)
# Coarse resolution (~10000 km² cells)
grid_coarse <- hex_grid(area_km2 = 10000)
obs_coarse <- hexify(observations, lon = "lon", lat = "lat", grid = grid_coarse)
cat(sprintf("Fine resolution: %d unique cells (area: %.1f km²)\n",
n_cells(obs_fine), grid_fine@area_km2))
#> Fine resolution: 98 unique cells (area: 863.8 km²)
cat(sprintf("Coarse resolution: %d unique cells (area: %.1f km²)\n",
n_cells(obs_coarse), grid_coarse@area_km2))
#> Coarse resolution: 93 unique cells (area: 7774.0 km²)# Extract data with cell IDs
fine_df <- as.data.frame(obs_fine)
fine_df$cell_id <- obs_fine@cell_id
coarse_df <- as.data.frame(obs_coarse)
coarse_df$cell_id <- obs_coarse@cell_id
# Species richness at each scale
richness_fine <- aggregate(species ~ cell_id, data = fine_df,
FUN = function(x) length(unique(x)))
richness_coarse <- aggregate(species ~ cell_id, data = coarse_df,
FUN = function(x) length(unique(x)))
cat(sprintf("Fine scale: mean %.2f species per cell\n", mean(richness_fine$species)))
#> Fine scale: mean 1.01 species per cell
cat(sprintf("Coarse scale: mean %.2f species per cell\n", mean(richness_coarse$species)))
#> Coarse scale: mean 1.06 species per cellJoin datasets based on shared grid cells rather than proximity.
# Dataset 1: Weather stations
stations <- data.frame(
station_id = paste0("ST", 1:50),
lon = runif(50, -10, 30),
lat = runif(50, 35, 60),
temperature = rnorm(50, 15, 5)
)
# Dataset 2: Cities
cities <- data.frame(
city = c("Vienna", "Paris", "London", "Berlin", "Rome",
"Madrid", "Prague", "Warsaw", "Budapest", "Amsterdam"),
lon = c(16.37, 2.35, -0.12, 13.40, 12.50,
-3.70, 14.42, 21.01, 19.04, 4.90),
lat = c(48.21, 48.86, 51.51, 52.52, 41.90,
40.42, 50.08, 52.23, 47.50, 52.37)
)
# Use a coarse grid for joining disparate points
grid <- hex_grid(area_km2 = 50000)
# Hexify both datasets with the same grid
stations_hex <- hexify(stations, lon = "lon", lat = "lat", grid = grid)
cities_hex <- hexify(cities, lon = "lon", lat = "lat", grid = grid)
# Extract with cell IDs
stations_df <- as.data.frame(stations_hex)
stations_df$cell_id <- stations_hex@cell_id
cities_df <- as.data.frame(cities_hex)
cities_df$cell_id <- cities_hex@cell_id
# Join by cell_id
city_weather <- merge(
cities_df[, c("city", "cell_id")],
aggregate(temperature ~ cell_id, data = stations_df, FUN = mean),
by = "cell_id",
all.x = TRUE
)
city_weather
#> cell_id city temperature
#> 1 865 London NA
#> 2 1478 Madrid NA
#> 3 1482 Paris NA
#> 4 1484 Amsterdam NA
#> 5 1540 Berlin NA
#> 6 1567 Prague 11.60463
#> 7 1591 Rome NA
#> 8 1594 Vienna 12.45533
#> 9 1621 Budapest NA
#> 10 2240 Warsaw 7.30659hex_summarize() aggregates data per cell without
manually splitting and merging, and get_neighbors() finds
the ring of cells surrounding a given cell — useful for smoothing,
spatial lag features, or checking what surrounds a hotspot.
# Aggregate the station data straight from the HexData object
hex_summarize(stations_hex, mean_temp = mean(temperature), n_stations = length(temperature))
#> cell_id cell_cen_lon cell_cen_lat cell_area_km2 n_points mean_temp
#> 1 1486 8.8605921 56.44133 69948.66 1 4.33327138
#> 2 1617 11.2500000 37.27999 69948.66 3 13.86698499
#> 3 756 2.7876081 59.40683 69948.66 1 14.78591715
#> 4 1702 23.6524007 40.11980 69948.66 1 22.07036539
#> 5 1533 0.9055244 38.03217 69948.66 2 15.79263349
#> 6 784 3.8134141 56.73586 69948.66 2 17.33034539
#> 7 891 -6.9845461 48.48206 69948.66 1 19.34760702
#> 8 2240 22.3252813 52.74188 69948.66 1 7.30659018
#> 9 1562 5.9134033 39.97473 69948.66 1 13.69252087
#> 10 836 -8.8840946 52.56766 69948.66 2 19.77801460
#> 11 918 -8.1917714 46.29966 69948.66 1 12.47700445
#> 12 2941 28.7694286 59.95664 69948.66 1 17.23507876
#> 13 2211 21.3956718 48.53188 69948.66 2 8.61959723
#> 14 1756 26.2563977 35.79230 69948.66 1 7.72706532
#> 15 1588 6.1897627 35.80560 69948.66 1 16.79208370
#> 16 1566 13.2634952 47.96291 69948.66 2 14.03583407
#> 17 864 -5.7057188 50.63018 69948.66 1 20.44733536
#> 18 919 -4.3481495 46.43340 69948.66 1 0.09218036
#> 19 2242 18.6865859 56.73586 69948.66 1 23.30899470
#> 20 1589 7.7872318 37.75542 69948.66 1 14.54380966
#> 21 2943 19.7123917 59.40683 69948.66 2 16.62937840
#> 22 2235 29.4014718 42.06641 69948.66 1 18.96105332
#> 23 1564 9.3676906 43.78074 69948.66 1 9.41982974
#> 24 1567 15.4482390 50.29623 69948.66 1 11.60462511
#> 25 1534 2.3674390 40.11958 69948.66 1 6.70912517
#> 26 2267 26.8454720 52.74001 69948.66 1 14.80152001
#> 27 1645 14.7127682 37.75542 69948.66 1 13.94307721
#> 28 1618 13.0258784 39.62728 69948.66 1 10.48546862
#> 29 1619 14.9098954 41.91219 69948.66 1 12.45167799
#> 30 2268 25.3924346 54.80755 69948.66 1 5.79488808
#> 31 1537 7.3499007 46.08969 69948.66 1 10.36833374
#> 32 1560 2.8450855 35.92015 69948.66 1 19.41016981
#> 33 1477 -5.8644255 37.80961 69948.66 1 21.22466440
#> 34 1509 3.4280982 46.36031 69948.66 2 10.81345455
#> 35 809 -6.7691252 55.02709 69948.66 1 18.67035320
#> 36 1594 17.3249995 48.35421 69948.66 1 12.45533116
#> 37 2206 28.3644255 37.80961 69948.66 1 12.86376434
#> 38 1481 -0.4835992 46.44662 69948.66 1 11.88084926
#> 39 946 -5.6635964 44.26334 69948.66 1 15.98975606
#> 40 1538 9.2365048 47.96291 69948.66 1 22.04184729
#> 41 2944 15.3912479 58.91103 69948.66 1 16.88313865
#> n_stations
#> 1 1
#> 2 3
#> 3 1
#> 4 1
#> 5 2
#> 6 2
#> 7 1
#> 8 1
#> 9 1
#> 10 2
#> 11 1
#> 12 1
#> 13 2
#> 14 1
#> 15 1
#> 16 2
#> 17 1
#> 18 1
#> 19 1
#> 20 1
#> 21 2
#> 22 1
#> 23 1
#> 24 1
#> 25 1
#> 26 1
#> 27 1
#> 28 1
#> 29 1
#> 30 1
#> 31 1
#> 32 1
#> 33 1
#> 34 2
#> 35 1
#> 36 1
#> 37 1
#> 38 1
#> 39 1
#> 40 1
#> 41 1# The 6 cells surrounding a given cell (k = 1), or a wider ring with k > 1
some_cell <- stations_hex@cell_id[1]
get_neighbors(some_cell, grid)
#> [[1]]
#> [1] 1 757 784 1485 1513 2215
get_neighbors(some_cell, grid, k = 2)
#> [[1]]
#> [1] 1 757 784 1485 1513 2215 28 2944 55 756 783 811 1484 1512 1540
#> [16] 2214 2242Use hex_grid() with area_km2 to get the
closest available resolution:
# Target: 100 km² cells
grid_100 <- hex_grid(area_km2 = 100, aperture = 3)
cat(sprintf("Target ~100 km²: resolution %d (actual: %.1f km²)\n",
grid_100@resolution, grid_100@area_km2))
#> Target ~100 km²: resolution 12 (actual: 96.0 km²)
# Target: 1000 km² cells
grid_1000 <- hex_grid(area_km2 = 1000, aperture = 3)
cat(sprintf("Target ~1000 km²: resolution %d (actual: %.1f km²)\n",
grid_1000@resolution, grid_1000@area_km2))
#> Target ~1000 km²: resolution 10 (actual: 863.8 km²)
# Target: 10000 km² cells
grid_10000 <- hex_grid(area_km2 = 10000, aperture = 3)
cat(sprintf("Target ~10000 km²: resolution %d (actual: %.1f km²)\n",
grid_10000@resolution, grid_10000@area_km2))
#> Target ~10000 km²: resolution 8 (actual: 7774.0 km²)| Resolution | # Cells | Cell Area (km²) | Spacing (km) |
|---|---|---|---|
| 0 | 12 | 42,505,468.5 | 7005.8 |
| 1 | 32 | 15,939,550.7 | 4290.2 |
| 2 | 92 | 5,544,191.5 | 2530.2 |
| 3 | 272 | 1,875,241.3 | 1471.5 |
| 4 | 812 | 628,159.6 | 851.7 |
| 5 | 2.4K | 209,730.9 | 492.1 |
| 6 | 7.3K | 69,948.7 | 284.2 |
| 7 | 21.9K | 23,320.5 | 164.1 |
| 8 | 65.6K | 7,774.0 | 94.7 |
| 9 | 196.8K | 2,591.4 | 54.7 |
| 10 | 590.5K | 863.8 | 31.6 |
| 11 | 1.8M | 287.9 | 18.2 |
| 12 | 5.3M | 96.0 | 10.5 |
| 13 | 15.9M | 32.0 | 6.1 |
| 14 | 47.8M | 10.7 | 3.5 |
| 15 | 143.5M | 3.6 | 2.0 |
Different apertures offer different resolution steps:
target_area <- 1000 # km²
cat(sprintf("Target: ~%d km² cells\n\n", target_area))
#> Target: ~1000 km² cells
for (ap in c(3, 4, 7)) {
grid <- hex_grid(area_km2 = target_area, aperture = ap)
n_cells <- 10 * (ap^grid@resolution) + 2
cat(sprintf("Aperture %d: resolution %d -> %.1f km² (%s cells globally)\n",
ap, grid@resolution, grid@area_km2,
format(n_cells, big.mark = ",")))
}
#> Aperture 3: resolution 10 -> 863.8 km² (590,492 cells globally)
#> Aperture 4: resolution 8 -> 778.3 km² (655,362 cells globally)
#> Aperture 7: resolution 6 -> 433.5 km² (1,176,492 cells globally)| Aperture | Best For | Trade-offs |
|---|---|---|
| 3 | Fine resolution control, dggridR compatibility | Slowest cell growth |
| 4 | Power-of-2 scaling, GIS workflows | Moderate resolution steps |
| 7 | Rapid cell count growth, coarse analysis | Largest resolution jumps |
| 4/3 | Balance of 4’s fast start + 3’s fine control | More complex indexing |
A grid partitions a sphere, and radius_km sets which
sphere. Pass a radius in kilometres or the name of a body:
mars <- hex_grid(area_km2 = 1000, radius_km = "mars")
mars
#> HexGridInfo Specification
#> -------------------------
#> Aperture: 3
#> Resolution: 9
#> Area: 733.48 km^2
#> Diagonal: 29.10 km
#> CRS: +proj=longlat +R=3389500 +no_defs
#> Radius: 3389.50 km
#> Total Cells: 196832
# The same grid, by radius
identical(mars@area_km2, hex_grid(resolution = mars@resolution, radius_km = 3389.5)@area_km2)
#> [1] TRUECell geometry is angular: which cell a coordinate falls in, where
centres and corners sit, the parent-child hierarchy and the neighbours
are the same on every body. The radius sets the kilometre figures — cell
area, diagonal, spacing, and the resolution that a target
area_km2 picks.
earth <- hex_grid(resolution = 5)
moon <- hex_grid(resolution = 5, radius_km = "moon")
lon <- c(0, 16.37, -70.5)
lat <- c(0, 48.21, -33.4)
identical(lonlat_to_cell(lon, lat, moon), lonlat_to_cell(lon, lat, earth))
#> [1] TRUE
c(earth = earth@area_km2, moon = moon@area_km2)
#> earth moon
#> 209730.93 15597.17Areas come from the sphere of that radius, 4 * pi * r^2;
Earth keeps the WGS84 ellipsoid area. Named bodies carry the IAU mean
radii (Archinal et al. 2018) tabulated by JPL Solar System Dynamics:
mercury, venus, earth,
moon, mars, ceres,
jupiter, io, europa,
ganymede, callisto, saturn,
enceladus, titan, uranus,
neptune, pluto.
radius_km reaches every function that reports
kilometres, because the grid object carries it:
dgearthstat(), cell_area(),
hexify_compare_resolutions() and the sf exports all read
the grid’s radius.
EPSG codes name Earth reference systems, so a grid built on another radius carries a longlat CRS on the sphere of that radius:
Every sf object built from that grid carries it, so cells, centres and hexified data come back in the body’s own coordinates. A planet of your own works the same way through a radius in kilometres:
world <- hex_grid(area_km2 = 5000, radius_km = 4200)
world@crs
#> [1] "+proj=longlat +R=4200000 +no_defs"crs also takes a value of your own, as an EPSG code or a
PROJ or WKT string, which is how a projected system for the body reaches
the grid:
hex_grid(area_km2 = 5000, radius_km = 4200,
crs = "+proj=laea +lat_0=0 +lon_0=0 +R=4200000 +no_defs")@crs
#> [1] "+proj=laea +lat_0=0 +lon_0=0 +R=4200000 +no_defs"A basemap for such a body is any file sf or terra reads:
sf::st_read() takes a shapefile, GeoJSON or GeoPackage of
coastlines, terra::rast() takes a GeoTIFF. Both go to
hexify_heatmap(basemap = ).
Both backends take a radius. H3 reports a cell’s area as its solid angle times Earth’s radius squared, so another radius scales those areas by the square of the radius ratio:
h3_mars <- hex_grid(resolution = 5, type = "h3", radius_km = "mars")
#> H3 cell IDs name a position in H3's topology, which Uber's H3 reads on Earth. A grid on another body reuses that topology and its own radius for areas; the IDs are not interchangeable with Earth H3 data.
#> This message is displayed once per session.
h3_mars@area_km2 / hex_grid(resolution = 5, type = "h3")@area_km2
#> [1] 0.2830447
# H3 measures against the WGS84 authalic radius, so that is what divides out
(3389.5 / 6371.007180918475)^2
#> [1] 0.2830447One caveat rides along with H3. A cell ID names a position in H3’s
topology, which Uber’s H3 reads on Earth. A grid on another body reuses
that topology and its own radius for areas, so its IDs are that topology
on that body, not interchangeable with Earth H3 data.
h3_crosswalk() needs both grids on the same body for the
same reason.
# Hexify some data
grid <- hex_grid(area_km2 = 20000)
result <- hexify(cities, lon = "lon", lat = "lat", grid = grid)
# Convert to sf points (uses cell centers)
sf_points <- as_sf(result, geometry = "point")
class(sf_points)
#> [1] "sf" "data.frame"
# Convert to sf polygons (for choropleth maps)
sf_polys <- as_sf(result, geometry = "polygon")
class(sf_polys)
#> [1] "sf" "data.frame"
# Or generate polygons directly from cell IDs
unique_cells <- cells(result)
cell_polys <- cell_to_sf(unique_cells, grid)europe <- hexify_world[hexify_world$continent == "Europe", ]
ggplot() +
geom_sf(data = europe, fill = "ivory", color = "gray70") +
geom_sf(data = cell_polys, fill = "steelblue", alpha = 0.5, color = "darkblue") +
coord_sf(xlim = c(-10, 25), ylim = c(35, 58)) +
labs(title = "European Cities - Hexagonal Grid") +
theme_minimal(base_size = FIG_BASE_SIZE)Use sf’s st_write() to export grids for use in external
GIS software:
# Generate a grid
grid <- hex_grid(area_km2 = 10000)
europe <- grid_rect(c(-10, 35, 25, 60), grid)
# Export to various formats
st_write(europe, "europe_grid.gpkg", layer = "hexgrid") # GeoPackage
st_write(europe, "europe_grid.shp") # Shapefile
st_write(europe, "europe_grid.geojson") # GeoJSON
st_write(europe, "europe_grid.kml", layer = "hexgrid") # KML (Google Earth)data_with_na <- data.frame(
lon = c(16.37, NA, 2.35, 13.40),
lat = c(48.21, 48.86, NA, 52.52)
)
grid <- hex_grid(area_km2 = 1000)
result <- hexify(data_with_na, lon = "lon", lat = "lat", grid = grid)
#> Warning in hexify(data_with_na, lon = "lon", lat = "lat", grid = grid): 2
#> coordinate pairs contain NA values and will be skipped
# Check which rows have valid cell assignments
cat("Cell IDs:", result@cell_id, "\n")
#> Cell IDs: 126594 122466
cat("NA indicates invalid coordinates\n")
#> NA indicates invalid coordinatesCoordinates with latitude > 89° may have projection artifacts. The grid remains valid, but polygon visualization can be distorted near poles.
Polygons crossing the date line (lon = ±180°) are handled
automatically. cell_to_sf() applies
sf::st_wrap_dateline() internally, so flat map projections
render correctly without manual intervention.
| Task | Function |
|---|---|
| Create grid specification | hex_grid(area_km2 = ...) |
| Assign points to cells | hexify(df, lon, lat, grid) |
| Get grid from HexData | grid_info(result) |
| Get unique cell IDs | cells(result) |
| Count cells | n_cells(result) |
| Extract data frame | as.data.frame(result) |
| Convert to sf | as_sf(result, geometry = "polygon") |
| Generate polygons | cell_to_sf(cell_ids, grid) |
| Grid over region | grid_rect(bbox, grid) |
| Global grid | grid_global(grid) |
| Coordinate conversion | lonlat_to_cell(), cell_to_lonlat() |
| Compare resolutions | hexify_compare_resolutions() |
| Aggregate data per cell | hex_summarize(result, ...) |
| Find neighboring cells | get_neighbors(cell_id, grid, k = ...) |
| Merge/split multi-resolution cells | hex_compact(), hex_uncompact() |
| Distance between cells | hex_distance(cell_id1, cell_id2, grid) |
| Extract raster values at cell centers | hex_extract(raster, grid) |
| Zonal statistics over cell polygons | hex_zonal(raster, grid) |
| Map ISEA cells to H3 (or vice versa) | h3_crosswalk() |
| Per-cell area | cell_area(cell_ids, grid) |
vignette("quickstart") - Getting started with
hexify
vignette("visualization") - Plotting with
plot(), hexify_heatmap()
vignette("theory") - Mathematical foundations (ISEA
projection, apertures)
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.