For AI agents: a documentation index is available at /llms.txt. Markdown versions of all pages
are available by appending .md to any URL.
Display HTML clusters with custom properties
This advanced example showcases the implementation of MapTiler SDK JS clustering by using HTML markers and custom property expressions. To substitute a MapTiler layer with HTML or SVG for clusters, it is necessary to manually synchronize the clustered source with a collection of marker objects that consistently update as the map view changes.
NPM module setup
npm install --save @maptiler/sdk
import { Map, MapStyle, config } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const map = new Map({
container: 'map',
zoom: 0.3,
center: [0, 20],
style: MapStyle.DATAVIZ.DARK
});
// filters for classifying earthquakes into five categories based on magnitude
const 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 categories
const colors = ['#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c'];
map.on('load', function () {
// add a clustered GeoJSON source for a sample set of earthquakes
map.addSource('earthquakes', {
'type': 'geojson',
'data':
'https://docs.maptiler.com/sdk-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= {};
const 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 already
for (let i = 0; i < features.length; i++) {
const coords = features[i].geometry.coordinates;
const props = features[i].properties;
if (!props.cluster) continue;
const id = props.cluster_id;
let marker = markers[id];
if (!marker) {
const el = createDonutChart(props);
marker = markers[id] = new maptilersdk.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 visible
for (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/moveend
map.on('data', function (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 properties
function 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 (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(' ');
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>Display HTML clusters with custom properties</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {margin: 0; padding: 0;}
#map {position: absolute; top: 0; bottom: 0; width: 100%;}