Display HTML clusters with custom properties
This advanced example uses NMaps GL JS clustering with HTML markers
and custom property expressions number-format
, get
, !=
, >
, >=
, all
, and case
.
To use HTML or SVG for clusters in place of a NMaps GL layer, you have to manually synchronize 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" /><script src="https://cdn.ndrive.com/nmapsgl/v.1.6.8/nmaps-gl.js"></script><link href="https://cdn.ndrive.com/nmapsgl/v.1.6.8/nmaps-gl.css" rel="stylesheet" /><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 nmapsgl.accessToken = '<your access token here>'; const map = new nmapsgl.Map({container: 'map',zoom: 0.3,center: [0, 20],style: 'Streets'}); map.addControl(new nmapsgl.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://nmaps-gl.ndrive.com/nmaps-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 Regular'],'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 (let i = 0; i < features.length; i++) {const coords = features[i].geometry.coordinates;const props = features[i].properties;if (!props.cluster) continue;let id = props.cluster_id; let marker = markers[id];if (!marker) {const el = createDonutChart(props);marker = markers[id] = new nmapsgl.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 (let id in markersOnScreen) {if (!newMarkers[id]) markersOnScreen[id].remove();}markersOnScreen = newMarkers;} // after the GeoJSON data is loaded, update markers on the screen and do so on every map move/moveendmap.on('data', (e) => {if (e.sourceId !== 'earthquakes' || !e.isSourceLoaded) return; map.on('move', updateMarkers);map.on('moveend', updateMarkers);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 (let i = 0; i < counts.length; i++) {offsets.push(total);total += counts[i];}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; return ['<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 + '" />'].join(' ');}</script> </body></html>