Display HTML clusters with custom properties
This advanced example uses Mapbox GL JS clustering and clusterProperties
with HTML markers and the custom property expressions number-format
, get
, !=
, >
, >=
, all
, and case
.
You can use HTML or SVG for clusters in place of a Mapbox GL layer by manually synchronizing the clustered source with a pool of marker objects that updates continuously while the map view changes.
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Display HTML clusters with custom properties</title><meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"><link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet"><script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js"></script><style>body { margin: 0; padding: 0; }#map { position: absolute; top: 0; bottom: 0; width: 100%; }</style></head><body><div id="map"></div> <script> // TO MAKE THE MAP APPEAR YOU MUST // ADD YOUR ACCESS TOKEN FROM // https://account.mapbox.com mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';const map = new mapboxgl.Map({container: 'map',zoom: 0.3,center: [0, 20],// Choose from Mapbox's core styles, or make your own style with Mapbox Studiostyle: 'mapbox://styles/mapbox/light-v11',projection: 'mercator'}); map.addControl(new mapboxgl.NavigationControl()); // filters for classifying earthquakes into five categories based on magnitudeconst mag1 = ['<', ['get', 'mag'], 2];const mag2 = ['all', ['>=', ['get', 'mag'], 2], ['<', ['get', 'mag'], 3]];const mag3 = ['all', ['>=', ['get', 'mag'], 3], ['<', ['get', 'mag'], 4]];const mag4 = ['all', ['>=', ['get', 'mag'], 4], ['<', ['get', 'mag'], 5]];const mag5 = ['>=', ['get', 'mag'], 5]; // colors to use for the categoriesconst colors = ['#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c']; map.on('load', () => {// add a clustered GeoJSON source for a sample set of earthquakesmap.addSource('earthquakes', {'type': 'geojson','data': 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson','cluster': true,'clusterRadius': 80,'clusterProperties': {// keep separate counts for each magnitude category in a cluster'mag1': ['+', ['case', mag1, 1, 0]],'mag2': ['+', ['case', mag2, 1, 0]],'mag3': ['+', ['case', mag3, 1, 0]],'mag4': ['+', ['case', mag4, 1, 0]],'mag5': ['+', ['case', mag5, 1, 0]]}});// circle and symbol layers for rendering individual earthquakes (unclustered points)map.addLayer({'id': 'earthquake_circle','type': 'circle','source': 'earthquakes','filter': ['!=', 'cluster', true],'paint': {'circle-color': ['case',mag1,colors[0],mag2,colors[1],mag3,colors[2],mag4,colors[3],colors[4]],'circle-opacity': 0.6,'circle-radius': 12}});map.addLayer({'id': 'earthquake_label','type': 'symbol','source': 'earthquakes','filter': ['!=', 'cluster', true],'layout': {'text-field': ['number-format',['get', 'mag'],{ 'min-fraction-digits': 1, 'max-fraction-digits': 1 }],'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],'text-size': 10},'paint': {'text-color': ['case',['<', ['get', 'mag'], 3],'black','white']}}); // objects for caching and keeping track of HTML marker objects (for performance)const markers = {};let markersOnScreen = {}; function updateMarkers() {const newMarkers = {};const features = map.querySourceFeatures('earthquakes'); // for every cluster on the screen, create an HTML marker for it (if we didn't yet),// and add it to the map if it's not there alreadyfor (const feature of features) {const coords = feature.geometry.coordinates;const props = feature.properties;if (!props.cluster) continue;const id = props.cluster_id; let marker = markers[id];if (!marker) {const el = createDonutChart(props);marker = markers[id] = new mapboxgl.Marker({element: el}).setLngLat(coords);}newMarkers[id] = marker; if (!markersOnScreen[id]) marker.addTo(map);}// for every marker we've added previously, remove those that are no longer visiblefor (const id in markersOnScreen) {if (!newMarkers[id]) markersOnScreen[id].remove();}markersOnScreen = newMarkers;} // after the GeoJSON data is loaded, update markers on the screen on every framemap.on('render', () => {if (!map.isSourceLoaded('earthquakes')) return;updateMarkers();});}); // code for creating an SVG donut chart from feature propertiesfunction createDonutChart(props) {const offsets = [];const counts = [props.mag1,props.mag2,props.mag3,props.mag4,props.mag5];let total = 0;for (const count of counts) {offsets.push(total);total += count;}const fontSize =total >= 1000 ? 22 : total >= 100 ? 20 : total >= 10 ? 18 : 16;const r =total >= 1000 ? 50 : total >= 100 ? 32 : total >= 10 ? 24 : 18;const r0 = Math.round(r * 0.6);const w = r * 2; let html = `<div><svg width="${w}" height="${w}" viewbox="0 0 ${w} ${w}" text-anchor="middle" style="font: ${fontSize}px sans-serif; display: block">`; for (let i = 0; i < counts.length; i++) {html += donutSegment(offsets[i] / total,(offsets[i] + counts[i]) / total,r,r0,colors[i]);}html += `<circle cx="${r}" cy="${r}" r="${r0}" fill="white" /><text dominant-baseline="central" transform="translate(${r}, ${r})">${total.toLocaleString()}</text></svg></div>`; const el = document.createElement('div');el.innerHTML = html;return el.firstChild;} function donutSegment(start, end, r, r0, color) {if (end - start === 1) end -= 0.00001;const a0 = 2 * Math.PI * (start - 0.25);const a1 = 2 * Math.PI * (end - 0.25);const x0 = Math.cos(a0),y0 = Math.sin(a0);const x1 = Math.cos(a1),y1 = Math.sin(a1);const largeArc = end - start > 0.5 ? 1 : 0; // draw an SVG pathreturn `<path d="M ${r + r0 * x0} ${r + r0 * y0} L ${r + r * x0} ${r + r * y0} A ${r} ${r} 0 ${largeArc} 1 ${r + r * x1} ${r + r * y1} L ${r + r0 * x1} ${r + r0 * y1} A ${r0} ${r0} 0 ${largeArc} 0 ${r + r0 * x0} ${r + r0 * y0}" fill="${color}" />`;}</script> </body></html>