Skip to content
Leaflet.js Shapes & GeoJSON — Circles, Polylines, Polygons

Leaflet.js Shapes & GeoJSON — Circles, Polylines, Polygons

DodaTech Updated Jun 6, 2026 9 min read

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
  
You should know how to create a Leaflet map and add markers from the Leaflet.js. Knowledge of JavaScript objects and arrays is helpful.

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. Return false to skip certain features (e.g., only show features with value > 10).
  • pointToLayer — Optional. Controls how Point geometries render. Default is L.marker. Override with L.circleMarker for 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

What is the difference between L.circle and L.circleMarker?
L.circle uses a real-world radius in meters and scales with zoom. L.circleMarker has a fixed pixel radius that stays constant across zoom levels, suitable for data point visualization.
Can I style individual segments of a polyline differently?
Not directly. Use separate L.polyline objects for each segment with different styles.
How do I update a polygon’s coordinates after creation?
Use polygon.setLatLngs(newLatLngs) to replace all coordinates, or polygon.spliceLatLngs() for partial updates.
Can Leaflet use Canvas rendering instead of SVG for vector layers?
Yes. Add preferCanvas: true to map options: L.map("map", { preferCanvas: true }). This improves performance with many vector layers.
What is the maximum number of vertices in a polygon?
No hard limit, but browser rendering degrades with thousands of vertices. Use smoothFactor to simplify shapes.

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: "&copy; 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

LessonDescription
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