mapbox-gl-jsmapbox-expressions

How to pick one of the cluster's elements


I want to use previously generated label of an element with the biggest sortKey, as a label for the cluster.

So I have this in my source, my logic is that I reduce all of the cluster's elements with a function that

The issue is that I have to specify an operator for reducer. In my code below it's 'concat'. This is not ideal, I cannot find anything that just "overwrites" a value.

I've tried number of small changes but hopelessly stuck. I'm quite new to mapbox (expressions) so any kind of a hint will be appreciated.

map.addSource('occurrencesSource', {
    type: 'geojson',
    data: occurrencesGeoJSON,
    cluster: true,
    clusterProperties: {
        highlighted: [
            'concat',
            [
                'case',
                [
                    '>',
                    ['get', 'sortKey', ['get', ['accumulated'], ['properties']]],
                    ['get', 'sortKey', ['get', 'highlighted', ['properties']]],
                ],
                ['get', ['accumulated']],
                ['get', 'highlighted'],
            ],
            null,
        ],
    });

Solution

  • TL; DR:

    This renders impossible to do with built in expression language, however there is a proper solution available.

    Full answer:

    The value of accumulated can only have a string value, hence you cannot query its properties. That being said, there are ways to accumulate item as f.e. {sortKey}***{label} and then split it on *** at each iteration, convert to numerical value, compare, and save the one with bigger sortKey the same way, but... I haven't tried it since it seems quite hacky and really looks like a potential performance bottleneck.

    What you should (probably) do instead:

    Use supercluster. It is already internally used by mapbox SDK. The goal of expression language is to make it JSON-compatible and therefore is fairly limited in the abilities it gives.

    Supercluster implementation is fairly straightforward.

    Below is an example of implementing supercluster. One thing worth mentioning is that in MapBox implementation, the supercluser runs in a serviceWorker what leads to additional performance gains. The source code of mapBox can be your reference there. For my usecase it probably won't be necessary as the overall performance is really great whatsoever.

    // Add source
    map.addSource('someSource', {
        type: 'geojson',
        data: featureCollection(yourData), // Data should be an array of GeoJSON features
    });
    
    // Create supercluster instance pointing to your source
    const superCluster = new Supercluster({
        radius: 80,
        maxZoom: 16,
        // This is the reduce function that works swell with the data from my question above
        reduce: (accumulated, props) => {
            if (accumulated.occurrence.sortKey < props.occurrence.sortKey) {
                accumulated.occurrence = props.occurrence;
            }
        },
    });
    
    // add layer displaying clusters
    map.addLayer({
        id: 'clusterLayer',
        type: 'symbol',
        source: 'someSource',
        filter: ['has', 'point_count'],
        layout: {
            'text-field': ['get', 'label', ['properties']],
        },
    });
    
    // add layer displaying unclustered markers
    map.addLayer({
        id: 'markersLayer',
        type: 'symbol',
        source: 'someSource',
        filter: ['!', ['has', 'point_count']],
        layout: {
            'text-field': ['get', 'label', ['properties']],
        },
    });
    

    To refresh cluster on each pan / move function (otherwise it could display wrong data)

    function refreshClusters () {
        const zoom = Math.floor(map.getZoom());
        const clusters = superCluster.getClusters([-180, -85, 180, 85], zoom);
        const features = featureCollection(clusters);
    
        map.getSource('someSource').setData(features);
    }
    
    // Bind to some map events
    map.on('zoomend', paintOccurrences);
    map.on('moveend', paintOccurrences);
    map.on('rotateend', paintOccurrences);
    map.on('pitchend', paintOccurrences);
    

    When your (reactive?) data changes, call this to update the cluster:

    superCluster.load(yourUpdatedData);
    refreshClusters();