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

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

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


Механизм кластеризации в Mapbox GL JS

В Mapbox GL JS кластеризация реализуется на уровне GeoJSONSource. Источник данных может быть сконфигурирован так, чтобы библиотека автоматически группировала точки на стороне клиента.

Ключевой принцип заключается в использовании пространственного индекса (вариант grid-based clustering и алгоритмов наподобие Supercluster), который формирует виртуальные группы точек в зависимости от текущего зума.

Основная конфигурация задаётся через параметры:

  • cluster: true — включает кластеризацию
  • clusterRadius — радиус объединения точек в пикселях
  • clusterMaxZoom — максимальный zoom, до которого выполняется кластеризация

Базовая конфигурация GeoJSON источника

Кластеризация активируется непосредственно в источнике данных:

map.addSource('earthquakes', {
  type: 'geojson',
  data: '/data/earthquakes.geojson',
  cluster: true,
  clusterRadius: 50,
  clusterMaxZoom: 14
});

При включённой кластеризации Mapbox автоматически добавляет служебные поля к объектам:

  • point_count — количество точек внутри кластера
  • point_count_abbreviated — сокращённое отображение (например, 1.2k)
  • cluster_id — уникальный идентификатор кластера

Отображение кластеров через слои

Кластеризация сама по себе не отображает визуальные элементы. Для визуализации используются отдельные слои:

Слой кластеров

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'earthquakes',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': '#51bbd6',
    'circle-radius': [
      'step',
      ['get', 'point_count'],
      15,
      100,
      25,
      750,
      35
    ]
  }
});

Логика step позволяет изменять размер круга в зависимости от плотности данных. Чем больше точек в кластере, тем крупнее визуальный элемент.


Слой числовых меток кластеров

map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'earthquakes',
  filter: ['has', 'point_count'],
  layout: {
    'text-field': '{point_count_abbreviated}',
    'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
    'text-size': 12
  }
});

Этот слой отвечает за отображение числового значения внутри кластеров.


Слой одиночных точек

map.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: 'earthquakes',
  filter: ['!', ['has', 'point_count']],
  paint: {
    'circle-color': '#f28cb1',
    'circle-radius': 6
  }
});

Фильтр исключает все объекты, которые являются кластерами, оставляя только исходные точки.


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

Одним из ключевых сценариев является «разворачивание» кластера при клике. Для этого используется метод получения границ следующего уровня масштабирования.

map.on('click', 'clusters', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['clusters']
  });

  const clusterId = features[0].properties.cluster_id;

  map.getSource('earthquakes').getClusterExpansionZoom(
    clusterId,
    (err, zoom) => {
      if (err) return;

      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom: zoom
      });
    }
  );
});

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


Взаимодействие с отдельными точками

Для точечных объектов, не входящих в кластеры, обычно добавляется отдельная обработка:

map.on('click', 'unclustered-point', (e) => {
  const coordinates = e.features[0].geometry.coordinates.slice();
  const properties = e.features[0].properties;

  new mapboxgl.Popup()
    .setLngLat(coordinates)
    .setHTML(`Magnitude: ${properties.magnitude}`)
    .addTo(map);
});

Такой подход позволяет комбинировать агрегацию и детализированную информацию в одном интерфейсе.


Внутренняя модель кластеризации

Хотя API скрывает детали реализации, концептуально используется подход пространственного разбиения:

  1. На каждом уровне zoom создаётся виртуальная сетка
  2. Точки попадают в ячейки этой сетки
  3. Близкие точки объединяются в один кластер
  4. Для каждого кластера вычисляется центр и агрегированные свойства

При увеличении масштаба сетка становится более детальной, и кластеры постепенно распадаются.


Использование кастомных свойств кластеров

GeoJSON позволяет передавать дополнительные свойства, которые могут агрегироваться через clusterProperties:

map.addSource('places', {
  type: 'geojson',
  data: '/data/places.geojson',
  cluster: true,
  clusterProperties: {
    sumRating: ['+', ['get', 'rating']]
  }
});

Здесь создаётся агрегированное поле sumRating, которое суммирует значения рейтинга всех точек внутри кластера.

Доступные операции:

  • + — сумма
  • max — максимум
  • min — минимум
  • avg — среднее значение

Настройка радиуса кластеризации

Параметр clusterRadius влияет на плотность кластеров:

  • малые значения → больше кластеров, выше детализация
  • большие значения → агрессивная агрегация
clusterRadius: 80

Выбор значения зависит от плотности данных и масштаба карты. Для городских данных обычно используется меньший радиус, для глобальных — больший.


Ограничение уровня кластеризации

clusterMaxZoom определяет предел, после которого кластеризация прекращается:

clusterMaxZoom: 12

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


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

Mapbox GL JS использует выражения фильтрации для разделения кластеров и одиночных точек:

  • ['has', 'point_count'] — определяет кластер
  • ['!', ['has', 'point_count']] — определяет одиночную точку

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


Производительность и особенности рендеринга

Кластеризация выполняется на стороне клиента и оптимизирована под WebGL. Это снижает нагрузку на сервер, но увеличивает потребление памяти в браузере.

Факторы влияния на производительность:

  • количество исходных точек
  • сложность clusterProperties
  • частота пересчёта при изменении данных
  • максимальный zoom карты

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


Динамическое обновление данных

Источник GeoJSON может быть обновлён в любой момент:

map.getSource('earthquakes').setData(newGeojson);

При этом кластеры пересчитываются автоматически. Это позволяет использовать потоковые данные (например, события в реальном времени), сохраняя стабильную визуализацию.


Комбинация с фильтрацией и стилями

Кластеры могут быть дополнительно стилизованы через выражения:

  • цвет в зависимости от плотности
  • размер по количеству точек
  • прозрачность по zoom

Пример:

'circle-color': [
  'step',
  ['get', 'point_count'],
  '#00BCD4',
  50,
  '#2196F3',
  200,
  '#3F51B5'
]

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


Типичные архитектурные сценарии

Кластеризация применяется в следующих типах приложений:

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

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


Ограничения модели кластеризации

Несмотря на эффективность, подход имеет ограничения:

  • невозможность сложной серверной логики агрегации без предварительной подготовки данных
  • зависимость от браузерных ресурсов
  • фиксированная модель пространственного разбиения
  • ограниченная гибкость кластеризации по нестандартным метрикам без clusterProperties

Эти ограничения компенсируются предварительной обработкой данных и гибкой настройкой источников.