Leaflet.markercluster для кластеризации

Принцип работы кластеризации маркеров

При отображении большого количества точечных объектов на карте возникает проблема перегрузки визуального слоя и снижения производительности. Кластеризация решает эту задачу путем группировки близко расположенных маркеров в один объект-кластер, который динамически изменяется при масштабировании карты.

Алгоритм работы основан на разбиении области видимости карты на сетку и объединении маркеров, попадающих в радиус кластеризации. При увеличении масштаба кластеры распадаются на более мелкие группы или отдельные точки.

Ключевой эффект:

  • снижение количества DOM-элементов
  • ускорение отрисовки
  • повышение читаемости данных на низких масштабах

Установка и подключение Leaflet.markercluster

Библиотека подключается как плагин к 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);

Маркеры не добавляются напрямую на карту, а помещаются в группу кластеров.


Поведение кластеров при масштабировании

Кластеры автоматически пересчитываются при событиях:

  • zoomend
  • moveend
  • добавление/удаление маркеров

На низком масштабе несколько точек объединяются в один кластер с числом элементов внутри. На высоком масштабе происходит постепенное раскрытие структуры.

Механизм основан на параметре maxClusterRadius.

const clusters = L.markerClusterGroup({
  maxClusterRadius: 80
});

Основные параметры конфигурации

maxClusterRadius

Определяет радиус в пикселях, в пределах которого маркеры объединяются.

maxClusterRadius: 60

Меньшее значение приводит к меньшим кластерам, большее — к более плотной агрегации.


disableClusteringAtZoom

Отключает кластеризацию при достижении заданного масштаба.

disableClusteringAtZoom: 16

spiderfyOnMaxZoom

При клике на кластер на максимальном уровне масштабирования маркеры «расползаются» вокруг центра.

spiderfyOnMaxZoom: true

showCoverageOnHover

Подсветка области покрытия кластера при наведении.

showCoverageOnHover: true

animate

Включает анимацию переходов кластеров.

animate: true

События кластеров

Плагин предоставляет набор событий для управления поведением.

clusterclick

Срабатывает при клике на кластер.

clusters.on('clusterclick', (e) => {
  console.log('Cluster clicked', e.layer);
});

clustermouseover / clustermouseout

Используются для интерактивной подсветки.

clusters.on('clustermouseover', (e) => {
  e.layer.setOpacity(0.7);
});

animationend

Фиксирует завершение анимации перерасчёта кластеров.


Добавление большого количества данных

Ключевая особенность — возможность обработки тысяч точек без деградации интерфейса.

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());

Метод вычисляет минимальный прямоугольник, содержащий все точки.


Spiderfy — раскрытие перекрывающихся точек

При максимальном зуме точки, находящиеся в одной позиции, раскрываются по окружности.

Механизм помогает избежать наложения маркеров.

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;
}

Производительность при больших данных

Кластеризация снижает нагрузку, но критические моменты сохраняются:

  • частое добавление маркеров вызывает перерасчёт кластеров
  • сложные кастомные иконки увеличивают время рендера
  • DOM-манипуляции остаются узким местом

Оптимизации:

  • пакетная загрузка через batchAddLayers
  • отключение анимации при больших наборах
  • использование Web Worker для подготовки данных

Фильтрация маркеров в кластере

Можно динамически изменять набор данных.

clusters.clearLayers();

const filtered = data.filter(p => p.type === 'city');

filtered.forEach(p => {
  clusters.addLayer(L.marker([p.lat, p.lng]));
});

Интеграция с внешними источниками данных

Кластеры часто используются с:

  • GeoJSON API
  • REST-сервисами
  • потоковыми данными (WebSocket)

GeoJSON пример:

L.geoJSON(data, {
  pointToLayer: (feature, latlng) => {
    return L.marker(latlng);
  }
}).eachLayer(layer => {
  clusters.addLayer(layer);
});

Управление стилями кластеров через CSS

.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);

Порядок слоёв влияет на визуальное восприятие данных.