Включение кластеризации в GeoJSON

При отображении большого количества точек на карте возникает сразу несколько проблем:

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

Кластеризация позволяет автоматически объединять близко расположенные точки в группы (кластеры). Вместо отображения тысяч отдельных маркеров пользователь видит ограниченное число агрегированных объектов. По мере увеличения масштаба кластеры распадаются на более мелкие группы и затем на отдельные точки.

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

Основной переключатель механизма.

cluster: true

Если указать:

cluster: false

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


Параметр clusterMaxZoom

Определяет максимальный уровень масштаба, на котором разрешено объединение объектов.

clusterMaxZoom: 14

Поведение:

Масштаб Результат
5 Крупные кластеры
10 Более мелкие кластеры
14 Последний уровень кластеризации
15+ Отдельные точки

Чем выше значение, тем дольше сохраняется объединение объектов.


Параметр clusterRadius

Определяет радиус группировки точек в пикселях.

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

});

Последовательность работы:

  1. определяется выбранный кластер;
  2. извлекается его cluster_id;
  3. вычисляется масштаб раскрытия;
  4. карта плавно увеличивается до нужного уровня.

Получение объектов внутри кластера

Иногда требуется узнать состав группы.

Для этого используется метод:

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 основана на высокопроизводительном алгоритме пространственного индексирования и рассчитана на работу с большими объёмами данных.

Практические рекомендации:

  • использовать GeoJSON только для реально отображаемых данных;
  • избегать избыточных свойств объектов;
  • подбирать разумное значение clusterRadius;
  • не создавать чрезмерно сложные выражения оформления;
  • использовать кластеризацию вместо тысяч HTML-маркеров.

При правильной настройке карта способна комфортно отображать десятки и сотни тысяч точек.


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

Наиболее распространённая структура выглядит следующим образом:

GeoJSON Source
│
├── clusters
│   └── круги кластеров
│
├── cluster-count
│   └── количество объектов
│
└── unclustered-point
    └── одиночные точки

Такой подход обеспечивает:

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