Кластеризация данных

Природа кластеризации в веб-картографии

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

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

В основе кластеризации в Mapbox GL JS лежит алгоритм Supercluster, обеспечивающий быстрый пространственный индекс и агрегацию данных в режиме реального времени.


Источник данных и включение кластеризации

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

map.addSource('points', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        properties: {},
        geometry: {
          type: 'Point',
          coordinates: [37.6173, 55.7558]
        }
      }
    ]
  },
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

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

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

Структура кластерных данных

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

  • cluster: true — признак кластера
  • point_count — количество точек внутри кластера
  • cluster_id — уникальный идентификатор кластера

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


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

Кластеры визуализируются через отдельные слои circle, symbol или комбинированные стили.

Пример базового отображения кластеров:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': '#51bbd6',
    'circle-radius': [
      'step',
      ['get', 'point_count'],
      15,
      10,
      20,
      50,
      30
    ]
  }
});

Отдельный слой для одиночных точек:

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

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

Часто требуется визуализировать число объектов внутри кластера. Это реализуется через слой symbol:

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

Поле {point_count_abbreviated} автоматически форматирует числа (например, 1.2k).


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

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

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

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

  const source = map.getSource('points');

  const zoom = await source.getClusterExpansionZoom(clusterId);

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

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


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

Кластер можно разложить на составляющие элементы:

map.getSource('points').getClusterLeaves(clusterId, 100, 0, (err, features) => {
  console.log(features);
});

Параметры:

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

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

Для анализа иерархии используется метод получения дочерних кластеров:

map.getSource('points').getClusterChildren(clusterId, (err, features) => {
  console.log(features);
});

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


Геометрическая логика кластеризации

Кластеризация основана на переводе координат в проекцию и применении пространственного индекса. Алгоритм учитывает:

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

Фактически используется структура данных, аналогичная R-tree + k-d tree, оптимизированная для Web Mercator проекции.


Динамическая перестройка кластеров

При изменении:

  • масштаба карты
  • центра карты
  • набора данных

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


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

Кластеры часто разделяются на категории по размеру:

[
  'step',
  ['get', 'point_count'],
  '#51bbd6',
  20,
  '#f1f075',
  100,
  '#f28cb1'
]

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


Работа с пользовательскими свойствами

GeoJSON позволяет передавать дополнительные свойства объектов, которые сохраняются при кластеризации.

{
  type: 'Feature',
  properties: {
    category: 'restaurant'
  },
  geometry: {
    type: 'Point',
    coordinates: [30.5, 50.5]
  }
}

Однако при агрегации свойства не суммируются автоматически. Для этого применяются выражения clusterProperties.


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

Mapbox GL JS позволяет вычислять агрегированные значения:

map.addSource('points', {
  type: 'geojson',
  data: geojsonData,
  cluster: true,
  clusterProperties: {
    sum: ['+', ['get', 'value']]
  }
});

Таким образом кластеры могут содержать сумму, среднее значение или другие метрики.


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

Кластеризация критична для работы с большими наборами данных (десятки тысяч и более объектов). Основные факторы производительности:

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

Увеличение радиуса снижает количество кластеров, но уменьшает точность локализации. Уменьшение радиуса повышает детализацию, но увеличивает нагрузку.


Обработка событий взаимодействия

Кластеры и отдельные точки обрабатываются через единый event API:

map.on('click', 'unclustered-point', (e) => {
  console.log(e.features[0]);
});

Такой подход позволяет разделять поведение для агрегированных и одиночных объектов.


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

Кластеризация в Mapbox GL JS имеет ряд особенностей:

  • невозможность частичной кластеризации внутри слоя
  • пересчёт при каждом изменении источника
  • зависимость от Web Mercator проекции
  • отсутствие кластеризации для raster-слоёв

Взаимодействие с серверной генерацией данных

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

  • автоматическому обновлению
  • адаптивности к масштабу
  • отсутствию необходимости пересчёта на backend

Пространственная логика отображения плотности

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

В результате формируется непрерывная модель плотности, приближенная к heatmap, но с дискретными интерактивными узлами.


Связь с внутренним индексированием

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

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

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