При отображении большого количества точек на карте возникает сразу несколько проблем:
Кластеризация позволяет автоматически объединять близко расположенные точки в группы (кластеры). Вместо отображения тысяч отдельных маркеров пользователь видит ограниченное число агрегированных объектов. По мере увеличения масштаба кластеры распадаются на более мелкие группы и затем на отдельные точки.
MapLibre GL JS поддерживает кластеризацию непосредственно на уровне GeoJSON-источника, благодаря чему отсутствует необходимость самостоятельно рассчитывать группы объектов.
Допустим, имеется набор точек:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [37.62, 55.75]
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [37.63, 55.76]
}
}
]
}
При включении кластеризации MapLibre анализирует пространственное расположение объектов и создаёт специальные точки-кластеры.
Каждый кластер получает дополнительные свойства:
| Свойство | Описание |
|---|---|
cluster |
Флаг кластера (true) |
cluster_id |
Уникальный идентификатор |
point_count |
Количество объектов внутри кластера |
point_count_abbreviated |
Сокращённое представление количества |
Пример свойств сформированного кластера:
{
cluster: true,
cluster_id: 45,
point_count: 178,
point_count_abbreviated: "178"
}
Кластеризация активируется при создании GeoJSON-источника.
map.addSource("earthquakes", {
type: "geojson",
data: "/data/earthquakes.geojson",
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Основной переключатель механизма.
cluster: true
Если указать:
cluster: false
или полностью убрать параметр, каждая точка будет отображаться отдельно.
Определяет максимальный уровень масштаба, на котором разрешено объединение объектов.
clusterMaxZoom: 14
Поведение:
| Масштаб | Результат |
|---|---|
| 5 | Крупные кластеры |
| 10 | Более мелкие кластеры |
| 14 | Последний уровень кластеризации |
| 15+ | Отдельные точки |
Чем выше значение, тем дольше сохраняется объединение объектов.
Определяет радиус группировки точек в пикселях.
clusterRadius: 50
Малое значение:
clusterRadius: 20
создаёт много небольших кластеров.
Большое значение:
clusterRadius: 100
объединяет объекты в крупные группы.
Сравнение:
| Радиус | Результат |
|---|---|
| 20 | Много кластеров |
| 50 | Сбалансированная группировка |
| 100 | Небольшое количество крупных кластеров |
После включения кластеризации необходимо создать слой для отображения групп.
Обычно используется слой типа circle.
map.addLayer({
id: "clusters",
type: "circle",
source: "earthquakes",
filter: ["has", "point_count"],
paint: {
"circle-color": "#51bbd6",
"circle-radius": 20
}
});
Кластеры содержат свойство:
point_count
Поэтому можно отбирать их следующим фильтром:
filter: ["has", "point_count"]
Он означает:
отображать только объекты, имеющие свойство
point_count.
Все кластеры удовлетворяют этому условию.
Обычно внутри круга выводится число объектов.
Для этого создаётся слой типа symbol.
map.addLayer({
id: "cluster-count",
type: "symbol",
source: "earthquakes",
filter: ["has", "point_count"],
layout: {
"text-field": ["get", "point_count_abbreviated"],
"text-size": 12
}
});
Теперь каждый кластер отображает количество содержащихся точек.
Примеры:
12
57
124
1.2k
5k
Объекты, не входящие в кластер, необходимо отображать отдельным слоем.
Используется противоположный фильтр:
filter: ["!", ["has", "point_count"]]
Полный пример:
map.addLayer({
id: "unclustered-point",
type: "circle",
source: "earthquakes",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": "#f28cb1",
"circle-radius": 6
}
});
Логика работы:
clusters;cluster-count;unclustered-point.map.addSource("places", {
type: "geojson",
data: "/data/places.geojson",
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.addLayer({
id: "clusters",
type: "circle",
source: "places",
filter: ["has", "point_count"],
paint: {
"circle-color": "#4caf50",
"circle-radius": 20
}
});
map.addLayer({
id: "cluster-count",
type: "symbol",
source: "places",
filter: ["has", "point_count"],
layout: {
"text-field": ["get", "point_count_abbreviated"],
"text-size": 14
}
});
map.addLayer({
id: "unclustered-point",
type: "circle",
source: "places",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": "#ff5722",
"circle-radius": 6
}
});
Часто размер круга отражает число точек внутри группы.
Для этого применяется выражение step.
paint: {
"circle-radius": [
"step",
["get", "point_count"],
15,
100,
20,
500,
30
]
}
Логика:
| Количество объектов | Радиус |
|---|---|
| 0–99 | 15 |
| 100–499 | 20 |
| 500+ | 30 |
Таким образом крупные группы становятся визуально заметнее.
Аналогичным образом можно изменять цвет.
paint: {
"circle-color": [
"step",
["get", "point_count"],
"#51bbd6",
100,
"#f1f075",
500,
"#f28cb1"
]
}
Результат:
| Количество точек | Цвет |
|---|---|
| до 100 | голубой |
| 100–499 | жёлтый |
| 500+ | розовый |
Подобная визуализация помогает быстро оценивать плотность данных.
Популярный сценарий — раскрытие кластера по щелчку.
Для этого необходимо определить уровень масштабирования, на котором кластер распадётся.
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("places");
const zoom =
await source.getClusterExpansionZoom(
clusterId
);
map.easeTo({
center:
features[0].geometry.coordinates,
zoom
});
});
Последовательность работы:
cluster_id;Иногда требуется узнать состав группы.
Для этого используется метод:
getClusterLeaves()
Пример:
const leaves =
await source.getClusterLeaves(
clusterId,
100,
0
);
Аргументы:
| Параметр | Назначение |
|---|---|
| clusterId | идентификатор кластера |
| 100 | максимальное число объектов |
| 0 | смещение |
Результат:
[
{
type: "Feature",
properties: {
name: "Object A"
}
},
{
type: "Feature",
properties: {
name: "Object B"
}
}
]
Полученные данные могут использоваться для формирования всплывающих окон, боковых панелей или таблиц.
Метод:
getClusterChildren()
возвращает непосредственные дочерние элементы выбранного кластера.
const children =
await source.getClusterChildren(
clusterId
);
Результатом могут быть:
Это позволяет строить собственную логику навигации по иерархии групп.
MapLibre позволяет вычислять собственные статистики при формировании кластеров.
Например, имеется набор объектов:
{
value: 25
}
Можно вычислять сумму значений.
map.addSource("places", {
type: "geojson",
data: geojson,
cluster: true,
clusterProperties: {
sum: [
["+", ["accumulated"], ["get", "value"]],
["get", "value"]
]
}
});
Теперь каждый кластер содержит дополнительное свойство:
{
sum: 4872
}
Его можно использовать в подписях, стилях и всплывающих окнах.
Допускается вычисление нескольких характеристик одновременно.
clusterProperties: {
totalPopulation: [
["+", ["accumulated"],
["get", "population"]],
["get", "population"]
],
totalSales: [
["+", ["accumulated"],
["get", "sales"]],
["get", "sales"]
]
}
После формирования кластера будут доступны:
cluster.properties.totalPopulation
cluster.properties.totalSales
Кластеризация в MapLibre основана на высокопроизводительном алгоритме пространственного индексирования и рассчитана на работу с большими объёмами данных.
Практические рекомендации:
clusterRadius;При правильной настройке карта способна комфортно отображать десятки и сотни тысяч точек.
Наиболее распространённая структура выглядит следующим образом:
GeoJSON Source
│
├── clusters
│ └── круги кластеров
│
├── cluster-count
│ └── количество объектов
│
└── unclustered-point
└── одиночные точки
Такой подход обеспечивает: