Кластеризация

Назначение кластеризации и проблема производительности

При отображении большого количества точек на интерактивной карте возникает сразу несколько технических ограничений. Основная проблема связана с производительностью браузера: каждый маркер представляет собой DOM-элемент или SVG/Canvas-объект, и при их числе в несколько тысяч отрисовка, пересчёт позиций и обработка событий начинают существенно замедлять работу карты.

Дополнительный фактор — визуальная перегрузка. Даже если производительность остаётся приемлемой, плотное размещение маркеров делает карту нечитаемой: элементы перекрываются, теряется структура данных, усложняется восприятие пространственных закономерностей.

Кластеризация решает обе задачи одновременно:

  • уменьшает количество отображаемых объектов на карте;
  • агрегирует близкие точки в группы;
  • динамически изменяет представление в зависимости от масштаба.

В Leaflet стандартным решением является плагин Leaflet.markercluster, который реализует пространственное объединение маркеров с автоматическим перерасчётом при изменении масштаба и области просмотра.


Базовая архитектура кластеризации

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

При изменении масштаба происходит пересчёт:

  1. каждая точка преобразуется в пиксельные координаты;
  2. точки объединяются в группы по расстоянию;
  3. группы могут объединяться рекурсивно, образуя кластеры разных уровней;
  4. формируется новый слой маркеров.

В основе лежит принцип:

  • на низком масштабе — минимальное количество крупных кластеров;
  • на высоком масштабе — раскрытие кластеров вплоть до отдельных точек.

Подключение Leaflet.markercluster

Плагин подключается отдельно от ядра Leaflet.

<link rel="stylesheet" href="MarkerCluster.css" />
<link rel="stylesheet" href="MarkerCluster.Default.css" />

<script src="leaflet.js"></script>
<script src="leaflet.markercluster.js"></script>

Создание кластерной группы:

const markers = L.markerClusterGroup();

Добавление маркеров:

markers.addLayer(L.marker([51.5, -0.09]));
markers.addLayer(L.marker([51.51, -0.1]));
markers.addLayer(L.marker([51.49, -0.08]));

map.addLayer(markers);

Алгоритм формирования кластеров

Внутренний алгоритм основан на последовательной агрегации точек:

  • задаётся радиус кластеризации (в пикселях);
  • строится индекс пространственного поиска;
  • точки проверяются на принадлежность к существующим кластерам;
  • при совпадении — добавляются в кластер;
  • при отсутствии — создаётся новый кластер.

Ключевой параметр — maxClusterRadius, определяющий плотность группировки:

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

Меньшие значения дают более детализированную карту, большие — более агрессивную агрегацию.


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

Каждое изменение zoom вызывает полную перестройку кластеров.

Процесс включает:

  • пересчёт пиксельных координат;
  • очистку текущего слоя;
  • повторную группировку;
  • перерисовку маркеров.

Leaflet.markercluster оптимизирует этот процесс за счёт:

  • кэширования геометрии;
  • использования quad-tree индекса;
  • минимизации DOM-операций.

Визуализация кластеров

Кластеры отображаются как специальные иконки, содержащие количество точек:

  • одиночные маркеры — стандартные иконки;
  • группы — круговые индикаторы с числом внутри.

Пример кастомизации:

const markers = L.markerClusterGroup({
  iconCreateFunction: function (cluster) {
    const count = cluster.getChildCount();

    return L.divIcon({
      html: `<div class="cluster-icon">${count}</div>`,
      className: 'custom-cluster',
      iconSize: L.point(40, 40)
    });
  }
});

CSS:

.custom-cluster {
  background: #2b8cbe;
  border-radius: 50%;
  color: white;
  text-align: center;
  line-height: 40px;
  font-weight: bold;
}

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

При максимальном приближении кластеры могут распадаться на точки, которые находятся в одном пикселе. Для этого используется механизм spiderfy.

Он распределяет маркеры по окружности:

  • центр — исходная точка;
  • вокруг — равномерное расположение связанных маркеров;
  • соединительные линии показывают принадлежность.

Настройка:

const markers = L.markerClusterGroup({
  spiderfyOnMaxZoom: true
});

Работа с GeoJSON

Кластеризация часто применяется к GeoJSON-данным.

const geoJsonLayer = L.geoJSON(data, {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng);
  }
});

markers.addLayer(geoJsonLayer);
map.addLayer(markers);

При больших наборах данных (десятки тысяч объектов) добавление выполняется пакетно:

markers.addLayers(geoJsonPointsArray);

Динамическая загрузка данных

При работе с сервером часто используется стратегия подгрузки по границам карты.

Логика:

  • определяется текущий bounding box;
  • отправляется запрос на сервер;
  • сервер возвращает точки в пределах области;
  • данные добавляются в кластер.
map.on('moveend', function () {
  const bounds = map.getBounds();

  fetch(`/api/points?bbox=${bounds.toBBoxString()}`)
    .then(r => r.json())
    .then(data => {
      markers.clearLayers();
      markers.addLayers(data.map(p => L.marker([p.lat, p.lng])));
    });
});

Серверная кластеризация

При очень больших объёмах данных (миллионы точек) клиентская кластеризация становится недостаточной.

В таких случаях применяется серверная агрегация:

  • сервер делит карту на сетку;
  • вычисляет количество точек в ячейках;
  • возвращает агрегированные кластеры вместо отдельных маркеров.

Типичный формат ответа:

{
  "clusters": [
    { "lat": 51.5, "lng": -0.1, "count": 120 },
    { "lat": 51.49, "lng": -0.08, "count": 45 }
  ]
}

На клиенте такие данные отображаются как кастомные маркеры без внутренней логики группировки.


Производительность и оптимизация

Ключевые факторы производительности:

  • количество DOM-элементов;
  • частота пересчёта кластеров;
  • сложность иконок;
  • обработка событий.

Практические оптимизации:

  • отключение анимаций при больших данных:
L.markerClusterGroup({
  animate: false
});
  • использование Canvas для маркеров при очень больших наборах;
  • минимизация тяжёлых HTML-иконок;
  • пакетное добавление точек вместо поштучного.

Настройка поведения кластеров

Основные параметры:

  • maxClusterRadius — радиус объединения;
  • disableClusteringAtZoom — отключение кластеризации на высоких zoom;
  • spiderfyOnEveryZoom — раскрытие при каждом увеличении;
  • removeOutsideVisibleBounds — оптимизация видимых областей.

Пример конфигурации:

const markers = L.markerClusterGroup({
  maxClusterRadius: 60,
  disableClusteringAtZoom: 18,
  spiderfyOnEveryZoom: true,
  removeOutsideVisibleBounds: true
});

Обработка событий кластеров

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

  • clusterclick — клик по группе;
  • clustermouseover — наведение;
  • animationend — завершение анимации.
markers.on('clusterclick', function (a) {
  console.log(a.layer.getAllChildMarkers().length);
});

Доступ к дочерним элементам:

const children = cluster.getAllChildMarkers();

Гибридные стратегии отображения

В сложных системах используется комбинированный подход:

  • кластеризация на низких масштабах;
  • heatmap на среднем уровне;
  • отдельные маркеры на высоком уровне.

Такая схема снижает нагрузку и улучшает восприятие плотных данных.


Типичные ошибки при использовании кластеризации

  • добавление уже кластеризованных слоёв повторно;
  • смешивание разных типов маркеров без унификации;
  • отсутствие очистки при перезагрузке данных;
  • слишком маленький радиус кластеризации, приводящий к «шума»;
  • слишком большие иконки кластеров, перекрывающие карту.

Архитектурные подходы в крупных проектах

В системах с высокой нагрузкой применяется разделение:

  • слой данных (raw points);
  • слой агрегации (clusters);
  • слой визуализации (markers/icons).

Дополнительно вводится кеширование кластеров по zoom-уровням, что позволяет избежать повторных вычислений при возврате к уже посещённым масштабам.