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

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

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

Каждая точка в GeoJSON может быть автоматически объединена в кластер в зависимости от:

  • уровня масштабирования карты (zoom)
  • радиуса кластеризации (clusterRadius)
  • максимального уровня, до которого разрешена кластеризация (clusterMaxZoom)

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

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

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

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

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

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

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

После включения кластеризации источник начинает возвращать два типа объектов:

  • кластеры (cluster: true)
  • одиночные точки (cluster: false или отсутствие cluster свойства)

Для их отображения используются отдельные слои.

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

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

Здесь фильтр ['has', 'point_count'] определяет, что слой применяется только к кластерным объектам.

Ключевое свойство:

  • point_count — количество точек внутри кластера

Слой количества точек

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

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}, который автоматически сокращает большие числа (например, 1200 → 1.2k).

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

map.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: 'points',
  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;
  const source = map.getSource('points');

  source.getClusterExpansionZoom(clusterId, (err, zoom) => {
    if (err) return;

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

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

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

Mapbox GL JS предоставляет API для извлечения точек внутри кластера:

source.getClusterChildren(clusterId, (err, features) => {
  console.log(features);
});

Также доступен метод получения всех точек внутри кластера:

source.getClusterLeaves(clusterId, limit, offset, (err, features) => {
  console.log(features);
});

Эти методы полезны для построения кастомных всплывающих окон и анализа состава кластера.

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

Визуальная дифференциация кластеров часто основана на размере группы. Для этого используется выражение step или interpolate:

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

Логика:

  • до 100 объектов — один цвет
  • от 100 до 750 — другой цвет
  • более 750 — третий цвет

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

Продвинутые выражения для кластеров

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

'circle-radius': [
  'interpolate',
  ['linear'],
  ['get', 'point_count'],
  1, 10,
  100, 20,
  1000, 40
]

Такой подход обеспечивает плавное масштабирование размеров кластеров.

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

Каждый кластер содержит агрегированные свойства, которые можно расширять через clusterProperties.

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

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

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

Кластеры пересчитываются автоматически при:

  • зуме карты
  • перемещении карты
  • изменении источника данных

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

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

Механизм кластеризации имеет ряд характерных особенностей:

  • кластеры формируются только для источников типа GeoJSON
  • данные должны быть загружены полностью в клиент (server-side кластеризация используется отдельно)
  • при очень больших наборах данных может потребоваться предварительная оптимизация GeoJSON
  • поведение кластеров зависит от текущего zoom, что требует аккуратного проектирования интерфейса

Практическая структура слоя кластеризации

Типичная архитектура слоёв включает три уровня:

  1. слой кластеров (агрегированные круги)
  2. слой числовых меток
  3. слой отдельных точек

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

Использование событий наведения и выделения

Для улучшения UX применяется обработка событий:

map.on('mouseenter', 'clusters', () => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'clusters', () => {
  map.getCanvas().style.cursor = '';
});

Дополнительно можно подсвечивать кластер при наведении, изменяя его стиль через feature-state.

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

Кластеры корректно реагируют на фильтры слоя:

map.setFilter('unclustered-point', [
  '==',
  ['get', 'category'],
  'restaurant'
]);

При этом пересчёт кластеров учитывает только активные данные слоя.

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

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

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

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

Роль кластеризации в масштабируемых картографических интерфейсах

В интерфейсах аналитики, логистики и геомаркетинга кластеризация становится основным инструментом визуального уплотнения информации. Она позволяет:

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

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