hexify v0.5.0 adds H3 as a
first-class grid type alongside ISEA. Every core function —
hexify(), grid_rect(),
grid_clip(), get_parent(),
get_children() — works with both grid systems through the
same interface. This vignette covers what H3 is, how to use it in
hexify, and when to prefer it over ISEA.
H3 is a hierarchical hexagonal grid system developed by Uber. It
partitions Earth’s surface into hexagonal cells at 16 resolutions
(0–15), each roughly 7\(\times\) finer
than the last. Cell IDs are 64-bit integers encoded as hexadecimal
strings (e.g., "8528342bfffffff").
H3 has become an industry standard adopted by the FCC, Foursquare, and numerous geospatial platforms.
Key difference from ISEA: H3 cells are not
equal-area. Cell area varies by ~1.6\(\times\) between the largest and smallest
hexagons at any given resolution, depending on latitude. For rigorous
equal-area analysis, use ISEA. For interoperability with H3 ecosystems,
use type = "h3".
Create an H3 grid by passing type = "h3" to
hex_grid():
library(sf)
#> Linking to GEOS 3.13.1, GDAL 3.11.4, PROJ 9.7.0; sf_use_s2() is TRUE
library(ggplot2)
# Create an H3 grid specification
grid_h3 <- hex_grid(resolution = 5, type = "h3")
#> H3 cells are not exactly equal-area; area varies ~3-5% by latitude.
#> This message is displayed once per session.
grid_h3
#> HexGridInfo Specification [H3]
#> -------------------------------
#> Grid Type: H3 (Uber)
#> Resolution: 5
#> Avg Area: 252.9040 km^2 (varies by location)
#> Avg Diagonal:17.09 km
#> CRS: EPSG:4326
#> Total Cells: 2016842
#> Note: H3 cells are NOT exactly equal-areaThen use hexify() with the grid object, just like
ISEA:
# Sample cities
cities <- data.frame(
name = c("Vienna", "Paris", "Madrid", "Berlin", "Rome",
"London", "Prague", "Warsaw", "Budapest", "Amsterdam"),
lon = c(16.37, 2.35, -3.70, 13.40, 12.50,
-0.12, 14.42, 21.01, 19.04, 4.90),
lat = c(48.21, 48.86, 40.42, 52.52, 41.90,
51.51, 50.08, 52.23, 47.50, 52.37)
)
result <- hexify(cities, lon = "lon", lat = "lat", grid = grid_h3)
result
#> HexData Object
#> --------------
#> Rows: 10
#> Columns: 3
#> Cells: 10 unique
#> Type: data.frame
#>
#> Grid:
#> H3 Resolution 5 (~252.9040 km^2 avg)
#>
#> Columns: name, lon, lat
#>
#> Data preview (with cell assignments):
#> name lon lat cell_id
#> Vienna 16.37 48.21 851e15b7fffffff
#> Paris 2.35 48.86 851fb467fffffff
#> Madrid -3.70 40.42 85390ca3fffffff
#> ... with 7 more rowsH3 cell IDs are character strings, unlike ISEA’s numeric IDs:
# Cell IDs are hexadecimal strings
result@cell_id
#> [1] "851e15b7fffffff" "851fb467fffffff" "85390ca3fffffff" "851f1d4bfffffff"
#> [5] "851e8053fffffff" "85194ad3fffffff" "851e3543fffffff" "851f53cbfffffff"
#> [9] "851e037bfffffff" "85196953fffffff"
# All standard accessors work
cells(result)
#> [1] "851e15b7fffffff" "851fb467fffffff" "85390ca3fffffff" "851f1d4bfffffff"
#> [5] "851e8053fffffff" "85194ad3fffffff" "851e3543fffffff" "851f53cbfffffff"
#> [9] "851e037bfffffff" "85196953fffffff"
n_cells(result)
#> [1] 10If you think in terms of cell area rather than resolution numbers,
pass area_km2 instead of resolution. hexify
picks the closest H3 resolution:
grid_area <- hex_grid(area_km2 = 500, type = "h3")
#> Warning in hex_grid(area_km2 = 500, type = "h3"): H3 cells are not exactly
#> equal-area. Closest resolution 5 has average area ~252.904 km^2 (requested
#> 500.000 km^2)
grid_area
#> HexGridInfo Specification [H3]
#> -------------------------------
#> Grid Type: H3 (Uber)
#> Resolution: 5
#> Avg Area: 252.9040 km^2 (varies by location)
#> Avg Diagonal:17.09 km
#> CRS: EPSG:4326
#> Total Cells: 2016842
#> Note: H3 cells are NOT exactly equal-areaAll grid generation functions work with H3 grids.
# Generate H3 hexagons over Western Europe
grid_h3 <- hex_grid(resolution = 3, type = "h3")
europe_h3 <- grid_rect(c(-10, 35, 25, 60), grid_h3)
# Basemap
europe <- hexify_world[hexify_world$continent == "Europe", ]
ggplot() +
geom_sf(data = europe, fill = "gray95", color = "gray60") +
geom_sf(data = europe_h3, fill = NA, color = "#E6550D", linewidth = 0.4) +
coord_sf(xlim = c(-10, 25), ylim = c(35, 60)) +
labs(title = sprintf("H3 Resolution %d Grid (~%.0f km² avg cells)",
grid_h3@resolution, grid_h3@area_km2)) +
theme_minimal()# Clip H3 grid to France
france <- hexify_world[hexify_world$name == "France", ]
grid_h3 <- hex_grid(resolution = 4, type = "h3")
france_h3 <- grid_clip(france, grid_h3)
#> Spherical geometry (s2) switched off
#> although coordinates are longitude/latitude, st_intersection assumes that they
#> are planar
#> Spherical geometry (s2) switched on
ggplot() +
geom_sf(data = france, fill = "gray95", color = "gray40", linewidth = 0.5) +
geom_sf(data = france_h3, fill = alpha("#E6550D", 0.3),
color = "#E6550D", linewidth = 0.3) +
coord_sf(xlim = c(-5, 10), ylim = c(41, 52)) +
labs(title = sprintf("H3 Grid Clipped to France (res %d)", grid_h3@resolution)) +
theme_minimal()The standard hexify workflow applies to H3 grids. Here’s a complete example using simulated species observations:
set.seed(42)
# Simulate observations across Europe
obs <- data.frame(
lon = c(rnorm(200, 10, 12), rnorm(100, 25, 8)),
lat = c(rnorm(200, 48, 6), rnorm(100, 55, 4)),
species = sample(c("Sp. A", "Sp. B", "Sp. C"), 300, replace = TRUE)
)
obs$lon <- pmax(-10, pmin(40, obs$lon))
obs$lat <- pmax(35, pmin(65, obs$lat))
# Hexify with H3
grid_h3 <- hex_grid(resolution = 3, type = "h3")
obs_hex <- hexify(obs, lon = "lon", lat = "lat", grid = grid_h3)
# Aggregate: species richness per cell
obs_df <- as.data.frame(obs_hex)
obs_df$cell_id <- obs_hex@cell_id
richness <- aggregate(species ~ cell_id, data = obs_df,
FUN = function(x) length(unique(x)))
names(richness)[2] <- "n_species"
# Map it
polys <- cell_to_sf(richness$cell_id, grid_h3)
polys <- merge(polys, richness, by = "cell_id")
europe <- hexify_world[hexify_world$continent == "Europe", ]
ggplot() +
geom_sf(data = europe, fill = "gray95", color = "gray70", linewidth = 0.2) +
geom_sf(data = polys, aes(fill = n_species), color = "white", linewidth = 0.3) +
scale_fill_viridis_c(option = "plasma", name = "Species\nRichness") +
coord_sf(xlim = c(-10, 40), ylim = c(35, 65)) +
labs(title = "Species Richness on H3 Grid",
subtitle = sprintf("H3 resolution %d (~%.0f km² avg cells)",
grid_h3@resolution, grid_h3@area_km2)) +
theme_minimal() +
theme(axis.text = element_blank(), axis.ticks = element_blank())hexify v0.6.0 added h3_crosswalk() for bidirectional
mapping between ISEA and H3 cell IDs. This is useful when you work in
ISEA for analysis but need to share results with H3 ecosystems (or vice
versa).
# Start with an ISEA grid and some cells
grid_isea <- hex_grid(resolution = 9, aperture = 3)
isea_ids <- lonlat_to_cell(
lon = c(16.37, 2.35, 13.40, -3.70, 12.50),
lat = c(48.21, 48.86, 52.52, 40.42, 41.90),
grid = grid_isea
)
# Map ISEA cells to their closest H3 equivalents
xw <- h3_crosswalk(isea_ids, grid_isea)
xw[, c("isea_cell_id", "h3_cell_id", "isea_area_km2", "h3_area_km2")]
#> isea_cell_id h3_cell_id isea_area_km2 h3_area_km2
#> 1 42280 841e15bffffffff 2591.375 1753.173
#> 2 39597 841fb43ffffffff 2591.375 1569.204
#> 3 40823 841f1d5ffffffff 2591.375 1570.136
#> 4 39585 84390cbffffffff 2591.375 1819.493
#> 5 42516 841e80dffffffff 2591.375 1885.191The area_ratio column shows how ISEA and H3 cell sizes
compare — values close to 1 mean the resolutions are well-matched.
| ISEA | H3 | |
|---|---|---|
| Cell area | Exactly equal | ~1.6\(\times\) variation |
| Cell IDs | Numeric (integer) | Character (hex string) |
| Apertures | 3, 4, 7, 4/3 | Fixed (7) |
| Resolutions | 0–30 | 0–15 |
| Hierarchy | Approximate (aperture-dependent) | Exact (7 children per parent) |
| Dependencies | None (built-in C++) | None (vendored H3 C library) |
| Industry adoption | Scientific / government | Tech industry / commercial |
Use ISEA when:
Equal-area cells are required (biodiversity surveys, density estimation, statistical sampling)
You need fine control over aperture and resolution
No external dependencies are acceptable
Use H3 when:
Interoperability with H3 ecosystems (Uber, Foursquare, DuckDB, BigQuery)
Clean hierarchical operations (parent/child traversal) are a priority
Slight area variation across latitudes is acceptable for your analysis
h3_res <- hexify_compare_resolutions(type = "h3", res_range = 0:15)
h3_res$n_cells_fmt <- ifelse(
h3_res$n_cells > 1e9,
sprintf("%.1fB", h3_res$n_cells / 1e9),
ifelse(h3_res$n_cells > 1e6,
sprintf("%.1fM", h3_res$n_cells / 1e6),
ifelse(h3_res$n_cells > 1e3,
sprintf("%.1fK", h3_res$n_cells / 1e3),
as.character(h3_res$n_cells)))
)
knitr::kable(
h3_res[, c("resolution", "n_cells_fmt", "cell_area_km2", "cell_spacing_km")],
col.names = c("Resolution", "# Cells", "Avg Area (km²)", "Spacing (km)"),
digits = 1
)| Resolution | # Cells | Avg Area (km²) | Spacing (km) |
|---|---|---|---|
| 0 | 122 | 4357449.4 | 2243.1 |
| 1 | 842 | 609788.4 | 839.1 |
| 2 | 5.9K | 86801.8 | 316.6 |
| 3 | 41.2K | 12393.4 | 119.6 |
| 4 | 288.1K | 1770.3 | 45.2 |
| 5 | 2.0M | 252.9 | 17.1 |
| 6 | 14.1M | 36.1 | 6.5 |
| 7 | 98.8M | 5.2 | 2.4 |
| 8 | 691.8M | 0.7 | 0.9 |
| 9 | 4.8B | 0.1 | 0.3 |
| 10 | 33.9B | 0.0 | 0.1 |
| 11 | 237.3B | 0.0 | 0.0 |
| 12 | 1661.0B | 0.0 | 0.0 |
| 13 | 11626.7B | 0.0 | 0.0 |
| 14 | 81386.8B | 0.0 | 0.0 |
| 15 | 569707.4B | 0.0 | 0.0 |
Areas are averages — actual cell area varies by latitude.
vignette("quickstart") - Getting started with hexify
(ISEA-focused)
vignette("workflows") - Grid generation,
multi-resolution analysis, GIS export
vignette("visualization") - Plotting with
plot(), hexify_heatmap()
vignette("theory") - Mathematical foundations of the
ISEA projection