Leaflet.js Shapes & GeoJSON — Circles, Polylines, Polygons
Vector layers let you draw shapes — circles, lines, polygons — on a map. Leaflet supports GeoJSON for importing geospatial data and offers rich styling options for all vector types.
What You’ll Learn
By the end of this tutorial you will draw circles, lines, polygons, and rectangles on a map, understand the difference between circles and circleMarkers, load and style GeoJSON features, and build a color-coded choropleth map.
Why Shapes & GeoJSON Matter
Shapes turn a map of dots into a map of territories, routes, and zones. Think delivery service areas, hiking trails, or property boundaries. Doda Browser uses polygon overlays for geofenced content zones, and Durga Antivirus Pro draws heat-radius circles around detected threat origins for visual threat analysis.
flowchart LR
A["Leaflet.js Basics"] --> B["Markers & Icons"]
B --> C["Shapes & GeoJSON 📐"]
C --> D["Layers & Controls"]
D --> E["Events"]
E --> F["Advanced & Plugins"]
A:::past
B:::past
C:::current
D:::future
E:::future
F:::future
classDef past fill:#4CAF50,stroke:#333,color:#fff
classDef current fill:#e91e63,stroke:#333,color:#fff
classDef future fill:#f5f5f5,stroke:#999,color:#666
Circles
var circle = L.circle([51.508, -0.11], {
color: "red", // Stroke (border) color
fillColor: "#f03", // Fill color
fillOpacity: 0.5, // Transparency (0 = invisible, 1 = solid)
radius: 500 // Radius in METERS (not pixels)
}).addTo(map);
circle.bindPopup("500m radius");Circle vs CircleMarker — What’s the Difference?
This confuses many beginners, so let’s be clear:
L.circle()— radius is in meters. The circle grows/shrinks as you zoom. Use it for real-world distances (e.g., “show all restaurants within 1km”).L.circleMarker()— radius is in pixels. The circle stays the same size on screen regardless of zoom. Use it for data points (e.g., “show population density”).
// Fixed pixel size — stays same at any zoom
L.circleMarker([51.5, -0.09], {
radius: 10, // Pixels (not meters!)
color: "red",
fillColor: "#f03",
fillOpacity: 0.5
}).addTo(map);Polylines
Polylines draw connected line segments. Think of routes, paths, or boundaries:
var latlngs = [
[51.5, -0.09],
[51.51, -0.08],
[51.52, -0.07],
[51.53, -0.06]
];
var polyline = L.polyline(latlngs, {
color: "red",
weight: 3, // Line thickness in pixels
opacity: 0.7,
dashArray: "10, 10" // Dashed line: 10px dash, 10px gap
}).addTo(map);When to Use Polylines
Use polylines for paths (driving routes, hiking trails, flight paths). Each point is a GPS coordinate along the route. The line connects them in order.
Polygons
Polygons are like polylines but the path closes and fills with color:
var latlngs = [
[51.515, -0.09],
[51.52, -0.08],
[51.51, -0.07],
[51.515, -0.09] // Back to first point — closes the shape
];
var polygon = L.polygon(latlngs, {
color: "green",
fillColor: "#32CD32",
fillOpacity: 0.3,
weight: 2
}).addTo(map);Important: The first and last point should be the same to close the polygon properly. Leaflet may render an unclosed polygon as a polyline.
Polygons with Holes
var latlngs = [
[[51.52, -0.10], [51.53, -0.08], [51.51, -0.06]], // Outer boundary
[[51.515, -0.085], [51.52, -0.08], [51.515, -0.075]] // Hole (inner ring)
];
L.polygon(latlngs, { color: "red" }).addTo(map);The inner ring creates a transparent “hole” in the polygon — useful for donut-shaped areas like a city boundary with a park excluded.
Rectangles
A convenience shortcut for rectangular bounds:
var bounds = [
[51.49, -0.08], // Southwest corner
[51.5, -0.06] // Northeast corner
];
L.rectangle(bounds, {
color: "#ff7800",
weight: 2,
fillColor: "#ff7800",
fillOpacity: 0.2
}).addTo(map);Common Vector Layer Styling Options
These options apply to circles, polylines, polygons, and rectangles:
{
color: "#3388ff", // Stroke color
weight: 3, // Stroke width in pixels
opacity: 1.0, // Stroke opacity
lineCap: "round", // Ends of lines: "butt", "round", "square"
lineJoin: "round", // Corners: "round", "bevel", "miter"
dashArray: null, // "10, 10" for dashed
fill: true, // Whether to fill the shape
fillColor: "#3388ff", // Fill color
fillOpacity: 0.2, // Fill transparency
smoothFactor: 1.0 // Higher = more simplification for performance
}GeoJSON — The Standard for Geospatial Data
GeoJSON is a JSON format for encoding geographic data. Think of it as a universal language for maps — every GIS tool, mapping library, and geospatial API speaks it.
GeoJSON Coordinate Order Trap
GeoJSON uses [longitude, latitude] — the opposite of Leaflet!
var geojsonFeature = {
type: "Feature",
properties: { name: "Point A" },
geometry: {
type: "Point",
coordinates: [-0.09, 51.5] // GeoJSON: [lng, lat] — NOT [lat, lng]!
}
};
L.geoJSON(geojsonFeature).addTo(map);Leaflet automatically converts GeoJSON’s [lng, lat] to [lat, lng] internally, so you don’t need to manually swap. But if you’re reading a GeoJSON file, the coordinates are always [lng, lat].
Loading GeoJSON from a File
fetch("data.geojson")
.then(function(res) { return res.json(); })
.then(function(data) {
L.geoJSON(data, {
style: function(feature) {
return {
fillColor: feature.properties.value > 50 ? "red" : "green",
weight: 2,
opacity: 1,
color: "white",
fillOpacity: 0.7
};
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.name) {
layer.bindPopup(feature.properties.name);
}
}
}).addTo(map);
});GeoJSON Options Explained Line by Line
style— A function that returns styling based on feature properties. Here, values > 50 get red fill, others green.onEachFeature— Runs once per feature. Use it to attach popups, event handlers, or custom behavior.filter— Optional. Returnfalseto skip certain features (e.g., only show features with value > 10).pointToLayer— Optional. Controls how Point geometries render. Default isL.marker. Override withL.circleMarkerfor data points.
Choropleth Map (Color-Coded by Data Value)
A choropleth map colors regions based on a data value — like population density by neighborhood:
function getColor(value) {
return value > 100 ? "#800026" :
value > 50 ? "#BD0026" :
value > 20 ? "#E31A1C" :
value > 10 ? "#FC4E2A" :
value > 5 ? "#FD8D3C" :
"#FEB24C";
}
L.geoJSON(data, {
style: function(feature) {
return {
fillColor: getColor(feature.properties.density),
weight: 2,
opacity: 1,
color: "white",
dashArray: "3",
fillOpacity: 0.7
};
},
onEachFeature: function(feature, layer) {
layer.bindPopup(
"<b>" + feature.properties.name + "</b><br>" +
feature.properties.density + " people/km²"
);
}
}).addTo(map);Each polygon gets a color based on its density value. The legend (which you’d add as a separate control) explains the color scale.
Common Mistakes
1. GeoJSON coordinate order confusion
GeoJSON uses [lng, lat]. Leaflet uses [lat, lng]. If you write GeoJSON coordinates in Leaflet order, your shapes will appear in the wrong location or wrap across the globe.
2. Circle radius in wrong units
L.circle() takes radius in meters. L.circleMarker() takes radius in pixels. Using meters for circleMarker or pixels for circle produces unexpected sizes.
3. Polygon not properly closed
A polygon must have its first and last point equal. If not, Leaflet may render it as a polyline (unfilled).
4. Coordinates outside valid range
Latitude must be between -90 and 90. Longitude between -180 and 180. Values outside these ranges cause rendering errors.
5. Forgetting to add the layer to the map
Creating a shape with L.polyline() or L.geoJSON() does nothing visible until you call .addTo(map). This is the same pattern as markers and tile layers.
Practice Questions
Q1: What is the difference between L.circle() and L.circleMarker()?
A: L.circle() radius is in real-world meters and scales with zoom. L.circleMarker() radius is in pixels and stays the same size on screen regardless of zoom.
Q2: Why does GeoJSON use [lng, lat] instead of [lat, lng]?
A: GeoJSON follows the mathematical convention of [x, y] (longitude = x-axis, latitude = y-axis). Leaflet uses [lat, lng] for convenience.
Q3: What does smoothFactor do in vector styling?
A: It simplifies the geometry for better performance. Higher values = more simplification = less detail but faster rendering.
Q4: How do you create a dashed polyline?
A: Set dashArray: "10, 10" — the two numbers are dash length and gap length in pixels.
Q5: What does onEachFeature do in L.geoJSON()?
A: It runs a function on every feature, letting you attach popups, event handlers, or custom styling per feature.
Challenge: Find a GeoJSON file for your city’s neighborhoods (many are available on public data portals). Load it into a Leaflet map, color each neighborhood by a property (like area or population), and add a legend control.
FAQ
Try It Yourself
Here’s a complete choropleth map of simulated NYC neighborhoods with mouseover highlighting:
<!DOCTYPE html>
<html>
<head>
<title>Neighborhood Explorer</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
body { font-family: sans-serif; padding: 20px; }
#map { height: 500px; border-radius: 8px; }
.info { padding: 6px 8px; font: 14px/16px sans-serif;
background: white; background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 5px; }
.info h4 { margin: 0 0 5px; color: #777; }
.legend { text-align: left; line-height: 18px; color: #555; }
.legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }
</style>
</head>
<body>
<h2>Population Density by Neighborhood</h2>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var map = L.map("map").setView([40.73, -73.94], 11);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap"
}).addTo(map);
var neighborhoods = {
type: "FeatureCollection",
features: [
{ type: "Feature", properties: { name: "Manhattan", density: 28000 },
geometry: { type: "Polygon", coordinates: [[[-73.97,40.80],[-73.95,40.88],
[-73.93,40.88],[-73.94,40.80],[-73.97,40.80]]] }},
{ type: "Feature", properties: { name: "Brooklyn", density: 15000 },
geometry: { type: "Polygon", coordinates: [[[-73.94,40.65],[-73.94,40.70],
[-73.98,40.70],[-73.99,40.65],[-73.94,40.65]]] }}
]
};
function getColor(d) {
return d > 25000 ? "#800026" : d > 15000 ? "#BD0026" :
d > 10000 ? "#E31A1C" : d > 5000 ? "#FC4E2A" : "#FEB24C";
}
function style(feature) {
return {
fillColor: getColor(feature.properties.density),
weight: 2, opacity: 1, color: "white",
dashArray: "3", fillOpacity: 0.7
};
}
L.geoJSON(neighborhoods, {
style: style,
onEachFeature: function(feature, layer) {
layer.bindPopup("<b>" + feature.properties.name + "</b><br>" +
feature.properties.density.toLocaleString() + " people/km²");
layer.on("mouseover", function() { this.setStyle({ weight: 4 }); });
layer.on("mouseout", function() { this.setStyle(style(feature)); });
}
}).addTo(map);
map.fitBounds(L.geoJSON(neighborhoods).getBounds());
</script>
</body>
</html>What’s Next
| Lesson | Description |
|---|---|
| Layers & Controls → | Layer groups, image/video overlays, and custom controls |
| Events → | Click handlers, hover effects, and event propagation |
| Advanced & Plugins → | Heatmaps, routing, and performance optimization |
Related Topics: JSON data format | JavaScript functions and arrays | Canvas rendering for performance
What’s Next
Congratulations on completing this Leafletjs Shapes tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro