Кластеризация точечных данных в GeoJSON-источниках Mapbox GL JS реализуется поверх алгоритма пространственного разбиения, основанного на плотности точек в текущем масштабе карты. Внутри Mapbox GL JS используется подход, близкий к Supercluster, где точки объединяются в группы в зависимости от зума и радиуса кластеризации.
Базовая активация кластеризации включается на уровне источника данных:
map.addSource('points', {
type: 'geojson',
data: geojsonData,
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14
});
Параметр clusterRadius определяет радиус в пикселях, в пределах которого точки считаются частью одного кластера. clusterMaxZoom ограничивает максимальный зум, на котором выполняется агрегация. После этого порога данные отображаются как отдельные точки.
Встроенная кластеризация ориентирована на геометрическую близость и не учитывает семантику данных. Это означает, что все точки равнозначны вне зависимости от их свойств. Такой подход подходит для визуализации плотности, но становится ограничением при необходимости бизнес-логики: веса, категории, временные интервалы.
Ключевая проблема заключается в том, что стандартная модель возвращает только базовые агрегаты:
Эти данные недостаточны для сложных сценариев анализа.
Расширение логики кластеризации возможно через clusterProperties. Этот механизм позволяет вычислять агрегированные значения на основе свойств исходных точек.
map.addSource('points', {
type: 'geojson',
data: geojsonData,
cluster: true,
clusterRadius: 60,
clusterProperties: {
totalValue: ['+', ['get', 'value']],
maxPriority: ['max', ['get', 'priority']]
}
});
Здесь используется выраженческая система Mapbox, позволяющая задавать редукционные функции:
+ — суммирование значенийmax — поиск максимумаmin — поиск минимумаВ результате каждый кластер содержит не только количество точек, но и агрегированную бизнес-информацию, что позволяет строить более сложную визуализацию.
Кластеры в Mapbox GL JS отображаются как отдельные feature с типом point, что позволяет применять стандартные слои:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-radius': [
'step',
['get', 'point_count'],
15, 10,
25, 50,
35, 100
],
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
50,
'#f1f075',
100,
'#f28cb1'
]
}
});
Использование выражений step позволяет динамически изменять стиль кластера в зависимости от его плотности.
Отдельно отображаются одиночные точки:
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'points',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6
}
});
В случаях, когда встроенный алгоритм недостаточен, применяется внешняя кластеризация до передачи данных в Mapbox. Наиболее распространённый подход — использование Supercluster напрямую.
import Supercluster from 'supercluster';
const index = new Supercluster({
radius: 60,
maxZoom: 16,
map: props => ({
weight: props.weight
}),
reduce: (accumulated, props) => {
accumulated.weight += props.weight;
}
});
index.load(geojsonData.features);
После этого данные запрашиваются вручную в зависимости от текущего зума:
map.on('moveend', () => {
const zoom = Math.floor(map.getZoom());
const bounds = map.getBounds();
const clusters = index.getClusters(
[
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth()
],
zoom
);
map.getSource('clusters').setData({
type: 'FeatureCollection',
features: clusters
});
});
Такой подход снимает ограничения встроенной модели и позволяет полностью контролировать алгоритм агрегации.
В некоторых архитектурах кластеризация выполняется на сервере. В этом случае Mapbox получает уже агрегированные данные:
map.addSource('server-clusters', {
type: 'geojson',
data: '/api/clusters'
});
Сервер может учитывать:
Клиентская часть только визуализирует результат, не выполняя вычислений.
Сложные сценарии часто комбинируют несколько полей:
clusterProperties: {
highPriorityCount: [
'+',
['case',
['>=', ['get', 'priority'], 5],
1,
0
]
]
}
Таким образом можно формировать кластеры, учитывающие не только количество, но и распределение категорий.
Стандартная кластеризация не разделяет объекты по типам. Для этого применяют либо фильтрацию, либо дублирование источников:
map.addSource('restaurants', {
type: 'geojson',
data: data,
cluster: true,
filter: ['==', ['get', 'type'], 'restaurant']
});
Либо используют единый источник и выражения фильтрации на слоях:
filter: ['all',
['has', 'point_count'],
['==', ['get', 'category'], 'food']
]
При работе с временными данными используется дополнительное поле времени:
clusterProperties: {
recentCount: [
'+',
['case',
['>', ['get', 'timestamp'], cutoffTime],
1,
0
]
]
}
Взвешенная кластеризация позволяет учитывать значимость точек:
clusterProperties: {
weightedSum: ['+', ['*', ['get', 'weight'], 1]]
}
Это превращает кластеры в аналитические единицы, а не просто визуальные группы.
Кластеризация напрямую влияет на производительность рендеринга WebGL-контекста. Встроенный механизм оптимизирован под tiled indexing, однако при больших наборах данных возникают ограничения:
Оптимизация достигается следующими методами:
Особенно эффективно использование тайловых источников:
map.addSource('tiles', {
type: 'vector',
url: 'mapbox://dataset.id'
});
В этом случае кластеризация может выполняться на уровне тайлов, что снижает нагрузку на клиент.
Кластеризация может адаптироваться к текущему состоянию интерфейса. Например, изменение радиуса в зависимости от масштаба:
map.on('zoom', () => {
const zoom = map.getZoom();
const radius = zoom > 10 ? 30 : 80;
map.getSource('points').setProps({
clusterRadius: radius
});
});
Такой подход позволяет балансировать между точностью и читаемостью визуализации.
Дополнительно применяется фильтрация по bounding box, что ограничивает набор данных только видимой областью, снижая вычислительную нагрузку и ускоряя обновление кластеров.