Skip to content

folium Essentials

Interactive Leaflet maps from Python. folium makes point markers, choropleths, and heatmaps easy to build and share as HTML — ideal for location-based data science results.

Table of Contents

Installation / Setup

pip install folium pandas     # or: uv add folium pandas
import folium
from folium import plugins

folium renders inline in Jupyter; in scripts it produces an HTML file you open in a browser. No API keys needed for OpenStreetMap tiles.

Core Syntax

Create a map

m = folium.Map(location=[51.5072, -0.1276],   # [lat, lng]
               zoom_start=10,
               tiles="OpenStreetMap",          # or "CartoDB positron", "CartoDB dark_matter", "Stamen Terrain"
               width="100%", height="70%",
               control_scale=True)
m
# or save: m.save("map.html")

Markers & popups

folium.Marker(
    location=[51.5, -0.12],
    popup=folium.Popup("<b>Office</b><br>Open 9-5", max_width=250),
    tooltip="Click me",
    icon=folium.Icon(color="red", icon="info-sign"),   # green, red, blue, orange...
).add_to(m)

folium.CircleMarker(
    location=[51.51, -0.13], radius=8,
    color="crimson", fill=True, fill_color="crimson",
    popup="circle",
).add_to(m)

Marker clusters & heatmaps (plugins)

cluster = plugins.MarkerCluster().add_to(m)
for lat, lng, name in zip(df["lat"], df["lng"], df["name"]):
    folium.Marker([lat, lng], popup=name).add_to(cluster)

plugins.HeatMap(df[["lat", "lng", "count"]].values, radius=15).add_to(m)

Layer control

folium.TileLayer("CartoDB positron").add_to(m)
folium.LayerControl().add_to(m)     # toggle tile layers / overlays in the corner

Common Operations

Choropleth (fill regions by a value)

# geo_data: GeoJSON with region shapes;  data: DataFrame with the value
folium.Choropleth(
    geo_data="world.geojson",                    # path/URL to GeoJSON
    data=df,                                     # DataFrame
    columns=["country", "value"],                # key column + value column
    key_on="feature.properties.name",            # GeoJSON property matching the key
    fill_color="YlOrRd",                         # colormap
    fill_opacity=0.7, line_opacity=0.2,
    legend_name="value",
    highlight=True,
).add_to(m)

# tooltips show per-region values when hovering
folium.features.GeoJsonTooltip(fields=["name"], aliases=["Country"]).add_to(...)

Polylines & shapes

folium.PolyLine([[51.5, -0.12], [51.51, -0.13], [51.52, -0.11]],
                color="blue", weight=3, opacity=0.8).add_to(m)
folium.Polygon([[51.5, -0.12], [51.51, -0.13], [51.52, -0.11]],
               color="green", fill=True).add_to(m)

Fullscreen & search (handy for reports)

plugins.Fullscreen().add_to(m)
plugins.MousePosition().add_to(m)

Save & embed

m.save("output/map.html")                    # standalone, shareable
html = m.get_root().render()                 # raw HTML string

Practical Examples

1. Plot store locations with revenue popups

import folium
import pandas as pd

stores = pd.read_csv("stores.csv")           # columns: name, lat, lng, revenue
m = folium.Map(location=[stores["lat"].mean(), stores["lng"].mean()], zoom_start=6)

for _, s in stores.iterrows():
    folium.Marker(
        [s["lat"], s["lng"]],
        popup=f"<b>{s['name']}</b><br>Revenue: ${s['revenue']:,.0f}",
        icon=folium.Icon(color="green" if s["revenue"] > 100_000 else "orange"),
    ).add_to(m)
m.save("stores.html")

2. Density heatmap of customer activity

heat = plugins.HeatMap(
    df[["lat", "lng"]].values, radius=12, blur=10, min_opacity=0.3
)
heat.add_to(m)
folium.LayerControl().add_to(m)

3. Choropleth of values by country

folium.Choropleth(
    geo_data="https://raw.githubusercontent.com/python-visualization/folium/main/examples/data/world-countries.json",
    data=df, columns=["country", "metric"],
    key_on="feature.properties.name",
    fill_color="BuPu", legend_name="metric",
).add_to(m)

Common Patterns

# Map centered on data bounds (automatically)
m = folium.Map(location=[df.lat.mean(), df.lng.mean()], zoom_start=5)

# Fit map to markers
m.fit_bounds([[df.lat.min(), df.lng.min()], [df.lat.max(), df.lng.max()]])

# Styled popup from a row
folium.Marker([lat, lng],
    popup=folium.Popup(f"{name} — {value:.1f}", show=False),
    tooltip=name).add_to(m)

# Always keep base map + data layers separable for LayerControl

Best Practices

  • Confirm lat/lng order — folium uses [latitude, longitude], the reverse of many GeoJSON tools.
  • Cluster many markers (MarkerCluster) — hundreds of raw markers slow the map.
  • Use light tiles (CartoDB positron) when overlaying heatmaps/choropleths for contrast.
  • Keep key_on consistent — choropleth joins on GeoJSON property names, which must match your columns values exactly.
  • Export m.save("map.html") for sharing; notebooks render inline automatically.
  • For heavy spatial analysis (joins, CRS), hand off to geopandas.md; folium is for display.

Common Mistakes

Mistake Consequence Correct approach
[lng, lat] ordering Markers in the ocean Always [lat, lng]
Key/value name mismatch in choropleth Regions silently unfilled Check GeoJSON property names vs DataFrame values
Raw markers for 10k points Frozen browser MarkerCluster or HeatMap
Choropleth legend in %/USD unclear Misleading map legend_name with units; check scale
Missing fill=True on circles Invisible fills Set fill=True, fill_color=...
GeoJson vs Choropleth confusion No value mapping Choropleth for value-filled regions

Troubleshooting

Symptom Cause Fix
Blank map in notebook Tiles blocked (offline/network) Check internet; try a different tiles provider
Choropleth all one color key_on doesn't match data keys Print one GeoJSON feature.properties to inspect
HeatMap values ignored Wrong column order Pass [lat, lng, weight] columns in that order
Map too tall/short Default sizing Set width/height (e.g. "100%", 600)
Save produces empty HTML m.save before adding layers Call save after all .add_to(m)

Official Documentation

Last Updated

2026-09-06 — Initial version.