Раскрытие кластеров

Кластеризация в Mapbox GL JS основана на алгоритме Supercluster, который выполняет пространственную агрегацию GeoJSON-объектов на стороне клиента. Основная идея заключается в объединении близко расположенных точек в агрегированные объекты (кластеры) при низких уровнях масштабирования и их постепенном «раскрытии» по мере увеличения zoom.

Кластер в терминах Mapbox GL JS представляет собой виртуальный GeoJSON-объект с дополнительными свойствами:

  • cluster: true — признак агрегированного объекта
  • cluster_id — идентификатор кластера
  • point_count — количество точек внутри кластера
  • point_count_abbreviated — сокращённое отображение количества

Формирование кластеров на уровне источника данных

Кластеризация активируется на уровне источника GeoJSONSource:

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

Ключевые параметры:

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

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


Визуальное представление кластеров

Для отображения кластеров обычно используются отдельные слои:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': '#51bbd6',
    'circle-radius': 18
  }
});

map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'points',
  filter: ['has', 'point_count'],
  layout: {
    'text-field': '{point_count_abbreviated}',
    'text-size': 12
  }
});

Отдельно отображаются одиночные точки:

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

Логика раскрытия кластеров

Раскрытие кластеров в Mapbox GL JS представляет собой переход от агрегированного состояния к более детализированному уровню отображения. Основной механизм реализуется через вычисление zoom-уровня, при котором кластер перестаёт существовать как единое целое.

Определение zoom для раскрытия кластера

Mapbox GL JS предоставляет метод:

source.getClusterExpansionZoom(clusterId, (err, zoom) => {
  map.easeTo({
    center: coordinates,
    zoom
  });
});

Функция возвращает минимальный zoom, при котором выбранный кластер разбивается на дочерние элементы.

Математически это связано с плотностью распределения точек и параметрами clusterRadius и текущей плиточной сеткой.


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

При обработке события клика по кластеру выполняется запрос информации о конкретном агрегированном объекте:

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

  const clusterId = features[0].properties.cluster_id;
  const coordinates = features[0].geometry.coordinates;
});

Далее используется последовательность API-вызовов:

map.getSource('points').getClusterExpansionZoom(
  clusterId,
  (err, zoom) => {
    map.easeTo({
      center: coordinates,
      zoom: zoom
    });
  }
);

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


Получение дочерних элементов кластера

Supercluster поддерживает навигацию по дереву кластеров.

Прямые дочерние элементы

source.getClusterChildren(clusterId, (err, children) => {
});

Возвращаются объекты следующего уровня иерархии — это могут быть как под-кластеры, так и отдельные точки.


Листовые элементы (точки внутри кластера)

source.getClusterLeaves(clusterId, limit, offset, (err, leaves) => {
});

Параметры:

  • limit — максимальное количество возвращаемых точек
  • offset — смещение для постраничной выборки

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


Альтернативные стратегии раскрытия

Пошаговая декомпозиция кластеров

Вместо изменения zoom возможна последовательная замена одного кластера его дочерними элементами. Логика основана на getClusterChildren, где каждый уровень отображается отдельно.

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


“Spiderfy”-раскрытие

При высокой плотности точек на ограниченной области применяется радиальное распределение объектов вокруг центра кластера. Геометрически точки размещаются по окружности:

  • центр кластера остаётся фиксированным
  • точки смещаются по углам с равным шагом
  • радиус увеличивается пропорционально количеству элементов

Данный подход позволяет визуально разделить объекты без изменения zoom.


Состояния кластера в зависимости от масштаба

Кластеры существуют только в пределах заданного диапазона zoom:

  • при низком zoom — крупные агрегаты
  • при среднем zoom — промежуточные кластеры
  • при высоком zoom — отдельные точки

Переходы между состояниями происходят на основе динамического перерасчёта Supercluster.


Обновление кластеров при изменении данных

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

map.getSource('points').setData(newGeoJson);

После этого:

  • пересчитывается кластерное дерево
  • обновляются cluster_id
  • пересоздаются уровни агрегации

Кластеризация не требует ручного пересоздания слоёв — визуализация остаётся связанной с source.


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

Часто применяется условная стилизация по point_count:

'circle-color': [
  'step',
  ['get', 'point_count'],
  '#51bbd6',
  100,
  '#f1f075',
  750,
  '#f28cb1'
],
'circle-radius': [
  'step',
  ['get', 'point_count'],
  15,
  100,
  20,
  750,
  25
]

Поведение основано на пороговых значениях:

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

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

Mapbox GL JS различает взаимодействие с:

  • кластером (cluster: true)
  • одиночной точкой

События привязываются к слоям:

map.on('click', 'unclustered-point', (e) => {});

или:

map.on('click', 'clusters', (e) => {});

Фильтрация выполняется через filter слоя, основанный на наличии point_count.


Ограничения и особенности поведения

Кластеризация зависит от нескольких факторов:

  • плотность распределения геоданных
  • радиус агрегации
  • текущий zoom
  • проекция карты

При больших наборах данных:

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

Границы раскрытия кластеров

Раскрытие кластера через getClusterExpansionZoom не гарантирует переход к единичным точкам. Результат зависит от структуры данных:

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

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

Поведение раскрытия может модифицироваться через:

  • изменение clusterRadius
  • динамическое обновление источника
  • кастомную обработку cluster_id
  • ручное управление zoom-переходами

При этом структура Supercluster остаётся неизменной, а изменяется только интерпретация результата.


Работа с вложенными уровнями кластеров

Иерархия кластеров представляет собой дерево:

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

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