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

Кластеризация позволяет отображать тысячи и десятки тысяч точек на карте без перегрузки интерфейса. Вместо отображения каждого объекта отдельно 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()

Основным инструментом раскрытия кластеров является метод 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
            });

        }
    );

});

Последовательность действий:

  1. Пользователь нажимает на кластер.
  2. Определяется объект кластера.
  3. Извлекается cluster_id.
  4. Вычисляется масштаб раскрытия.
  5. Карта плавно перемещается к кластеру.
  6. После завершения анимации кластер распадается на более мелкие группы.

Использование flyTo()

Для создания более выразительной навигации часто применяется метод 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;
  • произошёл сбой обработки GeoJSON.

Практический шаблон раскрытия кластеров

Ниже приведён типовой шаблон, применяемый в большинстве проектов:

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
            });

        }
    );

});

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