When implementing EPSG:3857 vs EPSG:4326 in Folium, the operational rule is strict: always supply coordinates in EPSG:4326 (WGS84 latitude/longitude). Folium’s Python API expects unprojected geographic coordinates for all markers, popups, and vector overlays. The underlying Leaflet engine automatically converts these inputs to EPSG:3857 (Web Mercator) for tile rendering and canvas projection.
If you pass pre-projected EPSG:3857 meter values directly into folium.Marker() or folium.GeoJson(), Leaflet will misinterpret them as degrees. This typically places features thousands of kilometers off-target, often rendering them in the Atlantic Ocean or Antarctica. Tile layers are served in EPSG:3857, but the input contract remains firmly anchored to EPSG:4326.
How the Projection Pipeline Works
Folium is a Python wrapper around Leaflet.js, which defaults to L.CRS.EPSG3857. The map canvas, zoom levels, and tile grids operate in Web Mercator meters. However, Leaflet’s public API is explicitly designed around EPSG:4326 for developer ergonomics.
The separation of concerns works as follows:
- Input Layer: You pass
[lat, lon]pairs to Folium methods. - Transformation Layer: Leaflet internally projects these coordinates using spherical Mercator math.
- Rendering Layer: The projected coordinates align with the EPSG:3857 tile grid served by providers like OpenStreetMap or CartoDB.
Folium does not expose a crs constructor argument — the rendering CRS is always EPSG:3857 and is managed entirely by Leaflet internally. For a deeper breakdown of how coordinate math stays deterministic across mapping stacks, review established Core Mapping Architecture & Rendering patterns.
Data Ingestion & ETL Best Practices
Automated web mapping requires strict CRS normalization before data reaches the Folium builder. Never rely on implicit conversions inside rendering calls:
- Verify Source CRS: Check metadata from PostGIS, ArcGIS exports, or CAD files. Assume nothing.
- Transform Early: Use
geopandasorpyprojto convert all spatial data to EPSG:4326 before serialization. - Validate Coordinate Order: Folium strictly expects
[latitude, longitude]. Many GIS tools default to[longitude, latitude]or(x, y). Swapping these is the most common cause of misaligned markers. - Centralize Projection Logic: When teams manage complex spatial workflows, centralized CRS & Projection Management prevents silent drift between staging and production deployments.
Production-Ready Implementation
The following snippet demonstrates correct coordinate handling, explicit CRS documentation, and a safe fallback for legacy EPSG:3857 datasets. It uses pyproj for deterministic transformations and aligns with the official pyproj Transformer documentation.
import folium
import pyproj
import json
# 1. Define CRS and transformer
wgs84 = pyproj.CRS("EPSG:4326")
web_mercator = pyproj.CRS("EPSG:3857")
# always_xy=True ensures (lon, lat) order matches standard GIS conventions
transformer = pyproj.Transformer.from_crs(web_mercator, wgs84, always_xy=True)
def epsg3857_to_folium(x_meters, y_meters):
"""Convert EPSG:3857 (meters) to Folium-ready [lat, lon]"""
lon, lat = transformer.transform(x_meters, y_meters)
return [lat, lon] # Folium strictly requires [lat, lon]
# 2. Initialize map — Folium always renders in EPSG:3857 internally;
# all coordinate inputs must be in EPSG:4326
m = folium.Map(
location=[40.7128, -74.0060], # NYC in EPSG:4326
zoom_start=12,
tiles="CartoDB positron",
)
# 3. Add native EPSG:4326 marker
folium.Marker(
location=[40.7128, -74.0060],
popup="Native WGS84 Coordinate",
tooltip="EPSG:4326 input"
).add_to(m)
# 4. Handle legacy EPSG:3857 data safely
legacy_x = -8238310.24 # NYC in Web Mercator meters
legacy_y = 4969803.55
converted_loc = epsg3857_to_folium(legacy_x, legacy_y)
folium.CircleMarker(
location=converted_loc,
radius=8,
color="red",
fill=True,
popup="Converted from EPSG:3857"
).add_to(m)
# 5. Add GeoJSON (must be EPSG:4326 compliant)
# folium.GeoJson reads GeoJSON spec, which mandates WGS84
geojson_data = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.0060, 40.7128] # GeoJSON spec: [lon, lat]
},
"properties": {"name": "GeoJSON Point"}
}
folium.GeoJson(geojson_data).add_to(m)
# m.save("map_output.html")
Key Implementation Notes
- GeoJSON Spec Compliance: The GeoJSON standard (RFC 7946) mandates
[longitude, latitude]. Folium parses this correctly and projects it internally. always_xy=True: Critical forpyproj. Without it, axis order defaults to the CRS definition, which can flip coordinates depending on the EPSG version.- No
crsparameter:folium.Map()does not accept acrsargument. Leaflet’s CRS is fixed atL.CRS.EPSG3857and cannot be overridden through Folium’s constructor. If you need a non-Mercator CRS (e.g., EPSG:4326 for polar maps), useipyleafletwith a customcrsprojection instead.
Common Pitfalls & Debugging Checklist
Silent projection mismatches rarely throw errors; they simply render features in the wrong location:
| Symptom | Likely Cause | Fix |
|---|---|---|
| Marker appears in ocean/Antarctica | EPSG:3857 meters passed as degrees | Run coordinates through pyproj.Transformer before Folium |
| Marker appears in correct city but flipped | [lon, lat] passed instead of [lat, lon] |
Swap tuple order or verify always_xy=True behavior |
| GeoJSON renders offset or invisible | Source GeoJSON uses local CRS instead of WGS84 | Reproject to EPSG:4326 using geopandas.to_crs("EPSG:4326") |
| Custom tiles misalign with markers | Tile server uses non-standard CRS | Verify tile provider CRS matches Leaflet’s L.CRS.EPSG3857 |
For quick coordinate validation, cross-check raw values against epsg.io before passing them to your mapping stack.
Summary
Folium abstracts projection math so developers can focus on data visualization, but it requires strict adherence to its input contract. Always normalize to EPSG:4326 before calling Folium constructors, handle EPSG:3857 conversions upstream in your ETL layer, and validate coordinate order at every ingestion point. There is no crs constructor argument in Folium — the library always renders through Leaflet’s EPSG:3857 pipeline. This approach guarantees pixel-perfect alignment across tile grids, vector overlays, and interactive markers.