Working with Clusters in Leaflet: Increasing Useability

In one of our latest projects we faced a sad truth: geocoding results often sucks and points are not scattered enough but concentrate on distinct locations and clusters will be full of markers. This will lead to heavy clustering if you work with such data in leaflet using the markercluster plugin. In the end it was always hard to find the right point of your interest if you’re facing 20 spiderfied points on one location.
Clusters and multiple markers
too much markers!

So we asked ourself: how can we increase the useability of clusters as we can’t change the location data itself? We came up with a quite nice approach: overview lists.

Too much Markers in your Clusters

The starting point is the situation: too many features share exactly the same coordinate. By clicking on a cluster the cluster spiderfies and presents all markers inside the clusters. But at the very moment we are still unsure which is the marker of interest. So you need to open every marker and hope you’re lucky enough to find the JSTN you’re interested in:
<!DOCTYPE html>
<html lang="en">
    <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta name="description" content="">
    <meta name="keywords" content="">
    <meta name="author" content="">
    <meta charset="utf-8">
    <script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script>
    <script src="data.js"> // data goes here</script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/leaflet.markercluster.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/MarkerCluster.Default.css" />
    <link rel="stylesheet" href="style.css" />
</head>
<body>
    <div id="map"></div>
    <script>
    var map = L.map('map', {zoomControl:true, maxZoom:18, minZoom:7}).fitBounds([[47.7931936169,7.5593073],[49.7572606169,10.4588028203]]);
    var basemap = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="http://cartodb.com/attributions">CartoDB</a>', subdomains: 'abcd'});
    basemap.addTo(map);
    function popUp(feature, layer) {
        var text = '<h3>' + feature.properties['name'] + '</h3><h4>' + feature.properties['location'] + ', ' + feature.properties['date'] + '</h4><img src="' + feature.properties.image_link + '"width="200px" /><br>Visitors: ' + String(feature.properties.visitors);
        layer.bindPopup(text);
    }
    var points = new L.geoJson(data,{
        onEachFeature: popUp
    });
    var markers = L.markerClusterGroup();
    markers.addLayer(points).addTo(map);
    </script>
</body>

So we will work with this small basis.

The Solution

The solution is provided by the markercluster plugin itself as it offers a way to respond to “clusterclick” events. So we will listen to this event and if the event is called on max zoom level, we will open a popup with content that comes from the markers inside the cluster:
markers.on('clusterclick', function(a){
        if(a.layer._zoom == 18){
            popUpText = '<ul>';
            //there are many markers inside "a". to be exact: a.layer._childCount much ;-)
            //let's work with the data:
            for (feat in a.layer._markers){
                popUpText+= '<li>' + a.layer._markers[feat].feature.properties['name'] + ', ' + a.layer._markers[feat].feature.properties['date'] + '</li>';
            }
            popUpText += '</ul>';
            //as we have the content, we should add the popup to the map add the coordinate that is inherent in the cluster:
            var popup = L.popup().setLatLng([a.layer._cLatLng.lat, a.layer._cLatLng.lng]).setContent(popUpText).openOn(map); 
        }
    })
This will now show us a nice list of some attributes inside the cluster:
list from attributes of markers in clusters
list of attributes from underlying markers

As we have the list now we will add some more magic so we can jump to the marker of interest and open the needed popup by simply clicking on the list entry:
function openPopUp(id, clusterId){
        map.closePopup(); //which will close all popups
        map.eachLayer(function(layer){     //iterate over map layer
            if (layer._leaflet_id == clusterId){         // if layer is markerCluster
                layer.spiderfy(); //spiederfies our cluster
            }
        });
        map.eachLayer(function(layer){     //iterate over map rather than clusters
            if (layer._leaflet_id == id){         // if layer is marker
                layer.openPopup();
            }
        });
    }
To call this function we need to extract the marker ID as well as the cluster ID from the cluster. We will do so by changing the line item:
popUpText+= '<li><u onclick=openPopUp(' + a.layer._markers[feat]._leaflet_id + ','+ a.layer._leaflet_id +')>' + a.layer._markers[feat].feature.properties['name'] + ', ' + a.layer._markers[feat].feature.properties['date'] + '</u></li>';
In the end it makes it easier to access the markers inside a cluster:

You can download the whole map with the data.
3.3 4 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

8 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Tom Chadwin
Tom Chadwin
6 years ago

I like this solution. I wish I could work out a good way to do something similar for overlapping polygons, rather than clustered points.

Riccardo
6 years ago
Reply to  Tom Chadwin

you need something like most likely for qgis2web? if so, I would propese to do this in the backend (python)…

Tom Chadwin
Tom Chadwin
6 years ago
Reply to  Riccardo

I don’t think you can do it in Python – it’s responding to a client-side click in Javascript after the map has been generated. No client-side Python.

Riccardo
6 years ago
Reply to  Tom Chadwin

I know. I am proposing another way: create “ghost” polygons when you’re dealing with the polygons during export of the webmap. Ghost polygons are the result of intersecting polygons in one layer. this ghost polygon layer is added to the map but not to the layer overview and is only clickable if the parenting layer is visible. it contains the fetaures of all intersecting polygons.

Tom Chadwin
Tom Chadwin
6 years ago
Reply to  Riccardo

Nice. That could work. Tricky if different polygons in the source layer have different styles. It’s an interesting way to achieve it, though. I thought one would have to implement point-in-polygon detection, which I imagine has poor performance.

Pives
Pives
6 years ago

Thanks for this example. I’m trying to do the same with overlapping polygons. It would be a nice improvement.

blogo
blogo
3 years ago

Hello, I’d like to know how do you make to show only the 3 markers info instead of All your markers info (4) ?? I have a map with culsters, and for exemple when I have a cluster with 2 markers, it shows me all my geoJson data (all markers) infos, instead of just the two that are in my clyster.. I tried with this code.. clusters.on(‘clusterclick’, function(a){
if(a.layer._zoom == 18){
var markers = a.layer.getAllChildMarkers();

for (var i = 0; i < markers.length; i++) {
$('#layer_infos .fill').append(html);
}
}
})

Sean
Sean
3 months ago

Amazing! do you know how to adjust either the in-between cluster lines CSS, or delay the transition after mouse click so that the black lines in the cluster disappear the same time as the icons. Big Thanks in advance!