GeoJSON is a lightweight format for encoding geographic data structures such as Points, LineStrings, and Polygons. It’s built on top of JSON and is widely supported across mapping libraries and APIs.
In this tutorial, we’ll explore how to render GeoJSON geometry types on a Leaflet map — including:
- Point features representing real-world locations from the Geoapify Places API
- LineString features representing travel routes from the Geoapify Routing API
- Polygon features representing reachable areas (isochrones or isodistances) from the Geoapify Isoline API
Each Geoapify API returns data as a FeatureCollection, where each Feature includes a geometry object and properties. Leaflet provides native support for GeoJSON via the L.geoJSON() method, making it easy to visualize these geometries directly on a map.
By the end of this tutorial, you’ll have an interactive Leaflet map that displays Points, LineStrings, and Polygons — all styled and layered together using real Geoapify API data.
Table of Contents
- About Leaflet Functions for Working with GeoJSON
- Visualizing GeoJSON Points (Places API)
- Visualizing GeoJSON Lines (Routing API)
- Visualizing GeoJSON Polygons (Isoline API)
- Summary
- FAQ
About Leaflet Functions for Working with GeoJSON
Leaflet provides native support for the GeoJSON format through the L.geoJSON() method. It can load any valid GeoJSON Feature, FeatureCollection, or Geometry, automatically creating the correct map layer type — a Marker for Point, a Polyline for LineString, or a Polygon for Polygon.
L.geoJSON(data, options)
The L.geoJSON() factory function takes two arguments:
-
data— a valid GeoJSON object (Feature or FeatureCollection) -
options— an optional configuration object that lets you customize how each geometry type is displayed and how features behave
Example:
L.geoJSON(geojsonData, options).addTo(map);
The options parameter supports several useful callbacks and style settings:
style
Applies visual styling (color, weight, fill, opacity, etc.) to LineString and Polygon geometries.
const lineLayer = L.geoJSON(routeData, {
style: function (feature) {
return { color: '#1976d2', weight: 4, opacity: 0.9 };
}
}).addTo(map);
Docs: Path options
pointToLayer
Controls how Point features are rendered — for example, as custom markers, circle markers, or icons.
const pointsLayer = L.geoJSON(placeData, {
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 6,
fillColor: '#ff5722',
color: '#fff',
weight: 1,
fillOpacity: 0.9
});
}
}).addTo(map);
Docs: GeoJSON options
onEachFeature
Adds interactivity to each feature — for example, binding popups or click handlers.
const interactiveLayer = L.geoJSON(geojsonData, {
onEachFeature: function (feature, layer) {
if (feature.properties && feature.properties.name) {
layer.bindPopup(feature.properties.name);
}
}
}).addTo(map);
Docs: Popup
By combining these options, you can display and style GeoJSON data returned by any of the Geoapify APIs — whether that’s Points from the Places API, LineStrings from the Routing API, or Polygons from the Isoline API.
1. Visualizing GeoJSON Points (Places API)
To start, we’ll visualize GeoJSON Point features returned by the Geoapify Places API.
Each Point represents a real-world place — in this example, schools within a specific area.
Live demo on CodePen: https://codepen.io/geoapify/pen/zxraMEp
In this code sample, each school from the Places API is shown as a stylized marker with a popup showing its name and address.
This demonstrates how to directly visualize API responses that use GeoJSON Point geometries in Leaflet.
GeoJSON Points: Fetching GeoJSON data from the Places API
The Places API endpoint returns a FeatureCollection where each Feature has:
geometry.type = "Point"geometry.coordinates = [longitude, latitude]-
propertiesdescribing the place (name, address, category, etc.)
Example fetch request (schools in Jacksonville, US):
const apiKey = "YOUR_API_KEY";
const url = `https://api.geoapify.com/v2/places?categories=education.school&filter=place:51bea59c2ff66954c059d1425dff09553e40f00101f901e0d0010000000000c00208&limit=500&apiKey=${apiKey}`;
fetch(url)
.then(response => response.json())
.then(data => {
L.geoJSON(data, options).addTo(map);
});
GeoJSON Points: Displaying GeoJSON with L.geoJSON()
Leaflet’s L.geoJSON() method automatically recognizes each geometry and adds it to the map.
To customize the rendering of Point features, we use the pointToLayer and onEachFeature options.
L.geoJSON(data, {
pointToLayer: function (feature, latlng) {
return L.marker(latlng, { icon: schoolIcon });
},
onEachFeature: function (feature, layer) {
const name = feature.properties.name || "Unnamed school";
const address = feature.properties.address_line2 || "";
layer.bindPopup(`<strong>${name}</strong><br>${address}`);
}
}).addTo(map);
GeoJSON Points: Adding custom marker icons
Instead of default Leaflet pins, we use the Geoapify Marker Icon API
to create branded, retina-friendly icons dynamically by URL.
const schoolIcon = L.icon({
iconUrl: `https://api.geoapify.com/v2/icon/?type=awesome&color=%23e2b928&size=48&icon=school&iconType=lucide&contentSize=20&noWhiteCircle&scaleFactor=2&apiKey=${yourAPIKey}`,
iconSize: [36, 53], // width, height in pixels
iconAnchor: [18, 48], // point of the icon which corresponds to marker’s location, 5px is for shadow
popupAnchor: [0, -55] // where popups open relative to the iconAnchor
});
This icon configuration:
- sets the Lucide "school" icon with a custom orange color (
#efa00b), - ensures crisp display on Retina screens,
- includes a small decorative shadow for better contrast on light map backgrounds.
2. Visualizing GeoJSON Lines (Routing API)
We’ll render a route as a GeoJSON MultiLineString returned by the Geoapify Routing API and style it with a subtle shadow, rounded caps/joins, and waypoint markers. Turn-by-turn steps are shown both in a sidebar and as small white circle markers on the route.
Live demo (CodePen): https://codepen.io/geoapify/pen/ogbyJeN
This code sample demonstrates:
- Calculating route between three waypoints with Geoapify Routing API
- Rendering a GeoJSON MultiLineString via L.geoJSON(data, options)
- Using the options parameter to style paths and layering the same geometry twice for a subtle shadow effect
- Reading waypoint and step metadata from properties to add markers and a turn-by-turn list
- Managing visual stacking with custom panes and zIndex
Here are key parts of the example related to GeoJSON LineString visualization:
GeoJSON Lines: Fetch route as GeoJSON
const apiKey = "YOUR_API_KEY";
const url = "https://api.geoapify.com/v1/routing?waypoints=...&mode=drive&apiKey=" + apiKey;
const res = await fetch(url);
const fc = await res.json();
const routeFeature = fc.features[0]; // geometry: MultiLineString
GeoJSON Lines: Draw the route with a shadow underlay
We render the same GeoJSON twice using L.geoJSON(data, options): first a thick semi-transparent shadow, then the colored line above it. Using panes keeps ordering predictable.
map.createPane('route-shadow');
map.getPane('route-shadow').style.zIndex = 399;
map.createPane('route-line');
map.getPane('route-line').style.zIndex = 400;
const shadow = L.geoJSON(routeFeature, {
pane: 'route-shadow',
style: { color: '#000', opacity: 0.25, weight: 10, lineCap: 'round', lineJoin: 'round' }
}).addTo(map);
const line = L.geoJSON(routeFeature, {
pane: 'route-line',
style: { color: '#1976d2', opacity: 0.95, weight: 5, lineCap: 'round', lineJoin: 'round' }
}).addTo(map);
Leaflet uses panes to control the drawing order of map layers.
Each pane acts like a separate <div> inside the map container, and layers added to panes with higher zIndex values appear above those in lower ones.
In this example, we create two panes:
-
route-shadow(zIndex 399) — draws the soft black shadow beneath the line -
route-line(zIndex 400) — renders the main colored route above the shadow
By separating the route into multiple panes, we achieve a clear visual hierarchy without merging styles or affecting interactivity.
Learn more in the Leaflet documentation: Map panes.
3. Visualizing GeoJSON Polygons (Isoline API)
We’ll render Polygon/MultiPolygon features returned by the Geoapify Isoline API (drive-time areas) and style them with a soft shadow underlay, semi-transparent fills, and crisp outlines. The map also shows an origin marker and a simple legend for time ranges.
Live demo (CodePen): https://codepen.io/geoapify/pen/EaPROwQ
What happens in the example:
- Requests a FeatureCollection from the Isoline API where each feature has geometry.type = Polygon or MultiPolygon and a properties.range (seconds).
- Renders polygons via L.geoJSON(data, options) with range-based fillColor, semi-transparent, with outline.
- Creates custom panes and z-index so shadows stay beneath fills and markers sit on top.
- Adds an origin marker made with the Geoapify Marker Icon API.
- Builds a legend from properties.range values and labels them in minutes.
Here are the key code parts used to render GeoJSON Polygons/MultiPolygons from the Isoline API in Leaflet:
GeoJSON Polygons: Fetch Isoline GeoJSON
const apiKey = "YOUR_API_KEY";
const lat = 32.82950455, lon = -96.73469695515405;
const url = `https://api.geoapify.com/v1/isoline?lat=${lat}&lon=${lon}&type=time&mode=drive&range=1200,1800,2400,3000&apiKey=${apiKey}`;
const res = await fetch(url);
const isolineData = await res.json(); // FeatureCollection with Polygon/MultiPolygon
GeoJSON Polygons: Palettes & range mapping (seconds → color)
// Colorful palette (light → dark / inner → outer)
const palette = ['#fff3bf', '#b2f2bb', '#a5d8ff', '#d0bfff', '#ffc9c9', '#ffd8a8'];
const ranges = Array.from(new Set(
isolineData.features.map(f => f.properties?.range).filter(r => typeof r === 'number')
)).sort((a, b) => a - b);
const colorByRange = {};
ranges.forEach((r, i) => { colorByRange[r] = palette[Math.min(i, palette.length - 1)]; });
const toMinutes = (s) => Math.round(s / 60);
GeoJSON Polygons: Filled polygons with outlines + popups + hover highlight
const fillsLayer = L.geoJSON(isolineData, {
pane: 'isoline-fill',
style: (feature) => {
const r = feature.properties?.range;
const fill = colorByRange[r] || '#74c0fc';
return {
color: '#1e3a8a', // outline
weight: 1.5,
opacity: 0.8,
fillColor: fill, // per-range color
fillOpacity: 0.35
};
},
onEachFeature: (feature, layer) => {
const r = feature.properties?.range;
const label = r ? `${toMinutes(r)} min` : 'Isoline';
layer.bindPopup(`Reachable within ${label}`);
// subtle hover effect
layer.on('mouseover', () => layer.setStyle({ weight: 2.5, opacity: 1, fillOpacity: 0.45 }));
layer.on('mouseout', () => layer.setStyle({ weight: 1.5, opacity: 0.8, fillOpacity: 0.35 }));
}
}).addTo(map);
These snippets cover the GeoJSON-specific logic: fetching Polygon/MultiPolygon features, mapping properties.range to colors, drawing a shadow + fill combo for visual depth, and providing popups/hover for interactivity.
Summary
In this tutorial, we explored how to visualize different GeoJSON geometries — Points, LineStrings, and Polygons — using Leaflet and the Geoapify APIs.
- GeoJSON Points were loaded from the Geoapify Places API and displayed as custom markers styled via the Geoapify Marker Icon API.
- GeoJSON LineStrings were fetched from the Geoapify Routing API and drawn as routes with shadows, colorful lines, and turn-by-turn markers.
- GeoJSON Polygons (and MultiPolygons) came from the Geoapify Isoline API, representing areas reachable within certain travel-time ranges.
Each geometry type used Leaflet’s L.geoJSON() with the options parameter to control styling and interactivity — including pointToLayer, style, and onEachFeature.
Together, these techniques demonstrate how Geoapify APIs and Leaflet can be combined to create powerful, visually appealing map visualizations for a wide range of geospatial data.
FAQ
Q: What is GeoJSON and why is it useful?
A: GeoJSON is a standard format for encoding geographic data structures such as Points, LineStrings, and Polygons. It’s easy to parse, visualize, and combine with APIs like Geoapify’s for web mapping.
Q: Which Geoapify APIs provide GeoJSON data?
A:
- Places API — returns Points (POIs).
- Routing API — returns LineStrings or MultiLineStrings (routes).
- Isoline API — returns Polygons or MultiPolygons (travel-time areas).
Q: How does Leaflet handle different geometry types?
A: Leaflet’s L.geoJSON() automatically detects geometry types and renders them appropriately. You can use callbacks like pointToLayer() for Points and style() for LineStrings or Polygons to customize their appearance.
Q: Can I use the same map to show Points, Lines, and Polygons together?
A: Yes. Leaflet allows multiple GeoJSON layers to be added on the same map. You can overlay POIs, routes, and isolines for richer visualization and interactivity.
Q: Why do GeoJSON coordinates sometimes appear flipped when displayed on a Leaflet map?
A: In GeoJSON, coordinates are always defined in [longitude, latitude] order, while Leaflet expects [latitude, longitude]. When you use L.geoJSON() or pass latlng inside pointToLayer or L.marker(), Leaflet automatically converts them correctly. Just make sure you don’t manually reverse the order unless you’re working directly with raw coordinate arrays.
Q: How can I customize the look of my map?
A: You can use the Geoapify Map Tiles to apply different base styles (Bright, Dark, Satellite, etc.) and adjust colors, icons, or shadows for your data layers.
Q: What are some practical use cases for these visualizations?
A:
- Points: show places of interest, user locations, or search results.
- Lines: display routes, tracks, or transit paths.
- Polygons: visualize travel-time zones, service areas, or delivery coverage.
Q: How do I make my layers interactive?
A: Use onEachFeature() in L.geoJSON() to bind popups or highlight layers on hover. This makes maps engaging and helps users explore the data intuitively.
Top comments (1)
Great tutorial! The step-by-step examples with Geoapify APIs make Leaflet GeoJSON visualization so approachable.