Marker Clustering Plus

Обработка большого количества маркеров на карте приводит к снижению производительности и ухудшению визуального восприятия: десятки или сотни объектов перекрывают друг друга, затрудняя навигацию и анализ данных. Решением выступает кластеризация — группировка близко расположенных маркеров в единые визуальные элементы, которые динамически изменяются при масштабировании карты.

В контексте Google Maps JavaScript API кластеризация чаще всего реализуется через сторонние библиотеки, одной из наиболее известных среди которых является MarkerClustererPlus.


Проблема перегруженных карт

При отображении большого числа точек возникают следующие ограничения:

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

Даже 200–300 маркеров могут создавать заметную нагрузку на слабых устройствах, особенно при активных событиях zoom_changed и drag.


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

Кластеризация основана на пространственном объединении маркеров в зависимости от текущего масштаба карты:

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

Алгоритмически используется разбиение по сетке (grid-based clustering) или деревья пространственного поиска (quad-tree / k-d tree), однако MarkerClustererPlus использует упрощённый grid-based подход, оптимизированный для браузера.


Подключение Google Maps JavaScript API

Перед использованием кластеризации необходимо подключить сам API:

<script
  src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
  async
  defer
></script>

Инициализация карты:

function initMap() {
  const map = new google.maps.Map(document.getElementById("map"), {
    center: { lat: 48.0, lng: 66.0 },
    zoom: 5,
  });
}

Подключение MarkerClustererPlus

Подключение библиотеки:

<script src="https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js"></script>

После подключения становится доступен объект MarkerClusterer.


Базовая интеграция

Создание маркеров и их объединение в кластер:

function initMap() {
  const map = new google.maps.Map(document.getElementById("map"), {
    center: { lat: 48.0, lng: 66.0 },
    zoom: 5,
  });

  const locations = [
    { lat: 48.0, lng: 66.0 },
    { lat: 49.0, lng: 67.0 },
    { lat: 47.5, lng: 65.5 },
  ];

  const markers = locations.map((position) => {
    return new google.maps.Marker({
      position,
      map,
    });
  });

  const clusterer = new MarkerClusterer(map, markers, {
    imagePath:
      "https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m",
  });
}

Настройка параметров кластеризации

MarkerClustererPlus поддерживает набор конфигураций, влияющих на поведение группировки:

  • gridSize — размер сетки кластеризации
  • maxZoom — уровень зума, при котором кластер перестаёт формироваться
  • minimumClusterSize — минимальное количество маркеров в кластере
  • styles — кастомные стили отображения кластеров

Пример настройки:

const clusterer = new MarkerClusterer(map, markers, {
  gridSize: 60,
  maxZoom: 15,
  minimumClusterSize: 2,
  imagePath:
    "https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m",
});

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

Визуальное оформление кластеров критично для UX. Можно задавать разные стили в зависимости от размера группы:

const styles = [
  {
    url: "cluster-small.png",
    width: 40,
    height: 40,
    textColor: "#ffffff",
    textSize: 12,
  },
  {
    url: "cluster-medium.png",
    width: 50,
    height: 50,
    textColor: "#ffffff",
    textSize: 13,
  },
  {
    url: "cluster-large.png",
    width: 60,
    height: 60,
    textColor: "#ffffff",
    textSize: 14,
  },
];

const clusterer = new MarkerClusterer(map, markers, {
  styles,
});

Стили применяются в зависимости от количества маркеров в группе.


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

Кластеры поддерживают события взаимодействия:

google.maps.event.addListener(clusterer, "clusterclick", (cluster) => {
  const bounds = cluster.getBounds();
  map.fitBounds(bounds);
});

Такой подход позволяет реализовать поведение «drill-down» — при клике карта приближается к области кластера.


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

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

function updateMarkers(newLocations) {
  const newMarkers = newLocations.map((pos) => {
    return new google.maps.Marker({ position: pos });
  });

  clusterer.clearMarkers();
  clusterer.addMarkers(newMarkers);
}

Ключевой момент — очистка старого состояния перед добавлением нового набора маркеров.


Фильтрация данных и пересборка кластеров

При применении фильтров (например, по категории или радиусу) используется пересборка:

function filterMarkers(conditionFn, allMarkers) {
  const filtered = allMarkers.filter((m) =>
    conditionFn(m.getPosition())
  );

  clusterer.clearMarkers();
  clusterer.addMarkers(filtered);
}

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

При работе с тысячами объектов необходимо учитывать:

  • минимизацию количества DOM-операций
  • использование DocumentFragment при генерации данных
  • отключение лишних обработчиков событий
  • предварительную агрегацию данных на сервере
  • lazy-loading маркеров при изменении bounds

Дополнительно полезно:

  • ограничивать maxZoom
  • уменьшать частоту пересоздания кластеров
  • избегать постоянного вызова setMap(null) для маркеров

Работа с большими наборами данных

При объёмах 10 000+ точек становится критичным перенос части логики на сервер:

  • серверная кластеризация (pre-clustering)
  • выдача данных по тайлам (tile-based loading)
  • использование GeoHash или S2 geometry для индексации

На клиенте остаётся только визуализация уже агрегированных данных.


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

Ключевой механизм работы кластеризации связан с событиями карты:

  • zoom_changed
  • bounds_changed

Каждое изменение пересчитывает распределение маркеров по сетке кластеров. MarkerClustererPlus автоматически подписывается на эти события и пересобирает структуру без ручного вмешательства.


Ограничения подхода MarkerClustererPlus

Несмотря на удобство, библиотека имеет ряд ограничений:

  • отсутствие поддержки WebGL-рендеринга
  • ограниченная гибкость алгоритма кластеризации
  • устаревшая архитектура по сравнению с современными решениями
  • зависимость от DOM-изображений для кластеров

В современных проектах иногда используется альтернативный @googlemaps/markerclusterer, однако MarkerClustererPlus остаётся распространённым в легаси-системах.


Интеграция с кастомными маркерами

Кластеры могут включать нестандартные маркеры:

const marker = new google.maps.Marker({
  position,
  icon: {
    url: "custom-pin.svg",
    scaledSize: new google.maps.Size(30, 30),
  },
});

Clusterer работает независимо от типа маркера, пока он наследует google.maps.Marker.


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

При сложных интерфейсах важно корректно управлять состоянием:

  • уничтожение кластеров при смене слоя данных
  • повторная инициализация при смене карты
  • синхронизация с внешним store (Redux / Zustand / Vuex)
function destroyCluster() {
  clusterer.clearMarkers();
  clusterer.setMap(null);
}

Поведение при интерактивных сценариях

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

  • геопоиском
  • фильтрацией по радиусу
  • отображением heatmap-слоёв
  • динамическими обновлениями через WebSocket

В таких случаях MarkerClustererPlus выступает только как визуальный слой агрегации поверх потоковых данных.