При отображении большого количества точечных объектов на карте возникает проблема перегрузки визуального слоя и снижения производительности. Кластеризация решает эту задачу путем группировки близко расположенных маркеров в один объект-кластер, который динамически изменяется при масштабировании карты.
Алгоритм работы основан на разбиении области видимости карты на сетку и объединении маркеров, попадающих в радиус кластеризации. При увеличении масштаба кластеры распадаются на более мелкие группы или отдельные точки.
Ключевой эффект:
Библиотека подключается как плагин к Leaflet.
Подключение через CDN:
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css">
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
При использовании сборщиков модулей:
npm install leaflet.markercluster
import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
Основной объект библиотеки — L.markerClusterGroup.
const map = L.map('map').setView([55.75, 37.61], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'OpenStreetMap'
}).addTo(map);
const clusters = L.markerClusterGroup();
const marker1 = L.marker([55.751, 37.618]);
const marker2 = L.marker([55.752, 37.620]);
const marker3 = L.marker([55.753, 37.622]);
clusters.addLayer(marker1);
clusters.addLayer(marker2);
clusters.addLayer(marker3);
map.addLayer(clusters);
Маркеры не добавляются напрямую на карту, а помещаются в группу кластеров.
Кластеры автоматически пересчитываются при событиях:
На низком масштабе несколько точек объединяются в один кластер с числом элементов внутри. На высоком масштабе происходит постепенное раскрытие структуры.
Механизм основан на параметре maxClusterRadius.
const clusters = L.markerClusterGroup({
maxClusterRadius: 80
});
Определяет радиус в пикселях, в пределах которого маркеры объединяются.
maxClusterRadius: 60
Меньшее значение приводит к меньшим кластерам, большее — к более плотной агрегации.
Отключает кластеризацию при достижении заданного масштаба.
disableClusteringAtZoom: 16
При клике на кластер на максимальном уровне масштабирования маркеры «расползаются» вокруг центра.
spiderfyOnMaxZoom: true
Подсветка области покрытия кластера при наведении.
showCoverageOnHover: true
Включает анимацию переходов кластеров.
animate: true
Плагин предоставляет набор событий для управления поведением.
Срабатывает при клике на кластер.
clusters.on('clusterclick', (e) => {
console.log('Cluster clicked', e.layer);
});
Используются для интерактивной подсветки.
clusters.on('clustermouseover', (e) => {
e.layer.setOpacity(0.7);
});
Фиксирует завершение анимации перерасчёта кластеров.
Ключевая особенность — возможность обработки тысяч точек без деградации интерфейса.
const clusters = L.markerClusterGroup();
fetch('/api/points')
.then(res => res.json())
.then(points => {
points.forEach(p => {
const marker = L.marker([p.lat, p.lng]);
clusters.addLayer(marker);
});
map.addLayer(clusters);
});
Внешний вид кластеров можно переопределять через
iconCreateFunction.
const clusters = L.markerClusterGroup({
iconCreateFunction: function (cluster) {
const count = cluster.getChildCount();
return L.divIcon({
html: `<div class="cluster">${count}</div>`,
className: 'custom-cluster',
iconSize: L.point(40, 40)
});
}
});
Кастомизация позволяет:
Кластер предоставляет доступ к вложенным объектам.
clusters.on('clusterclick', (e) => {
const markers = e.layer.getAllChildMarkers();
markers.forEach(m => {
console.log(m.getLatLng());
});
});
Методы:
getChildCount() — количество маркеровgetAllChildMarkers() — массив всех маркеровzoomToBounds() — масштабирование к области
кластераmap.fitBounds(clusters.getBounds());
Метод вычисляет минимальный прямоугольник, содержащий все точки.
При максимальном зуме точки, находящиеся в одной позиции, раскрываются по окружности.
Механизм помогает избежать наложения маркеров.
spiderfyShapePositions: function(count, centerPt) {
const angleStep = (Math.PI * 2) / count;
const res = [];
for (let i = 0; i < count; i++) {
res.push([
centerPt.x + Math.cos(i * angleStep) * 40,
centerPt.y + Math.sin(i * angleStep) * 40
]);
}
return res;
}
Кластеризация снижает нагрузку, но критические моменты сохраняются:
Оптимизации:
batchAddLayersМожно динамически изменять набор данных.
clusters.clearLayers();
const filtered = data.filter(p => p.type === 'city');
filtered.forEach(p => {
clusters.addLayer(L.marker([p.lat, p.lng]));
});
Кластеры часто используются с:
GeoJSON пример:
L.geoJSON(data, {
pointToLayer: (feature, latlng) => {
return L.marker(latlng);
}
}).eachLayer(layer => {
clusters.addLayer(layer);
});
.custom-cluster {
background: #2b8a3e;
color: white;
border-radius: 50%;
text-align: center;
line-height: 40px;
font-weight: bold;
}
Разные классы можно назначать в зависимости от размера кластера:
iconCreateFunction: function(cluster) {
const count = cluster.getChildCount();
let className = 'small-cluster';
if (count > 100) className = 'large-cluster';
else if (count > 20) className = 'medium-cluster';
return L.divIcon({
className
});
}
Кластеры могут работать совместно с:
Пример комбинирования:
map.addLayer(heatLayer);
map.addLayer(clusters);
Порядок слоёв влияет на визуальное восприятие данных.