I built out a Dash application that fetches all Declared Natural Disasters from the FEMA API, in this process I wanted to map each county that has declared a disaster. Outside of this in a previous Figure Friday focused on AWS data along with countless projects looking to break the USA down into sections I figured it would be worth sharing with the Plotly community how you can easily create County Borders for the USA. With this I created a script that will hit the census.gov
and fetch the current county borders and save them by state to a data/states/counties
folder.
import requests
from pathlib import Path
import zipfile
import geopandas as gpd
def download_county_data():
"""
Downloads county border data for all US states from the Census Bureau TIGER/Line
and saves them as GeoJSON files to data/states/counties/
"""
# Create directory structure if it doesn't exist
base_dir = Path("data/states/counties")
base_dir.mkdir(parents=True, exist_ok=True)
temp_dir = Path("temp")
temp_dir.mkdir(exist_ok=True)
# TIGER/Line FTP URL for 2023 county boundaries
base_url = "https://www2.census.gov/geo/tiger/TIGER2023/COUNTY"
filename = "tl_2023_us_county.zip"
url = f"{base_url}/{filename}"
print("Downloading US counties shapefile...")
try:
# Download the zipfile
response = requests.get(url, stream=True)
response.raise_for_status()
zip_path = temp_dir / filename
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Extract the zipfile
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
# Read the shapefile with geopandas
shp_path = temp_dir / "tl_2023_us_county.shp"
gdf = gpd.read_file(shp_path)
# Dictionary of state FIPS codes for reference
state_fips = {
'AL': '01', 'AK': '02', 'AZ': '04', 'AR': '05', 'CA': '06', 'CO': '08', 'CT': '09',
'DE': '10', 'FL': '12', 'GA': '13', 'HI': '15', 'ID': '16', 'IL': '17', 'IN': '18',
'IA': '19', 'KS': '20', 'KY': '21', 'LA': '22', 'ME': '23', 'MD': '24', 'MA': '25',
'MI': '26', 'MN': '27', 'MS': '28', 'MO': '29', 'MT': '30', 'NE': '31', 'NV': '32',
'NH': '33', 'NJ': '34', 'NM': '35', 'NY': '36', 'NC': '37', 'ND': '38', 'OH': '39',
'OK': '40', 'OR': '41', 'PA': '42', 'RI': '44', 'SC': '45', 'SD': '46', 'TN': '47',
'TX': '48', 'UT': '49', 'VT': '50', 'VA': '51', 'WA': '53', 'WV': '54', 'WI': '55',
'WY': '56'
}
# Invert the state_fips dictionary for lookup
fips_to_state = {v: k for k, v in state_fips.items()}
# Split and save by state
for state_fips, state_abbr in fips_to_state.items():
print(f"Processing {state_abbr}...")
state_counties = gdf[gdf['STATEFP'] == state_fips]
if not state_counties.empty:
output_file = base_dir / f"{state_abbr.lower()}_counties.geojson"
state_counties.to_file(output_file, driver='GeoJSON')
print(f"Successfully saved {state_abbr} counties")
# Clean up temporary files
for file in temp_dir.glob("tl_2023_us_county.*"):
file.unlink()
zip_path.unlink()
temp_dir.rmdir()
print("Successfully completed downloading and processing all county data")
except requests.exceptions.RequestException as e:
print(f"Error downloading county data: {e}")
except Exception as e:
print(f"Error processing county data: {e}")
if __name__ == "__main__":
download_county_data()
Then in your dash application you can just use these directly when building a leaflet map like so:
# Initialize a dictionary to store all state GeoJSON data
state_geojson_data = {}
# Get the path to the counties directory
counties_dir = Path('data/states/counties')
# Loop through all GeoJSON files in the directory
for geojson_file in counties_dir.glob('*.geojson'):
# Get state name from filename (assuming format like 'texas_counties.geojson' or 'texas_county_boundaries.geojson')
state_name = geojson_file.stem.split('_')[0].lower()
# Load the GeoJSON data
try:
with open(geojson_file) as f:
state_geojson_data[state_name] = json.load(f)
except Exception as e:
print(f"Error loading {geojson_file}: {e}")
# Create map layers dynamically
map_layers = [dl.TileLayer()]
map_layers.extend([
dl.GeoJSON(
data=geojson_data,
id=f"{state}-counties-layer",
hideout=dict(selected=[]),
style=style_handler
)
for state, geojson_data in state_geojson_data.items()
])
Then just put that map_layer in a dl.Map like:
dl.Map(
id='disaster-map',
center=[31.0686, -99.9018],
zoom=6,
children=map_layers,
style={'width': '100%', 'height': '50vh', "zIndex": 0}
)
Hope this helps with a data science project.
Cheers,
Pip