Building & Deploying a Geospatial R Shiny App (for Free)¶
Objective: Build an interactive world map where clicking countries toggles them red (visited), then deploy it online for free.
R Shiny transforms R into a web application framework. We'll create "Where Have You Been in the World?"βa clickable world map that tracks visited countries, then deploy it to the cloud without spending a dime.
1) Environment Setup¶
Install R and RStudio, then load the required packages:
# Install core packages (run once)
install.packages(c("shiny", "leaflet", "sf", "rnaturalearth",
"rnaturalearthdata", "dplyr", "rsconnect"))
Why: These packages provide the foundationβShiny for the web framework, Leaflet for interactive maps, sf for spatial data handling, and rnaturalearth for world country data.
2) Getting World Data¶
Load country polygons using rnaturalearth:
library(sf)
library(rnaturalearth)
# Get world countries as sf object
world <- ne_countries(returnclass = "sf")
# Check what we have
head(world[, c("name", "iso_a3", "geometry")])
Why: rnaturalearth provides clean, standardized country boundaries. The iso_a3 field gives us unique country identifiers for tracking clicks.
3) Building the Shiny App¶
Create a complete Shiny application with interactive map:
library(shiny)
library(leaflet)
library(sf)
library(rnaturalearth)
library(dplyr)
# Load world data once
world <- ne_countries(returnclass = "sf")
# UI Definition
ui <- fluidPage(
titlePanel("Where Have You Been in the World?"),
p("Click on countries to mark them as visited (red). Click again to unmark."),
leafletOutput("map", height = "600px"),
br(),
textOutput("visited_count")
)
# Server Logic
server <- function(input, output, session) {
# Reactive value to store visited countries
visited <- reactiveVal(character(0))
# Render the initial map
output$map <- renderLeaflet({
leaflet(world) %>%
addTiles() %>%
addPolygons(
layerId = ~iso_a3,
fillColor = "white",
color = "black",
weight = 1,
opacity = 0.8,
fillOpacity = 0.6,
highlightOptions = highlightOptions(
color = "blue",
weight = 3,
bringToFront = TRUE
),
label = ~name,
labelOptions = labelOptions(
style = list("font-weight" = "normal", padding = "3px 8px"),
textsize = "12px",
direction = "auto"
)
)
})
# Handle country clicks
observeEvent(input$map_shape_click, {
clicked_iso <- input$map_shape_click$id
current_visited <- visited()
# Toggle country in visited list
if (clicked_iso %in% current_visited) {
# Remove from visited
new_visited <- setdiff(current_visited, clicked_iso)
} else {
# Add to visited
new_visited <- c(current_visited, clicked_iso)
}
visited(new_visited)
# Update map colors
leafletProxy("map") %>%
clearShapes() %>%
addPolygons(
data = world,
layerId = ~iso_a3,
fillColor = ~ifelse(iso_a3 %in% visited(), "red", "white"),
color = "black",
weight = 1,
opacity = 0.8,
fillOpacity = 0.6,
highlightOptions = highlightOptions(
color = "blue",
weight = 3,
bringToFront = TRUE
),
label = ~name,
labelOptions = labelOptions(
style = list("font-weight" = "normal", padding = "3px 8px"),
textsize = "12px",
direction = "auto"
)
)
})
# Show visited count
output$visited_count <- renderText({
paste("Countries visited:", length(visited()))
})
}
# Run the app
shinyApp(ui, server)
Why: This creates a complete interactive experienceβusers click countries to toggle their visited status, with visual feedback and a counter. The reactiveVal persists state during the session.
4) Local Testing¶
Save the code above as app.R in a new directory, then run:
Expected behavior: - World map loads with white countries - Clicking a country turns it red - Clicking again toggles back to white - Counter shows number of visited countries - Hover shows country names
5) Free Deployment Options¶
Option A: ShinyApps.io (Recommended)¶
ShinyApps.io offers free hosting with generous limits:
# Install deployment package
install.packages("rsconnect")
# Authenticate (get credentials from shinyapps.io)
rsconnect::setAccountInfo(
name = 'your-username',
token = 'your-token',
secret = 'your-secret'
)
# Deploy your app
rsconnect::deployApp("path/to/your/app")
Why: ShinyApps.io is purpose-built for Shiny apps, handles scaling automatically, and provides a clean URL like https://yourname.shinyapps.io/your-app-name/.
Option B: Docker + Render (Advanced)¶
For more control, containerize your app:
# Dockerfile
FROM rocker/shiny:4.3
# Install system dependencies
RUN apt-get update && apt-get install -y \
libgdal-dev \
libproj-dev \
libgeos-dev \
&& rm -rf /var/lib/apt/lists/*
# Install R packages
RUN R -e "install.packages(c('shiny', 'leaflet', 'sf', 'rnaturalearth', 'dplyr'), repos='https://cran.rstudio.com/')"
# Copy app
COPY app.R /srv/shiny-server/
# Expose port
EXPOSE 3838
Deploy to Render's free tier with this render.yaml:
Why: Docker gives you complete control over the environment and allows deployment to any container platform.
6) Optimization for Production¶
Simplify Geometry¶
Large country polygons slow rendering. Simplify them:
library(rmapshaper)
# Simplify world data (reduce file size by ~80%)
world_simple <- ms_simplify(world, keep = 0.1)
# Use in your app
world <- world_simple
Cache Expensive Operations¶
# Cache world data globally
world <- ne_countries(returnclass = "sf")
world <- ms_simplify(world, keep = 0.1)
# Pre-compute country centroids for faster lookups
country_centroids <- st_centroid(world)
Limit Map Tiles¶
# Use lighter tile sets for faster loading
leaflet(world) %>%
addProviderTiles("CartoDB.Positron") %>% # Lightweight tiles
# ... rest of your map code
Why: Simplified geometry and lightweight tiles dramatically improve load times, especially on mobile devices.
7) Advanced Features¶
Add Country Search¶
# Add search functionality to UI
ui <- fluidPage(
titlePanel("Where Have You Been in the World?"),
sidebarLayout(
sidebarPanel(
selectInput("search_country", "Search Country:",
choices = sort(world$name),
selected = NULL),
actionButton("go_to_country", "Go to Country"),
br(), br(),
textOutput("visited_count")
),
mainPanel(
leafletOutput("map", height = "600px")
)
)
)
# Add search functionality to server
observeEvent(input$go_to_country, {
if (!is.null(input$search_country)) {
country_geom <- world[world$name == input$search_country, ]
if (nrow(country_geom) > 0) {
leafletProxy("map") %>%
setView(
lng = st_coordinates(st_centroid(country_geom))[1],
lat = st_coordinates(st_centroid(country_geom))[2],
zoom = 4
)
}
}
})
Export Visited Countries¶
# Add export button to UI
downloadButton("export_data", "Export Visited Countries")
# Add export functionality to server
output$export_data <- downloadHandler(
filename = function() {
paste("visited_countries_", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
visited_data <- world[world$iso_a3 %in% visited(), c("name", "iso_a3")]
write.csv(visited_data, file, row.names = FALSE)
}
)
Why: Search and export features make the app more useful for actual travel tracking and data analysis.
8) Troubleshooting Common Issues¶
Package Installation Problems¶
# If sf installation fails on macOS/Linux
install.packages("sf", configure.args = "--with-proj-lib=/usr/local/lib")
# Alternative: use conda
# conda install -c conda-forge r-sf r-leaflet
Memory Issues with Large Datasets¶
# Use simplified data
world <- ne_countries(scale = "medium", returnclass = "sf")
# Or load only specific regions
europe <- ne_countries(continent = "Europe", returnclass = "sf")
Deployment Authentication Issues¶
# Clear existing authentication
rsconnect::removeAccount("your-account")
# Re-authenticate with fresh credentials
rsconnect::setAccountInfo(...)
Why: These are the most common roadblocks when building and deploying Shiny apps with spatial data.
9) TL;DR Quickstart¶
# 1. Install packages
install.packages(c("shiny", "leaflet", "sf", "rnaturalearth", "dplyr", "rsconnect"))
# 2. Save the complete app code as app.R
# 3. Test locally
shiny::runApp(".")
# 4. Deploy to ShinyApps.io
rsconnect::setAccountInfo(name='yourname', token='token', secret='secret')
rsconnect::deployApp(".")
# 5. Your app is live at https://yourname.shinyapps.io/your-app-name/
10) Next Steps¶
- Add persistence: Store visited countries in a database
- User accounts: Let multiple users track their own visits
- Statistics: Add charts showing travel patterns
- Mobile optimization: Ensure touch-friendly interaction
- Performance: Implement lazy loading for large datasets
Why: This foundation gives you a working geospatial Shiny app that you can extend with advanced features as needed.
This tutorial provides everything needed to build and deploy an interactive geospatial R Shiny application. The code is production-ready and the deployment options are genuinely free.