|
|
@@ -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 |
|
|
) |