How to create Cluster, marker annotation and popover using Mapbox

Using Mapbox there are many scenario when we need to show a huge data set on the map. Markers will overlap each other, and the map will look very cluttered. Data clustering is one of the best options to represents large data on the map and user experience will be high as well. 

 

Clustering data based on map cameras zoom level is a good way to provide user with a cleaner UI experience and less overwhelming location data experience.

I have created a cluster-based Map experience using the COVID-19 data set, and the same I am going to write, How I did it. Before jumping into cluster and marker, if you are interested to understand Map layer and data set preparation using geoJSON, I would suggest going through my previous blog. Also for Cluster experience, have a look at the live COVID-19 data showing on Map.


cluster COVID-19 data on map  cluster data on area marker and popover on map

The following items are going to cover in this blog - 

  • Data set creation using Python script and transform to geoJSON
  • Initializing Cluster layer using SuperCluster module in mapbox-gl
  • Load and configure data on a map based on map camera zoom level
  • Show annotation and popover on the map
  • Summary

 


Data set creation using Python script and transform to geoJSON

To create Cluster data, first we need a JSON data in geoJSON format, and my targetted JSON will look like following - 

let featureObject = {

type: 'Feature',

geometry: {

coordinates: [],

type: 'Point'

},

properties: {

ISO2: '',

ISO3: '',

Confirmed: 0,

Deaths: 0,

total_deaths: 0,

Recovered: 0,

AdminRegion1: '',

AdminRegion2: '',

title: ''

}

};

 

 

To get the data source, I have used https://github.com/microsoft/Bing-COVID-19-Data/tree/master/data to get the CSV file. Then I ran a Python script to get CSV data into a JSON file based on my needs. Here the below is the python script (CSV to JSON using HTTPS connection) with pandas and request library to get the desire data -

python to csv to json    

Once I got the country-wise-covid.json file, I turn it into a geoJSON file -

json to geoJSON   

 

Initializing Cluster layer using SuperCluster module in mapbox-gl

Now next step is to use the geoJSON to cluster and initialize it with minZomm, maxZoom, radius, and the number you wanted to on top of the cluster circle. For to get the nearest locations confirmed cases count, I have used to map and reduce function to get the accumulated numbers. 

initializing cluster    

 

Then update cluster data based on world boundaries or countries' boundaries, depends upon your needs. (In my case it is word boundary) Also, set the cluster layer and data source for the Cluster as well. 

initializing cluster    

 

 

To handle cluster click event on map, need to add click event on map cluster layer and based on zoom level, find nearest cluster and show next child. If any of parent cluster node does not have any clild node, that means its a point and show marker with pop over. 

handleClickEventOnCluster(): void {

this.bxMap.on('click', 'clusters', (e) => {

this.isClusterOn = true;

this.selection = 'death';

const cluster = this.bxMap.queryRenderedFeatures(e.point, { layers: ['clusters'] });

const clusterFeature = cluster[0];

const clusters = this.findNearestCluster(this.bxMap, clusterFeature);

const nearestCluster = clusters[0];

let nextZoomLevel = this.cluster.getClusterExpansionZoom(nearestCluster.id, this.currentZoom);

 

if (Number.isNaN(nextZoomLevel)) {

nextZoomLevel = this.bxMap.getZoom() + 2;

}

const geomertyData: any = cluster[0].geometry;

const coordinates = geomertyData.coordinates;

if (clusterFeature && clusterFeature.properties.cluster) {

this.bxMap.flyTo({

center: coordinates,

around: e.lngLat,

zoom: nextZoomLevel,

bearing: 0

});

}

});

}


getGeoJSONDataBasedOnBoundary(map: any): any {

const covidDataArray: Array<any> = [];

 

const south = map.getBounds()._sw;

const north = map.getBounds()._ne;

const bounds = [south.lng, south.lat, north.lng, north.lat];

const fetchers = (this.cluster) ? this.cluster.getClusters(bounds, this.currentZoom) : [];

 

fetchers.forEach((eachObject: any) => {

if (eachObject && eachObject.id) {

const children: Array<any> = this.cluster.getChildren(eachObject.id) as Array<any>;

 

// tslint:disable-next-line:prefer-for-of

for (let i = 0; i < children.length; i++) {

const child = children[i];

if (child && child.id) {

const leaves: Array<any> = this.cluster.getLeaves(child.id, Infinity, 0) as Array<any>;

 

// tslint:disable-next-line:prefer-for-of

for (let j = 0; j < leaves.length; j++) {

covidDataArray.push(leaves[j]);

}

} else {

covidDataArray.push(child);

}

}

} else {

covidDataArray.push(eachObject);

}

});

}


findNearestCluster(map, marker): any {

const clusterSelected = marker;

 

const south = map.getBounds()._sw;

const north = map.getBounds()._ne;

const bounds = [south.lng, south.lat, north.lng, north.lat];

let fetchersArray;

try {

fetchersArray = this.cluster.getClusters(bounds, this.currentZoom);

} catch (error) {

if (error) {

const zoomValue = this.bxMap.getZoom() + 0.5;

this.currentZoom = zoomValue;

fetchersArray = this.cluster.getClusters(bounds, this.currentZoom);

}

}

 

const currentClusters = fetchersArray;

 

const compare = {

lng: clusterSelected.geometry.coordinates[0],

lat: clusterSelected.geometry.coordinates[1]

};

 

const minClusters = currentClusters.map(cluster => {

const lng = cluster.geometry.coordinates[0];

const lat = cluster.geometry.coordinates[1];

 

return {

id: cluster.properties.cluster_id,

geometry: cluster.geometry,

value: Math.pow(compare.lng - lng, 2) * Math.pow(compare.lat - lat, 2)

};

});


return minClusters.sort((a, b) => {

return a.value - b.value;

});

}

 


Show annotation and popover on the map

To show marker on the map, and popover on to of the marker, you can use the following code - 

map marker and popover   

 

And that's it. You can execute the application if your data set is correctly configured, cluster data should have appeared without any error. 

Happy Coding!

- Lazy Panda Tech