Created
November 23, 2025 19:34
-
-
Save walkerke/f1f8c7ff9421a30c7441106148844f70 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 <- 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")) |> | |
| 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 <- interpolate_palette( | |
| data = co_income, | |
| column = "estimate", | |
| method = "quantile", | |
| n = 5, | |
| palette = viridisLite::mako | |
| ) | |
| maplibre(style = carto_style("positron")) |> | |
| 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")) |> | |
| 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")) |> | |
| 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 | |
| ) | |
| ) |> | |
| 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" | |
| ) |> | |
| 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