javascript

Создание интерактивных карт с D3.js и Leaflet: Визуализация объектов и графов

  • суббота, 3 мая 2025 г. в 00:00:09
https://habr.com/ru/articles/906414/

Комбинация библиотек D3.js и Leaflet предоставляет мощный инструментарий для создания интерактивных географических визуализаций. Leaflet отвечает за отображение карт и управление слоями, а D3.js позволяет добавлять кастомные элементы, такие как маркеры, линии или сложные графы. В этой статье мы рассмотрим, как интегрировать D3.js с Leaflet для размещения объектов и построения графов на карте, центрированной в Казани, а также предоставим пример кода для практического применения.

Почему выбирают D3.js и Leaflet?

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

Использование этих библиотек вместе дает следующие преимущества:

  • Гибкость: Полный контроль над дизайном и поведением элементов на карте.

  • Интерактивность: Возможность добавлять анимации, всплывающие подсказки и обработчики событий.

  • Совместимость: Интеграция с веб‑стандартами (HTML, SVG, CSS) для создания современных приложений.

В этой статье мы сосредоточимся на карте, центрированной в Казани (координаты: [55.7963, 49.1064]), чтобы продемонстрировать, как размещать объекты и строить графы в этом регионе.

Основы интеграции D3.js и Leaflet

Интеграция D3.js с Leaflet основана на наложении SVG-слоя на карту и использовании D3.js для управления этим слоем. Ключевые шаги включают:

  1. Создание карты Leaflet: Инициализация карты с центром в Казани и добавление базового слоя (например, OpenStreetMap).

  2. Добавление SVG‑слоя: D3.js создает SVG‑контейнер, который синхронизируется с картой.

  3. Преобразование координат: Метод latLngToLayerPoint преобразует географические координаты (широта, долгота) в пиксельные координаты для SVG.

  4. Обновление при взаимодействии: Синхронизация SVG‑элементов с картой при масштабировании или перемещении.

Пример: Построение графа на карте, центрированной в Казани

Ниже приведен пример кода, который создает карту, центрированную в Казани, и отображает граф, состоящий из узлов (городов) и связей между ними. Узлы представляют Казань и несколько европейских городов, а связи показывают маршруты между ними.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D3.js and Leaflet: Graph on Map Centered in Kazan</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <style>
        #map { height: 500px; }
        .node { cursor: pointer; }
        .link { stroke: #333; stroke-width: 2px; }
        .tooltip { position: absolute; background: white; padding: 5px; border: 1px solid #ccc; border-radius: 3px; pointer-events: none; }
    </style>
</head>
<body>
    <div id="map"></div>

    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
        // Инициализация карты, центрированной на Казани
        const map = L.map('map').setView([55.7963, 49.1064], 5);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap contributors'
        }).addTo(map);

        // Данные: узлы (города) и связи
        const nodes = [
            { id: "Kazan", lat: 55.7963, lng: 49.1064 },
            { id: "Moscow", lat: 55.7558, lng: 37.6173 },
            { id: "Berlin", lat: 52.5200, lng: 13.4050 }
        ];
        const links = [
            { source: "Kazan", target: "Moscow" },
            { source: "Moscow", target: "Berlin" }
        ];

        // Создаем SVG-слой
        const svgLayer = L.svg();
        svgLayer.addTo(map);
        const svg = d3.select("#map").select("svg");
        const g = svg.select("g");

        // Создаем элемент для всплывающей подсказки
        const tooltip = d3.select("body").append("div").attr("class", "tooltip").style("opacity", 0);

        // Функция обновления графа
        function update() {
            // Отрисовка связей
            g.selectAll(".link")
                .data(links)
                .join("line")
                .attr("class", "link")
                .attr("x1", d => {
                    const sourceNode = nodes.find(n => n.id === d.source);
                    return map.latLngToLayerPoint([sourceNode.lat, sourceNode.lng]).x;
                })
                .attr("y1", d => {
                    const sourceNode = nodes.find(n => n.id === d.source);
                    return map.latLngToLayerPoint([sourceNode.lat, sourceNode.lng]).y;
                })
                .attr("x2", d => {
                    const targetNode = nodes.find(n => n.id === d.target);
                    return map.latLngToLayerPoint([targetNode.lat, targetNode.lng]).x;
                })
                .attr("y2", d => {
                    const targetNode = nodes.find(n => n.id === d.target);
                    return map.latLngToLayerPoint([targetNode.lat, targetNode.lng]).y;
                });

            // Отрисовка узлов
            g.selectAll(".node")
                .data(nodes)
                .join("circle")
                .attr("class", "node")
                .attr("cx", d => map.latLngToLayerPoint([d.lat, d.lng]).x)
                .attr("cy", d => map.latLngToLayerPoint([d.lat, d.lng]).y)
                .attr("r", 6)
                .attr("fill", d => d.id === "Kazan" ? "green" : "red")
                .on("mouseover", function (event, d) {
                    d3.select(this).attr("r", 8);
                    tooltip.transition().duration(200).style("opacity", 0.9);
                    tooltip.html(d.id)
                        .style("left", (event.pageX + 10) + "px")
                        .style("top", (event.pageY - 28) + "px");
                })
                .on("mouseout", function () {
                    d3.select(this).attr("r", 6);
                    tooltip.transition().duration(500).style("opacity", 0);
                });
        }

        // Обновляем при загрузке и взаимодействии
        update();
        map.on("moveend", update);
    </script>
</body>
</html>

Как работает код

1.     Инициализация карты:

  • Карта создается с помощью Leaflet и центрируется в Казани ([55.7963, 49.1064]) с уровнем масштаба 5, чтобы показать регион вокруг города.

  • Используется тайловый слой OpenStreetMap для базовой карты.

2.     Данные:

  • Массив nodes содержит три города: Казань, Москва и Берлин с их географическими координатами.

  • Массив links определяет связи между городами (Казань–Москва, Москва–Берлин).

3.     SVG-слой:

  • D3.js создает SVG-элементы (линии для связей и круги для узлов) на слое, наложенном на карту.

  • Координаты узлов преобразуются из географических в пиксельные с помощью map.latLngToLayerPoint.

4.     Интерактивность:

  • Узлы подсвечиваются при наведении (увеличиваются и меняют размер).

  • Всплывающая подсказка (tooltip) показывает название города при наведении на узел.

  • Казань выделена зеленым цветом, чтобы подчеркнуть ее центральное положение.

5.     Обновление:

  • Функция update вызывается при загрузке карты и при любом перемещении/масштабировании, чтобы синхронизировать позиции SVG-элементов.

Возможности и улучшения

Этот пример можно расширить для различных задач:

  • Добавление данных: Включите больше городов или динамически загружайте данные из JSON/CSV.

  • Кастомные маркеры: Замените круги на изображения или сложные SVG‑иконки.

  • Анимации: Используйте D3.js для анимации появления узлов или связей.

  • Фильтрация: Добавьте возможность фильтровать узлы или связи по критериям (например, только связи с Казанью).

  • Производительность: Для больших графов используйте Canvas вместо SVG, чтобы снизить нагрузку на браузер.

Области применения

Такие визуализации полезны в следующих областях:

  • Транспорт и логистика: Отображение маршрутов между городами или транспортных узлов.

  • Социальные сети: Визуализация связей между пользователями в разных регионах.

  • Туризм: Показ достопримечательностей Казани и маршрутов к ним.

  • Аналитика: Анализ географических данных, например, торговых путей или телекоммуникационных сетей.

Преимущества и ограничения

Преимущества:

  • Полная кастомизация визуализаций благодаря D3.js.

  • Простота интеграции с Leaflet для географического контекста.

  • Поддержка интерактивности и динамических данных.

Ограничения:

  • Требует знаний JavaScript, SVG и основ работы с картами.

  • Может быть сложным для новичков из‑за необходимости синхронизации координат.

  • Производительность может снижаться при большом количестве элементов.

Заключение

Интеграция D3.js с Leaflet открывает широкие возможности для создания интерактивных карт, которые могут включать как простые маркеры, так и сложные графы. Центрирование карты в Казани делает визуализацию актуальной для локальных проектов, таких как анализ инфраструктуры или туристических маршрутов. Приведенный код — это отправная точка, которую вы можете адаптировать под свои задачи, добавляя новые данные, стили или функции.

Для дальнейшего изучения обратитесь к документации D3.js (d3js.org) и Leaflet (leafletjs.com). Экспериментируйте с данными, пробуйте новые типы визуализаций и создавайте карты, которые оживят ваши идеи!