Точки и пузырьки на карте

Работа с географическими точками в D3.js строится вокруг преобразования координат данных (широта и долгота) в экранные координаты SVG или Canvas. Основой служит связка географических проекций, масштабирования и визуальных примитивов, чаще всего окружностей.

Ключевой элемент — географическая проекция, преобразующая сферические координаты в плоские:

const projection = d3.geoMercator()
  .scale(150)
  .translate([width / 2, height / 2]);

Проекция определяет способ отображения карты и влияет на визуальную искаженность объектов. geoMercator используется чаще всего в веб-картах, так как сохраняет углы и подходит для общего анализа распределений.


Преобразование координат в экранные точки

Любая точка на карте задаётся парой значений:

  • долгота (longitude)
  • широта (latitude)

D3 предоставляет функцию преобразования:

const [x, y] = projection([longitude, latitude]);

Результат — координаты в пикселях, которые можно использовать для размещения SVG-элементов.

Пример структуры данных:

const data = [
  { name: "A", coordinates: [71.4304, 51.1282] },
  { name: "B", coordinates: [37.6173, 55.7558] }
];

Построение точек на карте через SVG

Создание точек реализуется через привязку данных к элементам DOM:

svg.selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("cx", d => projection(d.coordinates)[0])
  .attr("cy", d => projection(d.coordinates)[1])
  .attr("r", 4)
  .attr("fill", "steelblue");

Каждый объект данных превращается в визуальный маркер. Такой подход обеспечивает декларативное связывание данных и визуализации.


Масштабирование точек: визуализация интенсивности

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

D3 предоставляет шкалы:

const radiusScale = d3.scaleSqrt()
  .domain([0, d3.max(data, d => d.value)])
  .range([2, 30]);

Использование квадратного корня позволяет компенсировать визуальное восприятие площади.

Применение:

.attr("r", d => radiusScale(d.value))

Пузырьковая карта как модель агрегации данных

Пузырьковая карта (bubble map) представляет точки с радиусом, пропорциональным значению метрики. Она применяется для отображения:

  • численности населения
  • количества событий
  • интенсивности явлений
  • экономических показателей

Структура данных:

[
  { coordinates: [71.4304, 51.1282], value: 120 },
  { coordinates: [55.7558, 37.6173], value: 300 }
]

Рендеринг:

svg.selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("cx", d => projection(d.coordinates)[0])
  .attr("cy", d => projection(d.coordinates)[1])
  .attr("r", d => radiusScale(d.value))
  .attr("fill", "orange")
  .attr("opacity", 0.6);

Работа с GeoJSON для фоновой карты

Для отображения карты используется формат GeoJSON. Он описывает географические объекты в виде полигонов.

d3.json("world.geojson").then(world => {
  svg.append("path")
    .datum(world)
    .attr("d", d3.geoPath().projection(projection))
    .attr("fill", "#eee")
    .attr("stroke", "#999");
});

geoPath преобразует географические данные в SVG-path.


Слойность визуализации: фон и данные

Корректная визуализация требует разделения слоёв:

  1. фон карты (страны, регионы)
  2. точки данных
  3. интерактивные элементы

Пример структуры:

const mapLayer = svg.append("g");
const pointsLayer = svg.append("g");
const overlayLayer = svg.append("g");

Такое разделение упрощает управление z-index и взаимодействием.


Интерактивность точек

D3 позволяет добавлять поведение к элементам через события:

pointsLayer.selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .on("mouseover", function(event, d) {
    d3.select(this)
      .attr("fill", "red")
      .attr("r", radiusScale(d.value) * 1.2);
  })
  .on("mouseout", function(event, d) {
    d3.select(this)
      .attr("fill", "orange")
      .attr("r", radiusScale(d.value));
  });

Такая модель обеспечивает динамическую обратную связь без перерисовки всей сцены.


Кластеризация точек для плотных распределений

При большом количестве точек визуальная перегрузка решается кластеризацией.

Подходы:

  • географическая агрегация по сетке
  • использование алгоритмов кластеризации (quadtree)
  • динамическое масштабирование

Пример использования d3.quadtree:

const quadtree = d3.quadtree()
  .x(d => projection(d.coordinates)[0])
  .y(d => projection(d.coordinates)[1])
  .addAll(data);

Quadtree позволяет эффективно искать соседние точки и группировать их.


Использование Canvas для высоконагруженных карт

SVG ограничен количеством DOM-элементов. При десятках тысяч точек используется Canvas:

const context = canvas.node().getContext("2d");

data.forEach(d => {
  const [x, y] = projection(d.coordinates);
  context.beginPath();
  context.arc(x, y, radiusScale(d.value), 0, 2 * Math.PI);
  context.fillStyle = "rgba(255, 165, 0, 0.6)";
  context.fill();
});

Canvas повышает производительность за счёт отказа от DOM.


Анимация появления и изменения пузырьков

Переходы D3 позволяют плавно изменять визуальные параметры:

svg.selectAll("circle")
  .data(data)
  .join("circle")
  .transition()
  .duration(800)
  .attr("r", d => radiusScale(d.value));

Анимация используется при обновлении данных или изменении масштаба карты.


Масштабирование карты и перерасчёт точек

При zoom-поведении необходимо пересчитывать координаты:

const zoom = d3.zoom()
  .scaleExtent([1, 8])
  .on("zoom", (event) => {
    mapLayer.attr("transform", event.transform);
    pointsLayer.attr("transform", event.transform);
  });

svg.call(zoom);

Zoom изменяет трансформацию всей сцены, сохраняя географическую согласованность.


Оптимизация отрисовки географических точек

Основные методы оптимизации:

  • использование scaleSqrt вместо линейного масштаба радиусов
  • ограничение количества DOM-элементов
  • применение Canvas при больших наборах данных
  • предварительная агрегация данных
  • использование quadtree для пространственных операций

Цветовое кодирование значений

Цвета позволяют дополнительно кодировать данные:

const colorScale = d3.scaleSequential(d3.interpolateOrRd)
  .domain([0, d3.max(data, d => d.value)]);

Применение:

.attr("fill", d => colorScale(d.value))

Цветовая шкала усиливает различимость значений при плотных распределениях.


Комбинированная визуализация: точки и плотность

В сложных случаях точки комбинируются с heatmap-подобным представлением:

  • крупные значения отображаются пузырьками
  • плотные области дополняются прозрачными слоями
  • малые значения агрегируются

Такой подход снижает визуальный шум и улучшает читаемость распределений данных на карте.