Skip to content

Instantly share code, notes, and snippets.

@kpivert
Forked from walkerke/custom-popup.R
Created June 5, 2025 17:19
Show Gist options
  • Save kpivert/f9d9fe5ce058b950e7c2ff2b05bade13 to your computer and use it in GitHub Desktop.
Save kpivert/f9d9fe5ce058b950e7c2ff2b05bade13 to your computer and use it in GitHub Desktop.

Revisions

  1. @walkerke walkerke created this gist Jun 5, 2025.
    179 changes: 179 additions & 0 deletions custom-popup.R
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,179 @@
    library(mapgl) # pak::pak("walkerke/mapgl")
    library(tidycensus)
    library(dplyr)
    library(viridisLite)

    # Get viridis colors
    viridis_colors <- viridis(5)

    # Get median household income by county in Texas
    tx_income <- get_acs(
    geography = "county",
    state = "TX",
    variables = "B19013_001",
    year = 2023,
    geometry = TRUE
    ) %>%
    mutate(
    # Format income with comma separator
    income_formatted = scales::dollar(estimate, accuracy = 1),
    # Create income categories for styling
    income_category = case_when(
    estimate < 50000 ~ "low",
    estimate < 75000 ~ "medium",
    estimate < 100000 ~ "high",
    TRUE ~ "very high"
    ),
    # Calculate margin of error percentage
    moe_percent = round((moe / estimate) * 100, 1),
    # Calculate percentile for progress bar (0-100)
    income_percentile = round(percent_rank(estimate) * 100)
    )

    # Create the map with an awesome styled popup
    mapboxgl(style = mapbox_style("dark"), bounds = tx_income) %>%
    add_fill_layer(
    id = "income",
    source = tx_income,
    fill_color = interpolate(
    column = "estimate",
    values = c(25000, 50000, 75000, 100000, 125000),
    stops = viridis_colors,
    na_color = "#808080"
    ),
    popup = concat(
    '<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); ',
    'padding: 20px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); ',
    'color: white; font-family: -apple-system, BlinkMacSystemFont, sans-serif; ',
    'max-width: 300px; position: relative; overflow: hidden;">',

    # CSS animations
    '<style>',
    '@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% {
    transform: scale(1); } }',
    '@keyframes slideIn { from { width: 0%; } to { width: ',
    get_column("income_percentile"),
    '%; } }',
    '@keyframes shimmer { 0% { background-position: -200% center; } 100% {
    background-position: 200% center; } }',
    '.income-badge { animation: pulse 2s ease-in-out infinite; }',
    '.progress-fill { animation: slideIn 1s ease-out forwards; }',
    '.shimmer { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3),
    transparent); ',
    'background-size: 200% 100%; animation: shimmer 2s linear infinite; }',
    '</style>',

    # Decorative background element with rotation animation
    '<div style="position: absolute; top: -50px; right: -50px; width: 150px; height: 150px;
    ',
    'background: rgba(255,255,255,0.1); border-radius: 50%; ',
    'animation: pulse 4s ease-in-out infinite;"></div>',

    # County name with icon
    '<h2 style="margin: 0 0 15px 0; font-size: 24px; font-weight: 700; ',
    'text-shadow: 2px 2px 4px rgba(0,0,0,0.3); display: flex; align-items: center; gap:
    10px;">',
    '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
    stroke-width="2">',
    '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>',
    '<circle cx="12" cy="10" r="3"></circle></svg>',
    get_column("NAME"),
    '</h2>',

    # Income display with animated badge
    '<div class="income-badge" style="background: rgba(255,255,255,0.2); padding: 15px;
    border-radius: 8px; ',
    'margin-bottom: 15px; backdrop-filter: blur(10px); position: relative;">',
    '<div class="shimmer" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;
    ',
    'border-radius: 8px; pointer-events: none;"></div>',
    '<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">Median Household
    Income</div>',
    '<div style="font-size: 32px; font-weight: 700; text-shadow: 2px 2px 4px
    rgba(0,0,0,0.3);">',
    get_column("income_formatted"),
    '</div>',
    '</div>',

    # Stats grid with conditional coloring
    '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom:
    15px;">',

    # MOE card
    '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;
    text-align: center;">',
    '<div style="font-size: 12px; opacity: 0.8; margin-bottom: 3px;">Margin of Error</div>',
    '<div style="font-size: 18px; font-weight: 600;">±',
    get_column("moe_percent"),
    '%</div>',
    '</div>',

    # Category card with conditional background (using viridis-inspired colors)
    '<div style="background: ',
    'rgba(',
    match_expr(
    column = "income_category",
    values = c("low", "medium", "high", "very high"),
    stops = c("68,1,84", "59,82,139", "33,145,140", "253,231,37")
    ),
    ',0.3); padding: 10px; border-radius: 6px; text-align: center;">',
    '<div style="font-size: 12px; opacity: 0.8; margin-bottom: 3px;">Income Level</div>',
    '<div style="font-size: 16px; font-weight: 600; text-transform: uppercase;">',
    get_column("income_category"),
    '</div>',
    '</div>',
    '</div>',

    # Animated progress bar with cleaner percentile display
    '<div style="margin-bottom: 10px;">',
    '<div style="font-size: 12px; opacity: 0.8; margin-bottom: 5px;">',
    'Percentile: ',
    get_column("income_percentile"),
    '</div>',
    '<div style="background: rgba(0,0,0,0.3); height: 12px; border-radius: 6px; overflow:
    hidden; position: relative;">',
    '<div class="progress-fill" style="background: linear-gradient(90deg, ',
    viridis_colors[1],
    ' 0%, ',
    viridis_colors[2],
    ' 25%, ',
    viridis_colors[3],
    ' 50%, ',
    viridis_colors[4],
    ' 75%, ',
    viridis_colors[5],
    ' 100%); ',
    'height: 100%; width: ',
    get_column("income_percentile"),
    '%; ',
    'box-shadow: 0 0 10px rgba(255,255,255,0.5); border-radius: 6px;"></div>',
    '<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; ',
    'background: repeating-linear-gradient(45deg, transparent, transparent 10px,
    rgba(255,255,255,0.1) 10px, rgba(255,255,255,0.1) 20px); ',
    'animation: slideIn 1s ease-out forwards;"></div>',
    '</div>',
    '</div>',

    # Data source footer
    '<div style="font-size: 11px; opacity: 0.7; text-align: center; ',
    'padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.2);">',
    '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
    style="vertical-align: middle; margin-right: 3px;">',
    '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1
    17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2
    2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1
    0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>',
    '</svg>',
    'American Community Survey 2023 (5-year estimates)',
    '</div>',

    '</div>'
    )
    ) %>%
    add_continuous_legend(
    colors = viridis_colors,
    values = c("$25k", "$50k", "$75k", "$100k", "$125k"),
    legend_title = "Median Household Income",
    position = "bottom-left",
    margin_bottom = 30
    )