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.