Maîtriser la Visualisation de Données Interactives avec D3.js et les Technologies Web
Maîtriser la Visualisation de Données Interactives avec D3.js et les Technologies Web

Visualisation de Données Géospatiales et Cartographie avec D3.js

Contexte du cours : Maîtriser la Visualisation de Données Interactives avec D3.js et les Technologies Web

Introduction

La visualisation de données géospatiales, ou cartographie, est un domaine puissant qui nous permet de comprendre et d'analyser des phénomènes complexes liés à des localisations géographiques. Qu'il s'agisse de visualiser la densité de population, la propagation d'une épidémie, les schémas de trafic ou les données climatiques, une carte interactive offre une perspective spatiale inégalée.

D3.js (Data-Driven Documents) est une bibliothèque JavaScript qui excelle dans la manipulation de documents basés sur les données. Bien qu'il ne soit pas une bibliothèque cartographique prête à l'emploi comme Leaflet ou Mapbox GL JS, sa flexibilité et son contrôle de bas niveau en font un outil incroyablement puissant pour créer des cartes personnalisées, des visualisations choroplèthes, des cartes de densité de points et bien plus encore, directement dans un navigateur web en utilisant les standards SVG et Canvas.

Dans cette leçon, nous allons explorer les concepts fondamentaux de la visualisation de données géospatiales avec D3.js, en couvrant le chargement des données, les projections cartographiques, le dessin des géométries et les bases de l'interactivité.

Prérequis

Pour tirer le meilleur parti de cette leçon, une compréhension de base des éléments suivants est recommandée :

  • HTML, CSS, JavaScript : Les fondations du développement web.
  • D3.js : Connaissance des concepts de base de D3.js (sélections, liaisons de données, échelles, etc.).
  • Concepts SIG (optionnel) : Une familiarité avec les systèmes d'information géographique est un plus, mais pas obligatoire.

Concepts Fondamentaux de la Cartographie avec D3.js

Avant de plonger dans le code, il est essentiel de comprendre certains concepts clés.

1. Données Géospatiales : GeoJSON et TopoJSON

Les données géospatiales sont des informations qui décrivent l'emplacement et les caractéristiques des entités géographiques.

  • GeoJSON : C'est le format le plus courant pour représenter des structures de données géographiques simples. Il est basé sur JSON et peut représenter des points, des lignes, des polygones, des multi-géométries, et des collections de caractéristiques (FeatureCollection). Chaque "Feature" contient une propriété geometry (décrivant la forme) et une propriété properties (contenant des attributs non géométriques comme le nom du pays, sa population, etc.).

    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [102.0, 0.5]
      },
      "properties": {
        "name": "Un point exemple"
      }
    }
    
  • TopoJSON : Une extension de GeoJSON qui encode la topologie des données. Cela signifie que les frontières partagées entre deux polygones (par exemple, deux pays adjacents) ne sont stockées qu'une seule fois. Cela réduit considérablement la taille du fichier et garantit que les frontières sont parfaitement alignées. D3.js inclut des utilitaires pour travailler avec TopoJSON. Pour convertir TopoJSON en GeoJSON, on utilise topojson.feature().

2. Projections Cartographiques

La Terre est une sphère (ou plutôt un géoïde), et nos écrans sont plats. Une projection cartographique est une méthode mathématique pour transformer les coordonnées tridimensionnelles (latitude, longitude) d'un point sur la surface de la Terre en coordonnées bidimensionnelles (x, y) sur un plan.

D3.js fournit une riche collection de projections cartographiques :

  • d3.geoMercator() : Très courante, préserve les angles mais déforme les aires, surtout près des pôles.
  • d3.geoAlbers() : Une projection conique équivalente, souvent utilisée pour les États-Unis car elle minimise la distorsion pour les pays/régions de taille modérée le long de parallèles spécifiques.
  • d3.geoNaturalEarth1() : Une projection pseudo-cylindrique qui est un bon compromis entre la forme et la surface des continents.

Chaque projection peut être configurée avec des paramètres tels que le centre (.center()), l'échelle (.scale()) et la translation (.translate()) pour positionner et dimensionner la carte sur l'écran.

3. Générateur de Chemin Géographique (d3.geoPath)

Une fois que vous avez une projection, vous avez besoin d'un moyen de "dessiner" les géométries GeoJSON sur votre élément SVG. C'est le rôle de d3.geoPath().

  • Il prend une projection en entrée (d3.geoPath().projection(maProjection)).
  • Il convertit les objets géométriques GeoJSON en chaînes de caractères path SVG (<path d="..." />).
  • Vous pouvez ensuite utiliser ces chaînes d comme attribut pour vos éléments <path> dans le SVG.
const projection = d3.geoMercator()
    .scale(100)
    .center([0, 0])
    .translate([width / 2, height / 2]);

const pathGenerator = d3.geoPath()
    .projection(projection);

// Plus tard, dans votre code de dessin :
svg.append("path")
    .attr("d", pathGenerator(geojsonData));

Mise en Pratique : Créer une Carte Simple avec D3.js

Nous allons créer une carte du monde simple en utilisant un fichier GeoJSON des pays.

1. Structure HTML

Créez un fichier index.html avec la structure de base suivante :

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Carte du Monde avec D3.js</title>
    <style>
        body { margin: 0; overflow: hidden; font-family: sans-serif; }
        .country {
            fill: #ccc; /* Couleur de remplissage par défaut pour les pays */
            stroke: #fff; /* Couleur de la bordure des pays */
            stroke-width: 0.5px;
        }
        .tooltip {
            position: absolute;
            text-align: center;
            padding: 8px;
            font: 12px sans-serif;
            background: lightsteelblue;
            border: 0px;
            border-radius: 8px;
            pointer-events: none; /* Important pour que la souris interagisse avec les éléments SVG en dessous */
            opacity: 0; /* Caché par défaut */
        }
    </style>
</head>
<body>
    <h1>Carte Interactive des Pays avec D3.js</h1>
    <div id="map-container"></div>

    <!-- Inclure la bibliothèque D3.js -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- Si vous utilisez TopoJSON et voulez le convertir en GeoJSON côté client -->
    <script src="https://unpkg.com/topojson-client@3"></script>
    <script src="app.js"></script>
</body>
</html>

2. Le Fichier JavaScript (app.js)

Nous allons maintenant écrire le code JavaScript pour charger les données, configurer la carte et la dessiner.

Nous utiliserons un fichier GeoJSON de pays (par exemple, countries.geojson). Pour cet exemple, vous pouvez télécharger un fichier GeoJSON de pays simplifié depuis des sources comme Natural Earth. Placez-le dans le même répertoire que index.html et app.js.

Note : Le chemin du fichier countries.geojson dans l'exemple ci-dessous est relatif. Assurez-vous que le fichier est accessible.

// app.js

// 1. Configuration de la carte
const width = window.innerWidth;
const height = window.innerHeight * 0.8; // Utilise 80% de la hauteur de la fenêtre

// Crée le conteneur SVG
const svg = d3.select("#map-container")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

// Crée un groupe pour les éléments de la carte afin de faciliter le zoom et le pan
const g = svg.append("g");

// 2. Définition de la projection cartographique
// Nous utilisons d3.geoNaturalEarth1 car c'est une projection globalement équilibrée.
const projection = d3.geoNaturalEarth1()
    .scale(width / 2 / Math.PI) // Échelle pour que la carte remplisse la largeur
    .center([0, 0]) // Centre la carte sur le méridien de Greenwich et l'équateur
    .translate([width / 2, height / 2]); // Translate la carte au centre du SVG

// 3. Création du générateur de chemin (path generator)
const pathGenerator = d3.geoPath()
    .projection(projection);

// 4. Création du tooltip
const tooltip = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);

// 5. Chargement des données géospatiales
d3.json("countries.geojson").then(data => {
    // Si le fichier était en TopoJSON, on le convertirait comme ceci :
    // const countries = topojson.feature(data, data.objects.countries); // 'countries' dépend du nom de l'objet dans votre TopoJSON
    const countries = data; // Si c'est déjà du GeoJSON

    // 6. Dessin des pays
    g.selectAll(".country")
        .data(countries.features)
        .enter()
        .append("path")
        .attr("class", "country")
        .attr("d", pathGenerator)
        .on("mouseover", function(event, d) {
            d3.select(this)
                .transition()
                .duration(100)
                .style("fill", "#007bff"); // Change la couleur au survol

            tooltip.transition()
                .duration(200)
                .style("opacity", .9);
            tooltip.html("Pays: <strong>" + d.properties.name + "</strong>") // Assurez-vous que 'name' existe dans les propriétés de votre GeoJSON
                .style("left", (event.pageX + 10) + "px")
                .style("top", (event.pageY - 28) + "px");
        })
        .on("mouseout", function(event, d) {
            d3.select(this)
                .transition()
                .duration(100)
                .style("fill", "#ccc"); // Revert à la couleur par défaut

            tooltip.transition()
                .duration(500)
                .style("opacity", 0);
        });

    // 7. Ajout du zoom et du pan
    const zoom = d3.zoom()
        .scaleExtent([1, 8]) // Limite le niveau de zoom (1x à 8x)
        .on("zoom", (event) => {
            g.attr("transform", event.transform); // Applique la transformation (zoom/pan) au groupe
        });

    svg.call(zoom); // Applique le comportement de zoom au SVG
}).catch(error => {
    console.error("Erreur lors du chargement ou du traitement des données GeoJSON :", error);
});

Explication du code :

  1. Configuration SVG : Nous définissons la largeur et la hauteur de notre conteneur SVG et l'ajoutons à notre div#map-container. Un groupe <g> est créé à l'intérieur du SVG pour contenir tous les chemins des pays. C'est sur ce groupe que nous appliquerons les transformations de zoom et de pan.
  2. Projection : Nous choisissons la projection d3.geoNaturalEarth1(). L'échelle et la translation sont ajustées pour centrer la carte et l'adapter à la taille du SVG.
  3. Générateur de chemin : d3.geoPath() est instancié avec notre projection. Il sera responsable de convertir les coordonnées géographiques en chemins SVG d.
  4. Chargement des données : d3.json("countries.geojson") est utilisé pour charger le fichier GeoJSON. Une fois les données chargées, la fonction .then() est exécutée.
  5. Dessin des pays :
    • g.selectAll(".country").data(countries.features) sélectionne tous les éléments avec la classe country (qui n'existent pas encore) et lie les données des caractéristiques GeoJSON (chaque pays est une caractéristique).
    • .enter().append("path") crée un nouvel élément <path> pour chaque caractéristique.
    • .attr("class", "country") ajoute la classe CSS.
    • .attr("d", pathGenerator) utilise le générateur de chemin pour obtenir la chaîne d pour chaque pays. C'est ici que la magie de la projection et du dessin se produit.
  6. Interactions de survol (mouseover/mouseout) : Des écouteurs d'événements sont ajoutés à chaque pays pour changer sa couleur au survol et afficher un tooltip avec le nom du pays. Le tooltip est un élément div HTML positionné absolument, dont l'opacité et la position sont gérées par D3.
  7. Zoom et Pan :
    • d3.zoom() crée un comportement de zoom.
    • .scaleExtent([1, 8]) limite le zoom à un facteur entre 1 et 8.
    • .on("zoom", (event) => { g.attr("transform", event.transform); }) attache une fonction qui est appelée à chaque événement de zoom ou de pan. event.transform contient les informations de translation et de zoom actuelles, que nous appliquons directement au transform du groupe g.
    • svg.call(zoom) applique ce comportement de zoom au conteneur SVG.

Approfondissements : Choroplèthes et Données Externes

Une carte choroplèthe est une carte thématique dans laquelle les régions sont colorées ou ombrées en proportion d'une quantité statistique d'un agrégat géographique donné.

Pour créer une carte choroplèthe, vous devez :

  1. Charger des données statistiques externes : Souvent un fichier CSV ou JSON. Ces données contiennent des valeurs numériques associées à des identifiants de région (par exemple, population par pays, PIB par région).
  2. Joindre les données statistiques aux données géospatiales : Il faut faire correspondre les identifiants entre votre GeoJSON et votre fichier de données statistiques.
  3. Définir une échelle de couleur : Utiliser d3.scaleQuantize(), d3.scaleThreshold() ou d3.scaleSequential() pour mapper les valeurs numériques aux couleurs.

Exemple de logique pour une carte choroplèthe (à intégrer au code précédent) :

Supposons que vous ayez un fichier data.csv avec la population par pays :

country_id,population
USA,331000000
CAN,38000000
MEX,128000000
...

Et que votre GeoJSON a une propriété id ou name qui correspond.

// ... (code précédent de configuration de la carte)

// Chargement des données statistiques et géospatiales en parallèle
Promise.all([
    d3.json("countries.geojson"),
    d3.csv("data.csv") // Supposons que c'est votre fichier de données statistiques
]).then(([geoData, populationData]) => {
    const countries = geoData;

    // Convertir les données de population en un objet Map pour un accès rapide
    const populationMap = new Map();
    populationData.forEach(d => {
        populationMap.set(d.country_id, +d.population); // + pour convertir en nombre
    });

    // Définir une échelle de couleur pour la population
    // Trouver les valeurs min/max de population pour l'échelle
    const minPop = d3.min(Array.from(populationMap.values()));
    const maxPop = d3.max(Array.from(populationMap.values()));

    const colorScale = d3.scaleSequential(d3.interpolateBlues) // Utilise une interpolation de couleur bleue
        .domain([minPop, maxPop]); // Le domaine de l'échelle est la plage des populations

    // Dessin des pays avec la couleur choroplèthe
    g.selectAll(".country")
        .data(countries.features)
        .enter()
        .append("path")
        .attr("class", "country")
        .attr("d", pathGenerator)
        .attr("fill", d => {
            // Récupérer la population pour ce pays
            const pop = populationMap.get(d.properties.iso_a2); // Assurez-vous que 'iso_a2' correspond à 'country_id'
            return pop ? colorScale(pop) : "#ccc"; // Si la donnée existe, applique la couleur, sinon gris par défaut
        })
        .on("mouseover", function(event, d) {
            d3.select(this)
                .transition()
                .duration(100)
                .style("stroke", "black") // Bordure noire au survol
                .style("stroke-width", "1.5px");

            const pop = populationMap.get(d.properties.iso_a2);
            tooltip.transition()
                .duration(200)
                .style("opacity", .9);
            tooltip.html(`Pays: <strong>${d.properties.name}</strong><br/>Population: <strong>${pop ? pop.toLocaleString() : 'N/A'}</strong>`)
                .style("left", (event.pageX + 10) + "px")
                .style("top", (event.pageY - 28) + "px");
        })
        .on("mouseout", function(event, d) {
            d3.select(this)
                .transition()
                .duration(100)
                .style("stroke", "#fff") // Revert bordure
                .style("stroke-width", "0.5px");

            tooltip.transition()
                .duration(500)
                .style("opacity", 0);
        });

    // ... (restes de l'ajout du zoom)
}).catch(error => {
    console.error("Erreur lors du chargement ou du traitement des données :", error);
});

Notes sur la choroplèthe :

  • L'utilisation de Promise.all permet de charger plusieurs fichiers de manière asynchrone et de les traiter une fois que tous sont disponibles.
  • Assurez-vous que la clé utilisée pour joindre les données (d.properties.iso_a2 dans cet exemple) correspond à la clé de votre fichier de données statistiques (d.country_id).
  • d3.scaleSequential(d3.interpolateBlues) crée une échelle qui mappe un domaine numérique à une gamme de couleurs bleues. D'autres interpolateurs de couleur sont disponibles (e.g., d3.interpolateReds, d3.interpolateGreens, d3.interpolateWarm, etc.).
  • La gestion des données manquantes est importante (pop ? colorScale(pop) : "#ccc").

Défis et Bonnes Pratiques

  • Performance : Pour des jeux de données géospatiales très volumineux, le rendu SVG peut devenir lent. Considérez l'utilisation de Canvas avec D3 pour de meilleures performances (le d3.geoPath peut aussi générer des chemins pour un contexte Canvas). Le TopoJSON est également crucial pour la taille des fichiers.
  • Projections : Le choix de la projection est fondamental. Il dépend de la région que vous visualisez et de ce que vous voulez préserver (surface, forme, distance, direction).
  • Données manquantes : Prévoyez toujours comment gérer les régions pour lesquelles vous n'avez pas de données statistiques.
  • Légendes : Pour les cartes choroplèthes, une légende claire est indispensable pour interpréter les couleurs. D3.js offre les outils pour construire des légendes basées sur vos échelles de couleur.
  • Accessibilité : Pensez aux utilisateurs ayant des déficiences visuelles. Les couleurs seules ne doivent pas être le seul moyen d'information.

Conclusion

La visualisation de données géospatiales avec D3.js offre un contrôle inégalé sur la conception et l'interactivité de vos cartes. Bien qu'elle nécessite une compréhension plus profonde des concepts cartographiques et de D3 par rapport à des bibliothèques de cartographie de haut niveau, elle vous permet de créer des expériences visuelles uniques et très personnalisées.

Vous avez appris à :

  • Comprendre les formats GeoJSON et TopoJSON.
  • Appliquer différentes projections cartographiques.
  • Utiliser d3.geoPath() pour dessiner des géométries sur SVG.
  • Charger et visualiser des données géospatiales.
  • Intégrer des interactions de base comme le survol et le zoom/pan.
  • Les principes pour créer des cartes choroplèthes.

Maintenant, à vous d'expérimenter avec différentes projections, jeux de données et techniques d'interaction pour raconter des histoires visuelles captivantes avec vos données géospatiales !