mapgl: Interactive WebGL Maps in R
Overview
mapgl wraps Mapbox GL JS and MapLibre GL JS for interactive WebGL maps in R. Data goes on a map as a source and is drawn by a layer. Typical pipe: maplibre() or mapboxgl() -> add_*_layer(); style with interpolate() / match_expr() / step_expr(); update reactively in Shiny with maplibre_proxy().
References
Read references/API.md before writing code.
references/API.md -- Complete function reference (every add_*, set_*, turf_*, expression helper, legend, Shiny output).
references/getting-started.md -- Vignette: Mapbox vs MapLibre, styles, tokens, *_view().
references/layers-overview.md -- Vignette: every layer type with full worked examples.
references/shiny.md -- Vignette: proxies, reactive map inputs, compare widgets.
references/story-maps.md -- Vignette: scrollytelling with story_map() / on_section().
references/turf.md -- Vignette: client-side spatial ops (buffer, filter, intersect).
When NOT to Use
- Simple marker maps with <1k points --
leaflet is lighter.
- Static print maps --
tmap or ggplot2 + sf.
- Non-spatial plots --
ggplot2.
- Datasets >100k features -- tile first with
freestiler, then add_pmtiles_source().
API Keys
maplibre() + carto_style() / openfreemap_style() -- no key. Default style is carto_style("voyager").
mapboxgl() / mapbox_style() -- set MAPBOX_PUBLIC_TOKEN in .Renviron.
maptiler_style() -- set MAPTILER_API_KEY.
Quick Reference
Create a map
| Function |
Purpose |
Key parameters |
maplibre() |
MapLibre map |
style (default carto_style("voyager")), center, zoom, bearing, pitch, bounds, projection, ... (e.g. scrollZoom=FALSE, maxZoom) |
mapboxgl() |
Mapbox map (needs token) |
same args + access_token (opt), parallels (adv) |
maplibre_view() / mapboxgl_view() |
Auto-styled view |
data (sf/terra), column (opt), n (opt) |
Layers
All take map, id, source, source_layer (opt, for vector/PMTiles), popup (opt), tooltip (opt), hover_options (opt), filter (opt), before_id (opt), min_zoom/max_zoom (opt).
| Function |
Most-used style params |
add_fill_layer() |
fill_color, fill_opacity, fill_outline_color |
add_line_layer() |
line_color, line_width, line_opacity, line_dasharray (opt) |
add_circle_layer() |
circle_color, circle_radius, circle_stroke_color, circle_stroke_width, cluster_options (opt) |
add_heatmap_layer() |
heatmap_weight, heatmap_intensity, heatmap_color, heatmap_radius |
add_symbol_layer() |
icon_image, icon_size, text_field, text_size, text_color |
add_fill_extrusion_layer() |
fill_extrusion_color, fill_extrusion_height, fill_extrusion_base. Use projection="mercator" on the map -- globe has artifacts. |
add_raster_layer() |
raster_opacity, raster_color (opt) |
Sources
| Function |
Use |
add_source(id, data) |
sf object or GeoJSON URL |
| `add_vector_source(id, url |
tiles, promote_id=NULL)` |
add_pmtiles_source(id, url, source_type="vector", maxzoom=22, promote_id=NULL) |
PMTiles archive |
| `add_raster_source(id, url |
tiles, tileSize=256, maxzoom=22)` |
add_raster_dem_source(id, url, tileSize=512) |
DEM for set_terrain() |
| `add_image_source(id, url |
data, coordinates)` |
promote_id is required on vector / PMTiles sources that need hover or feature_click.
Styling expressions (data-driven styling)
| Function |
Use |
interpolate(column, values, stops, type="linear", na_color=NULL) |
Continuous color/size |
match_expr(column, values, stops, default="#cccccc") |
Categorical |
step_expr(column, base, values, stops) |
Threshold classes |
step_equal_interval() / step_quantile() / step_jenks() |
Auto classes + legend helpers (get_legend_labels, get_legend_colors, get_breaks) |
interpolate_palette() |
Auto palette + breaks |
get_column("name") |
Data-column reference inside any expression |
Camera, controls, legends
- Camera:
fit_bounds(map, bbox, animate=FALSE), fly_to(map, center, zoom), ease_to(), jump_to(), set_view().
- Controls:
add_navigation_control(), add_fullscreen_control(), add_scale_control(), add_geolocate_control(), add_layers_control(layers=NULL, collapsible=TRUE), add_draw_control(rectangle=FALSE, show_measurements=FALSE), add_geocoder_control().
- Legends:
add_legend(legend_title, values, colors, type=c("continuous","categorical"), patch_shape="square", position="top-left", add=FALSE, layer_id=NULL, interactive=FALSE). Also add_categorical_legend(), add_continuous_legend(), clear_legend().
Shiny
- Output / render:
maplibreOutput("map", height="600px") + renderMaplibre({...}). Same for mapboxgl.
- Proxy (mutate without re-render):
maplibre_proxy("map") / mapboxgl_proxy("map") inside observeEvent().
- Proxy-compatible:
set_filter(), set_paint_property(), set_layout_property(), set_style(), clear_layer(), add_*_layer(), fly_to(), fit_bounds(), add_markers().
- Auto inputs:
input$<mapId>_click, ..._feature_click, ..._zoom, ..._center, ..._bbox, ..._drawn_features.
- Compare widget:
compare(m1, m2, mode="swipe"|"sync"); Shiny: maplibreCompareOutput() + maplibre_compare_proxy(map_side="before"|"after").
Story maps & turf.js
- Story:
story_map() / story_maplibre() UI + story_section(title, content, position="left") + server on_section(map_id, section_id, expr).
- Client-side spatial:
turf_buffer(), turf_filter(predicate="intersects"|"within"|"contains"|"crosses"|"disjoint"), turf_intersect(), turf_union(), turf_difference(), turf_convex_hull(), turf_concave_hull(), turf_voronoi(), turf_centroid().
Quick Start
library(mapgl); library(sf)
nc <- st_read(system.file("shape/nc.shp", package = "sf"))
maplibre(style = carto_style("positron"), bounds = nc) |>
add_fill_layer(
id = "counties",
source = nc,
fill_color = interpolate(
column = "BIR74",
values = c(500, 20000),
stops = c("#eff3ff", "#08519c"),
na_color = "lightgrey"
),
fill_opacity = 0.7,
popup = "NAME",
hover_options = list(fill_color = "yellow")
) |>
add_legend("Births (1974)",
values = c(500, 20000),
colors = c("#eff3ff", "#08519c"))
Shiny pattern (proxy, not re-render)
library(shiny); library(mapgl)
ui <- fluidPage(sliderInput("min", "Min BIR74", 0, 22000, 500),
maplibreOutput("map", height = "600px"))
server <- function(input, output, session) {
output$map <- renderMaplibre({
maplibre(carto_style("positron"), bounds = nc) |>
add_fill_layer(id = "ct", source = nc, fill_color = "steelblue")
})
observeEvent(input$min, {
maplibre_proxy("map") |>
set_filter("ct", list(">=", get_column("BIR74"), input$min))
})
}
PMTiles pattern (large data)
maplibre() |>
add_pmtiles_source(id = "src", url = "https://example.com/data.pmtiles") |>
add_fill_layer(id = "pm", source = "src",
source_layer = "features", # must match freestiler layer_name
fill_color = "steelblue")
Common Mistakes
| Mistake |
Fix |
Using %>% |
Use native |>. |
| Hard-coding colors for numeric data |
interpolate() + matching add_legend(). |
source_layer omitted for vector/PMTiles |
Required. Must match the tileset layer name. |
| Re-rendering map on every input |
Use maplibre_proxy() + set_* in observeEvent(). |
Missing promote_id for hover/feature_click |
Set promote_id on add_vector_source() / add_pmtiles_source(). |
| Fill-extrusion + globe projection |
Set projection = "mercator". |
mapboxgl() with no token |
Use maplibre() for token-free maps. |
circular_patches = TRUE |
Deprecated; use patch_shape = "circle". |
Resources
Package site | GitHub | Sibling skills r-freestiler (tiles) and r-mapping (chooser).
1---2name: r-mapgl3description: Use when code loads or uses mapgl (library(mapgl), mapgl::), calls maplibre()/mapboxgl()/add_*_layer()/maplibre_proxy(), builds interactive WebGL maps in R, displays PMTiles on a map, or creates story maps / scrollytelling in R Shiny4---56# mapgl: Interactive WebGL Maps in R78## Overview910**mapgl wraps Mapbox GL JS and MapLibre GL JS for interactive WebGL maps in R.** Data goes on a map as a **source** and is drawn by a **layer**. Typical pipe: `maplibre()` or `mapboxgl()` -> `add_*_layer()`; style with `interpolate()` / `match_expr()` / `step_expr()`; update reactively in Shiny with `maplibre_proxy()`.1112## References1314Read `references/API.md` before writing code.1516- `references/API.md` -- Complete function reference (every `add_*`, `set_*`, `turf_*`, expression helper, legend, Shiny output).17- `references/getting-started.md` -- Vignette: Mapbox vs MapLibre, styles, tokens, `*_view()`.18- `references/layers-overview.md` -- Vignette: every layer type with full worked examples.19- `references/shiny.md` -- Vignette: proxies, reactive map inputs, compare widgets.20- `references/story-maps.md` -- Vignette: scrollytelling with `story_map()` / `on_section()`.21- `references/turf.md` -- Vignette: client-side spatial ops (buffer, filter, intersect).2223## When NOT to Use2425- Simple marker maps with <1k points -- `leaflet` is lighter.26- Static print maps -- `tmap` or `ggplot2 + sf`.27- Non-spatial plots -- `ggplot2`.28- Datasets >100k features -- tile first with `freestiler`, then `add_pmtiles_source()`.2930## API Keys3132- `maplibre()` + `carto_style()` / `openfreemap_style()` -- **no key**. Default style is `carto_style("voyager")`.33- `mapboxgl()` / `mapbox_style()` -- set `MAPBOX_PUBLIC_TOKEN` in `.Renviron`.34- `maptiler_style()` -- set `MAPTILER_API_KEY`.3536## Quick Reference3738### Create a map3940| Function | Purpose | Key parameters |41|---|---|---|42| `maplibre()` | MapLibre map | `style` (default `carto_style("voyager")`), `center`, `zoom`, `bearing`, `pitch`, `bounds`, `projection`, `...` (e.g. `scrollZoom=FALSE`, `maxZoom`) |43| `mapboxgl()` | Mapbox map (needs token) | same args + `access_token` (opt), `parallels` (adv) |44| `maplibre_view()` / `mapboxgl_view()` | Auto-styled view | `data` (sf/terra), `column` (opt), `n` (opt) |4546### Layers4748All take `map, id, source, source_layer` (opt, for vector/PMTiles), `popup` (opt), `tooltip` (opt), `hover_options` (opt), `filter` (opt), `before_id` (opt), `min_zoom`/`max_zoom` (opt).4950| Function | Most-used style params |51|---|---|52| `add_fill_layer()` | `fill_color`, `fill_opacity`, `fill_outline_color` |53| `add_line_layer()` | `line_color`, `line_width`, `line_opacity`, `line_dasharray` (opt) |54| `add_circle_layer()` | `circle_color`, `circle_radius`, `circle_stroke_color`, `circle_stroke_width`, `cluster_options` (opt) |55| `add_heatmap_layer()` | `heatmap_weight`, `heatmap_intensity`, `heatmap_color`, `heatmap_radius` |56| `add_symbol_layer()` | `icon_image`, `icon_size`, `text_field`, `text_size`, `text_color` |57| `add_fill_extrusion_layer()` | `fill_extrusion_color`, `fill_extrusion_height`, `fill_extrusion_base`. **Use `projection="mercator"` on the map** -- globe has artifacts. |58| `add_raster_layer()` | `raster_opacity`, `raster_color` (opt) |5960### Sources6162| Function | Use |63|---|---|64| `add_source(id, data)` | sf object or GeoJSON URL |65| `add_vector_source(id, url|tiles, promote_id=NULL)` | Remote vector tiles |66| `add_pmtiles_source(id, url, source_type="vector", maxzoom=22, promote_id=NULL)` | PMTiles archive |67| `add_raster_source(id, url|tiles, tileSize=256, maxzoom=22)` | Remote raster tiles |68| `add_raster_dem_source(id, url, tileSize=512)` | DEM for `set_terrain()` |69| `add_image_source(id, url|data, coordinates)` | Single image or terra raster |7071`promote_id` is **required** on vector / PMTiles sources that need hover or `feature_click`.7273### Styling expressions (data-driven styling)7475| Function | Use |76|---|---|77| `interpolate(column, values, stops, type="linear", na_color=NULL)` | Continuous color/size |78| `match_expr(column, values, stops, default="#cccccc")` | Categorical |79| `step_expr(column, base, values, stops)` | Threshold classes |80| `step_equal_interval()` / `step_quantile()` / `step_jenks()` | Auto classes + legend helpers (`get_legend_labels`, `get_legend_colors`, `get_breaks`) |81| `interpolate_palette()` | Auto palette + breaks |82| `get_column("name")` | Data-column reference inside any expression |8384### Camera, controls, legends8586- Camera: `fit_bounds(map, bbox, animate=FALSE)`, `fly_to(map, center, zoom)`, `ease_to()`, `jump_to()`, `set_view()`.87- Controls: `add_navigation_control()`, `add_fullscreen_control()`, `add_scale_control()`, `add_geolocate_control()`, `add_layers_control(layers=NULL, collapsible=TRUE)`, `add_draw_control(rectangle=FALSE, show_measurements=FALSE)`, `add_geocoder_control()`.88- Legends: `add_legend(legend_title, values, colors, type=c("continuous","categorical"), patch_shape="square", position="top-left", add=FALSE, layer_id=NULL, interactive=FALSE)`. Also `add_categorical_legend()`, `add_continuous_legend()`, `clear_legend()`.8990### Shiny9192- Output / render: `maplibreOutput("map", height="600px")` + `renderMaplibre({...})`. Same for `mapboxgl`.93- Proxy (mutate without re-render): `maplibre_proxy("map")` / `mapboxgl_proxy("map")` inside `observeEvent()`.94- Proxy-compatible: `set_filter()`, `set_paint_property()`, `set_layout_property()`, `set_style()`, `clear_layer()`, `add_*_layer()`, `fly_to()`, `fit_bounds()`, `add_markers()`.95- Auto inputs: `input$<mapId>_click`, `..._feature_click`, `..._zoom`, `..._center`, `..._bbox`, `..._drawn_features`.96- Compare widget: `compare(m1, m2, mode="swipe"|"sync")`; Shiny: `maplibreCompareOutput()` + `maplibre_compare_proxy(map_side="before"|"after")`.9798### Story maps & turf.js99100- Story: `story_map()` / `story_maplibre()` UI + `story_section(title, content, position="left")` + server `on_section(map_id, section_id, expr)`.101- Client-side spatial: `turf_buffer()`, `turf_filter(predicate="intersects"|"within"|"contains"|"crosses"|"disjoint")`, `turf_intersect()`, `turf_union()`, `turf_difference()`, `turf_convex_hull()`, `turf_concave_hull()`, `turf_voronoi()`, `turf_centroid()`.102103## Quick Start104105```r106library(mapgl); library(sf)107nc <- st_read(system.file("shape/nc.shp", package = "sf"))108109maplibre(style = carto_style("positron"), bounds = nc) |>110 add_fill_layer(111 id = "counties",112 source = nc,113 fill_color = interpolate(114 column = "BIR74",115 values = c(500, 20000),116 stops = c("#eff3ff", "#08519c"),117 na_color = "lightgrey"118 ),119 fill_opacity = 0.7,120 popup = "NAME",121 hover_options = list(fill_color = "yellow")122 ) |>123 add_legend("Births (1974)",124 values = c(500, 20000),125 colors = c("#eff3ff", "#08519c"))126```127128## Shiny pattern (proxy, not re-render)129130```r131library(shiny); library(mapgl)132ui <- fluidPage(sliderInput("min", "Min BIR74", 0, 22000, 500),133 maplibreOutput("map", height = "600px"))134server <- function(input, output, session) {135 output$map <- renderMaplibre({136 maplibre(carto_style("positron"), bounds = nc) |>137 add_fill_layer(id = "ct", source = nc, fill_color = "steelblue")138 })139 observeEvent(input$min, {140 maplibre_proxy("map") |>141 set_filter("ct", list(">=", get_column("BIR74"), input$min))142 })143}144```145146## PMTiles pattern (large data)147148```r149maplibre() |>150 add_pmtiles_source(id = "src", url = "https://example.com/data.pmtiles") |>151 add_fill_layer(id = "pm", source = "src",152 source_layer = "features", # must match freestiler layer_name153 fill_color = "steelblue")154```155156## Common Mistakes157158| Mistake | Fix |159|---|---|160| Using `%>%` | Use native `\|>`. |161| Hard-coding colors for numeric data | `interpolate()` + matching `add_legend()`. |162| `source_layer` omitted for vector/PMTiles | Required. Must match the tileset layer name. |163| Re-rendering map on every input | Use `maplibre_proxy()` + `set_*` in `observeEvent()`. |164| Missing `promote_id` for hover/feature_click | Set `promote_id` on `add_vector_source()` / `add_pmtiles_source()`. |165| Fill-extrusion + globe projection | Set `projection = "mercator"`. |166| `mapboxgl()` with no token | Use `maplibre()` for token-free maps. |167| `circular_patches = TRUE` | Deprecated; use `patch_shape = "circle"`. |168169## Resources170171[Package site](https://walker-data.com/mapgl/) | [GitHub](https://github.com/walkerke/mapgl) | Sibling skills `r-freestiler` (tiles) and `r-mapping` (chooser).