Source code: Interactive web maps from Python with Folium and GeoPandas .explore()
Runnable Python scripts, requirements and console output for the tutorial “Interactive web maps from Python with Folium and GeoPandas .explore()”.
Last updated
These are the exact scripts used to produce every figure and number in Interactive web maps from Python with Folium and GeoPandas .explore(). Download them, install the requirements into a fresh virtual environment and run them from this folder.
- folium_maps.py
- requirements.txt
- console.txt: the console output recorded when we ran it
python -m venv .venv.venv\Scripts\activate # macOS/Linux: source .venv/bin/activatepip install -r requirements.txtpython folium_maps.pyfolium_maps.py
"""Interactive web maps with Folium - TechnicalGIS tutorial.
Builds two Leaflet maps from Python: 1. a one-line choropleth with GeoDataFrame.explore() 2. a hand-crafted Folium map with a choropleth, clustered city markers, multiple basemaps, plugins and a layer switcher.Run with: python folium_maps.py (then open output/*.html in a browser)"""import urllib.requestfrom pathlib import Path
import branca.colormap as cmimport foliumimport geopandas as gpdfrom folium import plugins
HERE = Path(__file__).parentDATA = HERE / "data"OUT = HERE / "output"DATA.mkdir(exist_ok=True)OUT.mkdir(exist_ok=True)NE = "https://naciscdn.org/naturalearth/50m/cultural/"# Keyless basemaps. (CARTO's free tiles now require an API key for most uses.)ESRI_GRAY = "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}"ESRI_GRAY_ATTR = "Tiles © Esri — Esri, DeLorme, NAVTEQ"
def fetch(name: str) -> Path: dest = DATA / name if not dest.exists(): req = urllib.request.Request(NE + name, headers={"User-Agent": "technicalgis-tutorial"}) with urllib.request.urlopen(req) as r: dest.write_bytes(r.read()) return dest
def section(title: str) -> None: print(f"\n=== {title} ===")
# --- Step 1: prepare the data ---------------------------------------------------countries = gpd.read_file(fetch("ne_50m_admin_0_countries.zip"))countries = countries[["NAME", "CONTINENT", "POP_EST", "GDP_MD", "geometry"]].rename( columns={"NAME": "name", "CONTINENT": "continent", "POP_EST": "population", "GDP_MD": "gdp_musd"})europe = countries[countries["continent"] == "Europe"].copy()europe["area_km2"] = europe.to_crs("EPSG:3035").area / 1e6 # ETRS89 LAEA Europe, equal areaeurope["density"] = (europe["population"] / europe["area_km2"]).round(1)europe["gdp_per_capita"] = (europe["gdp_musd"] * 1e6 / europe["population"]).round(0)europe = europe[europe["population"] > 0]
places = gpd.read_file(fetch("ne_50m_populated_places.zip"))[["NAME", "ADM0NAME", "POP_MAX", "geometry"]]places = places.rename(columns={"NAME": "city", "ADM0NAME": "country", "POP_MAX": "population"})eu_places = gpd.sjoin(places, europe[["geometry"]], predicate="within").drop(columns="index_right")section("data")print(len(europe), "European countries;", len(eu_places), "populated places")print(europe.sort_values("density", ascending=False)[["name", "population", "area_km2", "density"]] .head(5).round(0).to_string(index=False))
# Simplify geometries (~1 km) to keep the HTML small - the browser draws every vertex.# Clip to a European window first: Russia crosses the antimeridian, which would otherwise# draw a band across the whole map.europe = europe.clip((-25, 34, 50, 72))europe["geometry"] = europe.to_crs("EPSG:3035").simplify(1000).to_crs("EPSG:4326")
# --- Step 2: the one-liner -------------------------------------------------------m1 = europe.explore(column="density", scheme="Quantiles", k=5, cmap="YlGnBu", tooltip=["name", "population", "density"], legend_kwds={"caption": "People per km²", "colorbar": False, "fmt": "{:.0f}"}, tiles=ESRI_GRAY, attr=ESRI_GRAY_ATTR, location=[54, 15], zoom_start=4)m1.save(OUT / "explore_density.html")
# --- Step 3: a hand-crafted Folium map --------------------------------------------m = folium.Map(location=[54, 12], zoom_start=4, tiles=None, control_scale=True)folium.TileLayer("OpenStreetMap", name="OpenStreetMap").add_to(m)folium.TileLayer(ESRI_GRAY, attr=ESRI_GRAY_ATTR, name="Light grey (Esri)").add_to(m)folium.TileLayer( tiles="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", attr="Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community", name="Esri World Imagery").add_to(m)
colormap = cm.LinearColormap(["#f7fcf0", "#7bccc4", "#0868ac"], vmin=float(europe.gdp_per_capita.quantile(0.05)), vmax=float(europe.gdp_per_capita.quantile(0.95)), caption="GDP per capita (USD, Natural Earth estimates)")
folium.GeoJson( europe[["name", "population", "density", "gdp_per_capita", "geometry"]], name="GDP per capita", style_function=lambda f: {"fillColor": colormap(f["properties"]["gdp_per_capita"]), "color": "#475569", "weight": 0.6, "fillOpacity": 0.7}, highlight_function=lambda f: {"weight": 2.5, "color": "#e8590c", "fillOpacity": 0.85}, tooltip=folium.GeoJsonTooltip(fields=["name", "gdp_per_capita", "density"], aliases=["Country", "GDP per capita (USD)", "People per km²"], localize=True, sticky=True),).add_to(m)colormap.add_to(m)
cluster = plugins.MarkerCluster(name="Cities (clustered)").add_to(m)for _, r in eu_places.iterrows(): folium.Marker( location=[r.geometry.y, r.geometry.x], tooltip=r["city"], popup=folium.Popup(f"<b>{r['city']}</b><br>{r['country']}<br>Population: {r['population']:,}", max_width=220), icon=folium.Icon(color="darkblue", icon="info-sign"), ).add_to(cluster)
big = eu_places[eu_places["population"] >= 2_000_000]fg = folium.FeatureGroup(name="Cities ≥ 2 million", show=False).add_to(m)for _, r in big.iterrows(): folium.CircleMarker([r.geometry.y, r.geometry.x], radius=4 + r["population"] / 1.5e6, color="#e8590c", fill=True, fill_opacity=0.6, weight=1, tooltip=f"{r['city']}: {r['population']:,}").add_to(fg)
plugins.Fullscreen().add_to(m)plugins.MiniMap(toggle_display=True).add_to(m)plugins.MeasureControl(primary_length_unit="kilometers").add_to(m)folium.LayerControl(collapsed=False).add_to(m)m.save(OUT / "europe_folium.html")
section("saved")for f in sorted(OUT.glob("*.html")): print(f"{f.name:24s} {f.stat().st_size / 1024:,.0f} KB")