Created
June 5, 2025 15:28
-
-
Save walkerke/2b62551c5fecfd88575a1b5de8793bda 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(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 | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment