3

Is it possible to use a custom icon instead of a circle and a symbol (with the cluster_count string) for Mapbox GL Js' "Superclusters?"

My goal here is to approximate something like Pie Clusters or MiniCharts that used to be possible in Leaflet. I need some way to visually represent what's inside a cluster.

1
  • This is possible by including the external Supercluster library (using 2.3.0, the same version that's in Mapbox GL). By using the Supercluster object, you can add a "layer" of custom icons that could include pie charts. A big drawback is that the custom cluster "layer" won't be affected by filters, though.
    – DMack
    Commented Nov 21, 2017 at 18:55

2 Answers 2

11

I wound up getting a satisfactory result, but it took some doing. I expanded on this very helpful Supercluster property aggregation example with custom icons.

Mapbox GL pie clusters example

The main thing is to use the external Supercluster library to do all the clustering, and not use mapbox-gl's built-in clustering at all. Trying to share the work between internal and external Supercluster just complicates things. Future versions of mapbox-gl will expose this functionality in their API, but we can just use the external Supercluster for now. I'm using [email protected] and [email protected] here.

First, create the cluster object with the initial/map/reduce methods for property aggregation:

// I did this in a Vue component so forgive all the "this" bits
this.cluster = supercluster({
    radius: this.clusterRadius,
    maxZoom: this.clusterMaxZoom, // 14 here
    initial() {
        return {
            // example: household pets
            cats: 0,
            dogs: 0,
            fish: 0,
        };
    },
    map(properties) {
        return {
            cats: properties.pet === 'cat' ? 1 : 0,
            dogs: properties.pet === 'dog' ? 1 : 0,
            fish: properties.pet === 'fish' ? 1 : 0,
        };
    },
    reduce(accumulated, properties) {
        accumulated.cats += properties.cats;
        accumulated.dogs += properties.dogs;
        accumulated.fish += properties.fish;
    },
});

// load the initial data
this.cluster.load(pointsData);

Then, to get the cluster data as a geojson feature collection, I used the featureCollection function in @turf/helpers:

/**
 * update the cluster layer data with the
 * current clusters for this zoom level
 */
updateClusterData() {
    this.clusterData = featureCollection(
        this.cluster.getClusters(this.worldBounds, this.intZoom),
    );
},

Note that worldBounds is the bounding box inwhich to calculate your clusters. I used the whole world because I don't have a lot of clusters. If you use your viewport bounding box, you'll need to recalculate clusters every time the map moves. intZoom is the current integer zoom level (Math.floor(this.mapZoom)).

On the actual map, add the clusterData geojson featureCollection as a source.

this.map.addSource('my-locations', {
    type: 'geojson',
    data: this.clusterData,
    buffer: 1, // I honestly don't know what this does
    maxzoom: this.clusterMaxZoom,
});

I also added a regular mapbox-gl layer for the unclustered points as per the regular cluster example. (filter: ['!has', 'point_count'],). The clusters themselves will be all custom icons, though.

Mapbox GL doesn't do "layers" when it comes to custom icons (not in the way Leaflet does), so just use an array to keep track of all of your icon objects. (this.customClusters = [])

Each time you change your clusterData, you will want to update and re-render the clusters:

/**
 * re-render the custom clusters layer
 */
updateCustomClusters() {
    // clear existing custom clusters "layer"
    this.customClusters.forEach(markerObj => markerObj.remove());
    this.customClusters = [];

    // create a custom icon (HTML element) for each cluster
    this.clusterData.features.forEach((c) => {
        // skip non-cluster points
        if (!c.properties.cluster) {
            return;
        }

        const coords = c.geometry.coordinates;
        const clusterId = c.properties.cluster_id;

        // create a DOM element for the marker
        const el = document.createElement('div');
        el.classList.add('cluster-marker');

        // add marker to map
        const markerObj = new mapboxgl.Marker(el)
        .setLngLat(coords)
        .addTo(this.map);


        // [...]
        // All of the cluster properties are available here in
        // c.properties, including the property aggregation 
        // (dogs: 5, cats: 7...)
        // Here is where you would render a pie chart with d3
        // or whatever you want. I mounted a Vue component onto the 
        // div.custom-marker element. 
        // Add any mouse event handlers here, etc.
        // [...]


        // keep track of the clusters in this "layer"
        // so they can be removed on zoom change
        this.customClusters.push(markerObj);
    });
},

As I mentioned above, using custom icons means we can't use mapbox-gl's filter system, so we'll have to manually filter the array and update the clusters.

The last step is to re-calculate and re-render the clusters when the map or the data changes.

Re-calculate clusters when the zoom level changes by a whole value (this.intZoom)

this.updateClusterData();
// update the source for the unclustered points layer
this.map.getSource('my-locations').setData(this.clusterData);

Re-calculate clusters when the geojson data changes (i.e. from filtering)

this.cluster.load(filteredPointsData);
this.updateClusterData();
// update the source for the unclustered points layer
this.map.getSource('my-locations').setData(this.clusterData);

Re-render clusters when the cluster layer data changes (this.clusterData)

this.updateCustomClusters();

Math.floor(this.mapZoom)

7
  • Is it still the only way to go? I am trying to achieve data-drive clusters with mapbox and haven't found a solution yet: stackoverflow.com/questions/52359737/…
    – LBes
    Commented Sep 17, 2018 at 17:16
  • Maybe not! It looks like the ability to inspect clusters landed in v0.47.0 but I haven't tried it yet. I think you still can't do the map-reduce stuff natively without the external Supercluster dependency, but you might be able to pull off the effect above without it. (Maybe?) On zoom, use the built-in getLeaves/getChildren to re-render the custom icons somehow?
    – DMack
    Commented Sep 18, 2018 at 16:34
  • in this case I really worry about performance then. Because the good thing with supercluster the way I understand it is that it probably does the clustering and map reduce once and for all. I have 2-3 million datapoints so it seems that this would just be overkill. Still looking for a proper way to do it with supercluster and a documented recent example that does this kind of things cause I'm having a hard time understanding how this works
    – LBes
    Commented Sep 18, 2018 at 22:41
  • feel free to take a look here: stackoverflow.com/questions/52359737/…
    – LBes
    Commented Sep 19, 2018 at 11:53
  • The example above is making custom elements (divs with svg elements, etc.) and I'm guessing that's still not possible with the current Mapbox GL API. If you just want data-driven styles on a regular cluster marker, you may not have to do so much funky stuff. At a glance, it still might not be possible, though. The newly-exposed API methods will definitely help in cases when you click a cluster, but styling them might be another story. We might still have to do it the hard/hackish way for now.
    – DMack
    Commented Sep 19, 2018 at 16:43
3

As of March 2019 Mapbox GL JS now has this functionality and two examples of implementing it as below. Note requirement for data source to be GeoJSON

https://blog.mapbox.com/clustering-properties-with-mapbox-and-html-markers-bb353c8662ba

https://docs.mapbox.com/mapbox-gl-js/example/cluster-html/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.