Кластеризация позволяет отображать тысячи и десятки тысяч точек на карте без перегрузки интерфейса. Вместо отображения каждого объекта отдельно MapLibre GL JS группирует близко расположенные точки в кластеры, отображая их как единый объект с числом вложенных элементов.
Однако простого отображения кластеров недостаточно. Пользователю необходимо быстро переходить от общего обзора данных к детальному просмотру конкретных объектов. Для этого применяется механизм раскрытия кластеров (cluster expansion), при котором карта автоматически увеличивает масштаб и центрируется на выбранном кластере, показывая его внутреннюю структуру.
При включённой кластеризации каждый кластер обладает набором специальных свойств:
cluster — признак того, что объект является
кластером;cluster_id — уникальный идентификатор кластера;point_count — количество объектов внутри кластера;point_count_abbreviated — сокращённое представление
количества точек.Пример GeoJSON-источника с поддержкой кластеризации:
map.addSource('earthquakes', {
type: 'geojson',
data: '/data/earthquakes.geojson',
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14
});
Когда пользователь нажимает на кластер, приложение получает его идентификатор и вычисляет масштаб, на котором данный кластер распадётся на более мелкие группы или отдельные точки.
Основным инструментом раскрытия кластеров является метод
getClusterExpansionZoom().
Сигнатура метода:
source.getClusterExpansionZoom(clusterId, callback);
Параметры:
| Параметр | Описание |
|---|---|
clusterId |
Идентификатор кластера |
callback |
Функция получения результата |
Метод возвращает масштаб, на котором кластер перестанет существовать в текущем виде.
Пример:
const source = map.getSource('earthquakes');
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
console.log(zoom);
}
);
Результат может выглядеть следующим образом:
8
Это означает, что при переходе на масштаб 8 кластер раскроется и покажет более детальное содержимое.
Перед раскрытием необходимо определить, какой именно кластер был выбран пользователем.
Для этого используется обработчик события клика по слою кластеров.
Пример слоя:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#51bbd6',
'circle-radius': 20
}
});
Обработчик клика:
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(
e.point,
{
layers: ['clusters']
}
);
const clusterId =
features[0].properties.cluster_id;
console.log(clusterId);
});
После получения cluster_id можно вычислить необходимый
масштаб раскрытия.
Наиболее распространённый сценарий — плавное увеличение масштаба после клика по кластеру.
Полный пример:
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(
e.point,
{
layers: ['clusters']
}
);
const clusterId =
features[0].properties.cluster_id;
const source =
map.getSource('earthquakes');
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
map.easeTo({
center:
features[0].geometry.coordinates,
zoom
});
}
);
});
Последовательность действий:
cluster_id.Для создания более выразительной навигации часто применяется метод
flyTo().
Пример:
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
map.flyTo({
center: coordinates,
zoom: zoom
});
}
);
Особенности flyTo():
Такой подход особенно удобен при работе с глобальными картами.
Анимацию можно настраивать.
Пример:
map.easeTo({
center: coordinates,
zoom: zoom,
duration: 1200
});
Или:
map.flyTo({
center: coordinates,
zoom: zoom,
speed: 0.8,
curve: 1.4
});
Основные параметры:
| Параметр | Назначение |
|---|---|
duration |
Длительность анимации |
speed |
Скорость перелёта |
curve |
Кривизна траектории |
essential |
Пометка важной анимации |
Корректно подобранные параметры делают раскрытие кластеров визуально понятным и комфортным.
Иногда требуется раскрывать кластер не до вычисленного уровня, а до заранее заданного масштаба.
Пример:
map.on('click', 'clusters', (e) => {
const cluster =
e.features[0];
map.easeTo({
center:
cluster.geometry.coordinates,
zoom: 10
});
});
Такой вариант используется, когда:
Недостаток заключается в том, что кластер может остаться нераскрытым, если выбранный масштаб окажется недостаточным.
При большом объёме данных кластер может раскрываться несколько раз подряд.
Например:
Масштаб 3 → 5000 точек
Масштаб 5 → 800 точек
Масштаб 8 → 150 точек
Масштаб 11 → 25 точек
Масштаб 14 → отдельные объекты
Каждый клик раскрывает только текущий уровень группировки.
Преимущества такого подхода:
Иногда требуется самостоятельно контролировать процесс раскрытия.
Для этого используется метод получения дочерних элементов кластера.
Пример:
source.getClusterChildren(
clusterId,
(error, features) => {
if (error) {
return;
}
console.log(features);
}
);
Метод возвращает:
Это позволяет строить собственные механизмы навигации по иерархии данных.
Для полного раскрытия содержимого применяется метод
getClusterLeaves().
Пример:
source.getClusterLeaves(
clusterId,
100,
0,
(error, features) => {
if (error) {
return;
}
console.log(features);
}
);
Параметры:
| Параметр | Описание |
|---|---|
| Второй аргумент | Максимальное количество объектов |
| Третий аргумент | Смещение для пагинации |
Полученные данные могут использоваться для:
После раскрытия часто отображается дополнительная информация.
Пример:
map.on('click', 'clusters', (e) => {
const feature = e.features[0];
new maplibregl.Popup()
.setLngLat(
feature.geometry.coordinates
)
.setHTML(`
<b>Объектов:</b>
${feature.properties.point_count}
`)
.addTo(map);
});
Возможны различные варианты:
Для улучшения пользовательского опыта рекомендуется визуально обозначать интерактивность кластеров.
Пример:
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
Пользователь сразу понимает, что кластер можно раскрыть.
Иногда требуется предотвратить слишком глубокое увеличение.
Пример:
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
map.easeTo({
center: coordinates,
zoom: Math.min(zoom, 12)
});
}
);
Такой подход полезен:
Любые операции с кластерами должны учитывать возможные ошибки.
Пример:
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
console.error(error);
return;
}
map.easeTo({
center: coordinates,
zoom
});
}
);
Типичные причины ошибок:
cluster_id;Ниже приведён типовой шаблон, применяемый в большинстве проектов:
map.on('click', 'clusters', (e) => {
const features =
map.queryRenderedFeatures(
e.point,
{
layers: ['clusters']
}
);
const cluster =
features[0];
const clusterId =
cluster.properties.cluster_id;
const source =
map.getSource('earthquakes');
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
map.easeTo({
center:
cluster.geometry.coordinates,
zoom: zoom,
duration: 1000
});
}
);
});
Данный механизм обеспечивает естественный переход от обзорного представления данных к детальному просмотру объектов, сохраняя высокую производительность карты даже при работе с очень большими наборами геоданных.