Digital Geography

7. April 2017

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.

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 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.
  • Tom Chadwin

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

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

      • Tom Chadwin

        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.

        • 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

            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

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