freestiler creates PMTiles vector tilesets from R. Give it an sf object, a file on disk, or a DuckDB SQL query, and it writes a single .pmtiles file you can serve from anywhere. The tiling engine is written in Rust and runs in-process, so there’s nothing else to install.
About this fork
This is an R-only fork of walkerke/freestiler. It tracks upstream’s R package and API, with one deliberate difference: native builds always statically compile both Rust backends into the package:
-
Rust DuckDB (
duckdb-rs, bundled) — powersfreestile_query()and theengine = "duckdb"file reader, enabling out-of-R-memory, file-to-file and SQL-to-tiles workflows. -
GeoParquet (native Rust reader) — powers direct
.parquettiling viafreestile_file().
Upstream ships these off by default on Windows; here they are always on, including on Windows (built with the Rtools45 GNU/UCRT toolchain). Because DuckDB is statically linked, the result is a single self-contained DLL with no external runtime dependencies — once installed, library(freestiler) gives you the full feature set with no PATH or runtime setup. Python packaging is intentionally not part of this fork.
Installation
This fork is Windows-focused and always compiles the Rust GeoParquet and DuckDB backends in. Install it from GitHub with pak:
# install.packages("pak")
pak::pak("jimbrig/freestiler")From a local clone of this repo:
pak::local_install()Once an r-universe binary has built, you can install it without a Rust toolchain:
install.packages(
"freestiler",
repos = c("https://jimbrig.r-universe.dev", "https://cloud.r-project.org")
)The upstream package (Rust DuckDB off by default on Windows) is on CRAN and walkerke.r-universe.dev.
Verifying the Rust backends
library(freestiler)
freestiler:::.has_rust_duckdb()
freestiler:::.has_rust_geoparquet()Building from source (maintainers)
install.R and build.R are convenience scripts for source builds. They locate Rtools via pkgbuild, isolate the build from any personal ~/.R/Makevars, and verify both Rust backends are compiled in:
-
source("install.R")— install into your normal library -
source("build.R")— install into an isolatedbuild/library/for testing
tools/feature-smoke.R runs the same feature checks used in CI.
Quick start
The main function is freestile(). Let’s tile the North Carolina counties dataset that ships with sf:
library(sf)
library(freestiler)
nc <- st_read(system.file("shape/nc.shp", package = "sf"))
freestile(nc, "nc_counties.pmtiles", layer_name = "counties")That’s useful for checking your installation, but the same API handles much bigger data. Here we tile all 242,000 US block groups from tigris:
Viewing tiles
The quickest way to view a tileset is view_tiles(), which starts a local server and opens an interactive map:
view_tiles("us_bgs.pmtiles")For more control, use serve_tiles() to start a local server and build your map with mapgl:
library(mapgl)
serve_tiles("us_bgs.pmtiles")
maplibre(hash = TRUE) |>
add_pmtiles_source(
id = "bgs-src",
url = "http://localhost:8080/us_bgs.pmtiles",
promote_id = "GEOID"
) |>
add_fill_layer(
id = "bgs-fill",
source = "bgs-src",
source_layer = "bgs",
fill_color = "navy",
fill_opacity = 0.5,
hover_options = list(
fill_color = "#ffffcc",
fill_opacity = 0.9
)
)The built-in server handles CORS and range requests automatically. For tilesets larger than ~1 GB, use an external server like npx http-server /path --cors -c-1 for better performance. See the Mapping with mapgl article for a full walkthrough.
DuckDB queries
If your data lives in DuckDB, freestile_query() lets you filter, join, and transform with SQL before tiling:
freestile_query(
query = "SELECT * FROM read_parquet('blocks.parquet') WHERE state = 'NC'",
output = "nc_blocks.pmtiles",
layer_name = "blocks"
)For very large point datasets, the streaming pipeline avoids loading the full result into memory. On a recent run, freestile_query() streamed 146 million US job points from DuckDB into a 2.3 GB PMTiles archive in about 12 minutes:
freestile_query(
query = "SELECT naics, state, ST_Point(lon, lat) AS geometry FROM jobs_dots",
output = "us_jobs_dots.pmtiles",
db_path = db_path,
layer_name = "jobs",
tile_format = "mvt",
min_zoom = 4,
max_zoom = 14,
base_zoom = 14,
drop_rate = 2.5,
source_crs = "EPSG:4326",
streaming = "always",
overwrite = TRUE
)Direct file input
You can tile spatial files without loading them into R first:
# GeoParquet
freestile_file("census_blocks.parquet", "blocks.pmtiles")
# GeoPackage, Shapefile, or other formats via DuckDB
freestile_file("counties.gpkg", "counties.pmtiles", engine = "duckdb")For GeoParquet, the direct file path is powered by the Rust geoparquet backend, which this fork always compiles in. freestile_file() reads WKB-based GeoParquet directly without materializing the data in the R session first.
Dynamic hexagonal binning
For dense point datasets, freestile_h3() aggregates points into H3 hexagons at zoom-appropriate resolutions: low zooms show coarse hexes summarizing whole regions, intermediate zooms show progressively finer hexes, and base_zoom and above reveal the underlying points. Aggregation rules are user-defined SQL (COUNT(*), SUM(pop), AVG(value), …).
freestile_h3(
pts,
"wind.pmtiles",
agg = c(n = "COUNT(*)", total_mw = "SUM(capacity_mw)"),
min_zoom = 2, max_zoom = 12, base_zoom = 10
)
view_h3_tiles("wind.pmtiles", agg_column = "n")Pass fade = TRUE to cross-fade between resolutions instead of swapping cleanly. Requires DuckDB and its H3 community extension; see the Hexagonal binning with H3 article.
Multi-layer tilesets
pts <- st_centroid(nc)
freestile(
list(
counties = freestile_layer(nc, min_zoom = 0, max_zoom = 10),
centroids = freestile_layer(pts, min_zoom = 6, max_zoom = 14)
),
"nc_layers.pmtiles"
)Tile formats
freestiler defaults to Mapbox Vector Tiles (MVT), the widely-supported protobuf format that works with both MapLibre GL JS and Mapbox GL JS. The experimental MapLibre Tiles (MLT) format is also available via tile_format = "mlt" and can produce smaller files for polygon and line data.
Learn more
- Getting Started - full tutorial
- Mapping with mapgl - viewing and styling tiles with mapgl
- MapLibre Tiles (MLT) - MLT vs MVT and when to use each
