Skip to content

Instantly share code, notes, and snippets.

@walkerke
Created November 23, 2025 19:34
Show Gist options
  • Select an option

  • Save walkerke/f1f8c7ff9421a30c7441106148844f70 to your computer and use it in GitHub Desktop.

Select an option

Save walkerke/f1f8c7ff9421a30c7441106148844f70 to your computer and use it in GitHub Desktop.
library(shiny)
library(mapgl)
library(tidycensus)
library(sf)
# Get data: median household income by tract in Colorado
co_income <- get_acs(
geography = "tract",
variables = "B19013_001",
state = "CO",
year = 2023,
geometry = TRUE
)
ui <- fluidPage(
tags$head(
tags$style(HTML(
"
.code-container {
position: relative;
}
.copy-button {
position: absolute;
bottom: 5px;
right: 5px;
padding: 5px 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
opacity: 0.7;
transition: opacity 0.2s;
}
.copy-button:hover {
opacity: 1;
}
.code-container pre {
background-color: #f5f5f5;
padding: 15px;
padding-bottom: 40px;
border-radius: 5px;
overflow-x: auto;
}
.code-container pre code {
font-family: 'Courier New', Monaco, Consolas, monospace;
font-size: 13px;
}
"
))
),
tags$script(HTML(
"
function copyCode(button) {
const code = button.nextElementSibling.querySelector('code').textContent;
navigator.clipboard.writeText(code).then(function() {
button.textContent = 'Copied!';
setTimeout(function() {
button.textContent = 'Copy';
}, 2000);
});
}
"
)),
story_maplibre(
map_id = "map",
sections = list(
"intro" = story_section(
title = "Building Interactive Maps with mapgl",
content = list(
h3("An Interactive Tutorial"),
p(
"Welcome! This tutorial will walk you through creating a professional choropleth map in R using the mapgl package."
),
p(
"We'll build a map of median household income by Census tract in Colorado, step by step."
),
h4("Installation"),
p("First, install the required packages:"),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>install.packages(c("tidycensus", "mapgl", "shiny"))</code></pre>
</div>'
),
p("Then load the libraries:"),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>library(shiny)
library(mapgl)
library(tidycensus)
library(sf)</code></pre>
</div>'
),
p("Ready? Let's get started!")
),
position = "center",
width = 600
),
"raw" = story_section(
title = "STEP 1: Acquire the Data",
width = 500,
content = list(
p(
"First, we need to get Census data with geometry using tidycensus:"
),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>library(tidycensus)
library(sf)
co_income &lt;- get_acs(
geography = "tract",
variables = "B19013_001",
state = "CO",
year = 2023,
geometry = TRUE
)</code></pre>
</div>'
),
p(
"This fetches median household income (B19013_001) for all Census tracts in Colorado with spatial boundaries."
)
)
),
"basic" = story_section(
title = "STEP 2: Display the Polygons",
width = 500,
content = list(
p(
"Now let's add the Census tracts to the map using add_fill_layer():"
),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>library(mapgl)
maplibre(style = carto_style("positron")) |&gt;
add_fill_layer(
id = "income",
source = co_income
)</code></pre>
</div>'
),
p(
"With no styling specified, all polygons appear black with full opacity."
)
)
),
"refined" = story_section(
title = "STEP 3: Add Color with interpolate_palette()",
width = 500,
content = list(
p("Now let's style the polygons with a continuous color scale:"),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>library(viridisLite)
income_scale &lt;- interpolate_palette(
data = co_income,
column = "estimate",
method = "quantile",
n = 5,
palette = viridisLite::mako
)
maplibre(style = carto_style("positron")) |&gt;
add_fill_layer(
id = "income",
source = co_income,
fill_color = income_scale$expression,
fill_opacity = 0.7
)</code></pre>
</div>'
),
p(
"The interpolate_palette() helper automatically calculates breaks and creates smooth color transitions."
)
)
),
"interactive" = story_section(
title = "STEP 4: Add Interactivity",
width = 500,
content = list(
p(
"Finally, let's add tooltips and hover effects for user interaction:"
),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>maplibre(style = carto_style("positron")) |&gt;
add_fill_layer(
id = "income",
source = co_income,
fill_color = income_scale$expression,
fill_opacity = 0.7,
tooltip = "estimate",
hover_options = list(
fill_color = "yellow",
fill_opacity = 1
)
)</code></pre>
</div>'
),
p(
"Now users can hover to see income values and the tract highlights in yellow!"
)
)
),
"polish" = story_section(
title = "STEP 5: Add Legend and Geocoder",
width = 500,
content = list(
p(
"Let's polish the map by adding a legend and search functionality:"
),
HTML(
'<div class="code-container">
<button class="copy-button" onclick="copyCode(this)">Copy</button>
<pre><code>maplibre(style = carto_style("positron")) |&gt;
add_fill_layer(
id = "income",
source = co_income,
fill_color = income_scale$expression,
fill_opacity = 0.7,
tooltip = "estimate",
hover_options = list(
fill_color = "yellow",
fill_opacity = 1
)
) |&gt;
add_legend(
"Median Household Income",
values = get_legend_labels(
income_scale,
digits = 0,
format = "compact",
prefix = "$"
),
colors = get_legend_colors(income_scale),
type = "continuous",
width = "300px"
) |&gt;
add_geocoder_control()</code></pre>
</div>'
),
p(
"The legend uses helper functions to extract values and colors from our income_scale, and the geocoder lets users search for locations."
)
)
),
"final" = story_section(
title = "The Complete Map",
content = list(
h3("Mission Accomplished!"),
p(
"You've just watched a professional choropleth map come together, step by step."
),
p("mapgl makes this process seamless in R.")
),
position = "center"
)
)
)
)
server <- function(input, output, session) {
# Create the continuous color scale
income_scale <- interpolate_palette(
data = co_income,
column = "estimate",
method = "quantile",
n = 5,
palette = viridisLite::mako
)
output$map <- renderMaplibre({
maplibre(
style = carto_style("positron"),
bounds = co_income,
scrollZoom = FALSE
)
})
# Intro: Just show the base map
on_section("map", "intro", {
maplibre_proxy("map") |>
clear_layer("income") |>
clear_layer("boundaries") |>
clear_controls() |>
fit_bounds(
co_income,
animate = TRUE,
duration = 2000,
pitch = 0,
bearing = 0
)
})
# Step 1: Don't show any data yet, just the base map
on_section("map", "raw", {
maplibre_proxy("map") |>
clear_layer("income") |>
clear_layer("boundaries") |>
clear_controls() |>
fit_bounds(
co_income,
animate = TRUE,
duration = 2000,
pitch = 0,
bearing = 0
)
})
# Step 2: Add polygons with default styling (black, opacity 1)
on_section("map", "basic", {
maplibre_proxy("map") |>
clear_layer("boundaries") |>
clear_controls() |>
add_fill_layer(
id = "income",
source = co_income
)
})
# Step 3: Add continuous color scale with interpolate_palette()
on_section("map", "refined", {
maplibre_proxy("map") |>
clear_controls() |>
set_paint_property(
"income",
"fill-color",
income_scale$expression
) |>
set_paint_property(
"income",
"fill-opacity",
0.7
) |>
fly_to(
center = c(-104.9903, 39.7392),
zoom = 10,
pitch = 30,
bearing = 0,
duration = 2500
)
})
# Step 4: Add interactivity with tooltip and hover effects
on_section("map", "interactive", {
maplibre_proxy("map") |>
clear_layer("income") |>
clear_controls() |>
clear_legend() |>
add_fill_layer(
id = "income",
source = co_income,
fill_color = income_scale$expression,
fill_opacity = 0.7,
tooltip = "estimate",
hover_options = list(
fill_color = "yellow",
fill_opacity = 1
)
) |>
fly_to(
center = c(-104.9903, 39.7392),
zoom = 11,
pitch = 45,
bearing = 0,
duration = 2000
)
})
# Step 5: Add legend and geocoder control
on_section("map", "polish", {
maplibre_proxy("map") |>
clear_controls() |>
add_legend(
"Median Household Income",
values = get_legend_labels(
income_scale,
digits = 0,
format = "compact",
prefix = "$"
),
colors = get_legend_colors(income_scale),
type = "continuous",
width = "300px",
margin_bottom = 30,
position = "bottom-right"
) |>
add_geocoder_control() |>
fly_to(
center = c(-104.9903, 39.7392),
zoom = 10,
pitch = 0,
bearing = 0,
duration = 2000
)
})
# Final: Pull back to see the whole state
on_section("map", "final", {
maplibre_proxy("map") |>
fit_bounds(
co_income,
animate = TRUE,
duration = 3000,
pitch = 0,
bearing = 0
)
})
}
shinyApp(ui, server)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment