Séance 5 — Construire et raconter avec D3

Séance 5 sur 63 heures

Question directrice

Que gagne-t-on lorsque les données ne remplissent plus un graphique déjà prévu, mais pilotent directement les éléments d’une page ?

Vous allez construire une visualisation D3 à partir des primitives du Web, comprendre les sélections, la jointure de données, les échelles et les axes, puis transformer ce graphique en récit vérifiable. L’objectif n’est pas de mémoriser une recette : c’est d’acquérir un modèle mental que vous pourrez reconnaître dans les exemples D3 les plus variés.

Point de départ : Chart.js vous a appris à configurer un graphique et à coordonner plusieurs vues. D3 vous donne moins de décisions toutes faites, mais un contrôle beaucoup plus fin sur les marques, les annotations, l’interaction et la narration.

Comment utiliser cette leçon

Cette séance comporte deux mouvements. Le premier est constructif : nous partons d’un tableau de données et produisons un document SVG. Le second est éditorial : nous choisissons ce que la personne verra d’abord, ensuite et finalement.

En direct, il est utile que l’enseignant écrive lentement le premier graphique, en montrant le DOM dans les outils de développement. Les personnes participantes peuvent prédire le résultat de chaque ligne avant de l’exécuter. La deuxième moitié devient un atelier de mise en scène : le même graphique est lu, annoté et séquencé de plusieurs façons.

Temps en directActivitéRésultat
0:00–0:20Chart.js et D3 sur le même problèmeSavoir pourquoi on change d’outil
0:20–0:55SVG, sélections et jointure de donnéesDes marques liées à des objets identifiables
0:55–1:30Échelles, axes et premier graphique completUne vue D3 lisible et redimensionnable
1:30–1:40Pause
1:40–2:10Charger et normaliser les données Banque mondialeUne série temporelle multi-pays fiable
2:10–2:35Storytelling, titres et annotationsUn argument visuel qui conserve son contexte
2:35–3:00Scènes narratives et atelierUne courte histoire pilotée par un état

1. D3 n’est pas un catalogue de graphiques

Avec Chart.js, vous fournissez une configuration : un type, des labels, des datasets et des options. La bibliothèque possède déjà une notion de « graphique en barres » ou de « graphique linéaire ». Elle calcule le dessin, gère le canvas et fournit plusieurs interactions.

D3 adopte une autre philosophie. Il fournit des outils pour :

  • sélectionner des éléments du document;
  • lier des données à ces éléments;
  • traduire des valeurs en positions, tailles et couleurs;
  • générer des axes et des formes;
  • interpoler entre deux états;
  • calculer des dispositions spécialisées.

Mais D3 ne vous oblige pas à assembler ces outils sous la forme d’un graphique conventionnel. Une donnée peut piloter la largeur d’une barre SVG, le texte d’un paragraphe, la position d’un élément HTML, la forme d’une carte ou l’état d’un bouton. C’est le sens de Data-Driven Documents : les données transforment un document Web natif.

Cette transparence est importante. Si D3 crée vingt cercles SVG, vous pouvez inspecter ces vingt cercles dans le DOM. Vous pouvez leur appliquer du CSS, leur ajouter des événements et les combiner à des titres ou à des contrôles HTML. Il n’existe pas de scène graphique opaque cachée derrière une API de haut niveau.

1.1 Construire plutôt que configurer

Comparez les deux modèles mentaux :

Avec Chart.jsAvec D3
« Je veux un graphique en barres. »« Je veux un rectangle par station. »
Les données suivent le contrat de la bibliothèque.Les éléments du document suivent les données.
Les axes et tooltips sont fournis.Les axes, labels et interactions sont composés.
Le rendu principal est un canvas.SVG, HTML et Canvas peuvent être combinés.
Personnalisation par options et plugins.Personnalisation par attributs, styles, fonctions et DOM.

Ce n’est pas une compétition. Pour un dashboard standard, Chart.js reste souvent le choix le plus économique. D3 devient intéressant lorsque la forme, la transition ou l’interaction fait partie de l’idée elle-même.


2. Le terrain de jeu : HTML, SVG et coordonnées

Nous allons dessiner en SVG. Contrairement au canvas, un SVG contient des éléments vectoriels : <rect>, <circle>, <path>, <line> et <text>. Chaque marque reste inspectable.

Ajoutez un conteneur à index.html :

<section aria-labelledby="life-title">
  <h2 id="life-title">Espérance de vie dans quatre pays</h2>
  <p id="life-summary">Chargement des données…</p>
  <div id="life-chart"></div>
  <div id="life-table"></div>
</section>

Dans src/main.js, importez D3 :

import * as d3 from "d3";

Créez ensuite une surface SVG :

const width = 900;
const height = 520;

const svg = d3
  .select("#life-chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("role", "img")
  .attr("aria-labelledby", "life-title life-summary");

Le viewBox définit un système de coordonnées interne de 900 × 520 unités. Le CSS peut ensuite afficher ce SVG à 900, 600 ou 320 pixels de largeur sans recalculer toutes les positions. Ajoutez :

#life-chart svg {
  display: block;
  width: 100%;
  height: auto;
}

2.1 Le point d’origine surprend une fois

Dans SVG, (0, 0) se trouve en haut à gauche. La coordonnée x augmente vers la droite et y augmente vers le bas. Une valeur élevée doit pourtant apparaître en haut d’un graphique. L’échelle verticale traduira donc une grande valeur de donnée vers une petite coordonnée en pixels.

données : 70 -------- 85
pixels  : 450 -------- 30

Ce renversement n’est pas un truc de D3; il vient du système de coordonnées de l’écran.

2.2 Une marge n’est pas une décoration

Les axes et leurs labels ont besoin d’espace. Utilisons la convention des marges :

const margin = { top: 36, right: 110, bottom: 48, left: 58 };

const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

const plot = svg
  .append("g")
  .attr("transform", `translate(${margin.left}, ${margin.top})`);

Le groupe plot crée un nouveau repère. À l’intérieur, (0, 0) correspond désormais au coin supérieur gauche de la zone de données, pas de la page entière. La marge de droite est plus grande parce que nous voulons étiqueter directement les lignes à leur extrémité.


3. Les sélections : désigner avant de transformer

Une sélection D3 est un ensemble d’éléments du document auquel on applique des opérations. Elle ressemble à une sélection CSS, mais elle transporte aussi les données liées aux éléments.

d3.select("#life-title")
  .style("color", "currentColor")
  .text("L’espérance de vie a suivi des trajectoires différentes");

select prend le premier élément correspondant. selectAll prend tous les éléments correspondants dans un parent :

plot.selectAll("circle");

Au premier rendu, cette sélection est vide. C’est intentionnel. Nous demandons ensuite à D3 de faire correspondre cette sélection aux données.

3.1 La jointure est une comparaison

Supposons ce tableau :

const stations = [
  { id: "A", departures: 22 },
  { id: "B", departures: 38 },
  { id: "C", departures: 61 },
];

Le pattern moderne est :

plot
  .selectAll("circle.station")
  .data(stations, (d) => d.id)
  .join("circle")
  .attr("class", "station")
  .attr("cx", (d) => d.departures * 5)
  .attr("cy", (_, index) => 40 + index * 45)
  .attr("r", 8);

Lisez-le comme une phrase :

Dans plot, prends tous les cercles de classe station; compare-les au tableau stations selon leur id; crée ou retire les cercles nécessaires; puis calcule leurs attributs à partir de la donnée liée.

Le callback reçoit généralement :

  • d, la donnée liée;
  • i, l’index courant;
  • parfois le tableau de nœuds du groupe.

Le nom d est une convention très répandue dans les exemples D3. Vous pouvez écrire station pour rendre le code plus explicite :

.attr("cx", (station) => station.departures * 5)

3.2 Entrer, mettre à jour, sortir

La jointure distingue trois ensembles :

  • enter : une donnée existe, mais aucun élément ne lui correspond encore;
  • update : une donnée et un élément existant partagent la même identité;
  • exit : un élément existe encore, mais sa donnée a disparu du nouveau tableau.

Le raccourci .join("circle") ajoute les éléments entrants, conserve ceux à mettre à jour et retire les sortants. Pour traiter les trois chemins différemment :

plot
  .selectAll("circle.station")
  .data(stations, (d) => d.id)
  .join(
    (enter) => enter
      .append("circle")
      .attr("class", "station")
      .attr("r", 0),
    (update) => update,
    (exit) => exit.remove()
  )
  .attr("cx", (d) => x(d.departures))
  .attr("cy", (d) => y(d.id))
  .attr("r", 8);

Nous animerons ces chemins à la séance 6. Pour le moment, la structure importante est celle-ci : la création et la mise à jour sont deux moments différents.

3.3 La clé est une affirmation sur l’identité

Sans fonction-clé, D3 associe les données et les éléments par index. Si le premier objet disparaît, tous les objets suivants peuvent recevoir une nouvelle identité visuelle. Avec (d) => d.id, un cercle lié à la station B reste celui de la station B, même si B change de position dans le tableau.

Une bonne clé est :

  • stable dans le temps;
  • unique dans le groupe joint;
  • issue du sens des données, pas de la mise en page;
  • disponible avant et après la transformation.

Un numéro de station, un code de pays ou un identifiant de produit conviennent. L’index convient seulement lorsque l’ordre est l’identité et que les éléments ne seront jamais insérés, retirés ou réordonnés.


4. Les échelles : traduire sans perdre la structure

Multiplier une valeur par cinq fonctionne pour une démonstration, mais ne résiste pas à un changement de dataset ou de largeur. Une échelle est une fonction qui traduit un domaine de données vers une plage visuelle.

Pour une position quantitative :

const x = d3
  .scaleLinear()
  .domain([0, 120])
  .range([0, innerWidth]);

x(0);   // 0
x(60);  // environ la moitié de innerWidth
x(120); // innerWidth

Le domaine répond à « quelles valeurs de données la fonction doit-elle accepter ? ». La plage répond à « quelles valeurs visuelles doit-elle produire ? ».

Cette séparation est un pivot intellectuel de D3. Les données ne connaissent pas les pixels. Le dessin ne doit pas contenir partout des calculs ad hoc sur les données. L’échelle devient le contrat entre les deux.

4.1 Domaine observé, domaine significatif

D3 peut calculer un minimum et un maximum :

const domain = d3.extent(rows, (d) => d.lifeExpectancy);

Mais un domaine automatique n’est pas toujours un domaine honnête ou utile. Pour des barres qui encodent une longueur, commencer à zéro est généralement nécessaire. Pour une ligne d’espérance de vie variant entre 70 et 83 ans, un axe de 0 à 85 écraserait presque toute la variation. Un domaine resserré peut être approprié si le titre, les ticks et le contexte rendent l’amplitude explicite.

Autrement dit, d3.extent calcule une réponse mathématique; il ne prend pas la décision éditoriale.

const y = d3
  .scaleLinear()
  .domain([68, 84])
  .nice()
  .range([innerHeight, 0]);

.nice() étend légèrement le domaine vers des bornes faciles à lire. .clamp(true) peut empêcher les valeurs hors domaine de dépasser la plage, mais masque aussi leur dépassement. Utilisez-le seulement si l’interface explique ce qui arrive aux valeurs extrêmes.

4.2 Une échelle par structure de variable

StructureÉchelle D3 fréquenteExemple
Quantitative continuescaleLineartempérature → position verticale
Quantitative très étenduescaleLog ou scaleSymlogpopulation → position
Temporelle localescaleTimedate → position horizontale
Temporelle stable entre fuseauxscaleUtctimestamp UTC → position
Catégorielle avec largeurscaleBandstation → position et largeur de barre
Catégorielle sans largeurscalePointcatégorie → position d’un point
Catégorielle vers une couleurscaleOrdinalpays → couleur
Quantitative vers une couleurscaleSequentialdensité → teinte/luminance

Pour des barres :

const y = d3
  .scaleBand()
  .domain(stations.map((d) => d.name))
  .range([0, innerHeight])
  .padding(0.18);

y("Métro Mont-Royal"); // début de la bande
y.bandwidth();         // hauteur disponible pour chaque barre

Pour le temps, D3 sait proposer des ticks calendaires. La documentation recommande scaleUtc lorsque les données ne dépendent pas de l’heure locale : les jours y ont une durée prévisible et le navigateur ne réinterprète pas le domaine selon son fuseau.

4.3 Une échelle peut aussi être interrogée

Les échelles continues offrent souvent .invert(). Si x(date) traduit une date en pixel, x.invert(pixel) retrouve la date associée à une position du pointeur. C’est la base de plusieurs interactions : tooltip, brush, sélection temporelle.


5. Les axes rendent une échelle visible

Une échelle est une fonction invisible. Un axe montre une partie de son domaine, ses ticks et son formatage.

const xAxis = d3
  .axisBottom(x)
  .ticks(d3.utcYear.every(4))
  .tickFormat(d3.utcFormat("%Y"));

plot
  .append("g")
  .attr("transform", `translate(0, ${innerHeight})`)
  .call(xAxis);

.call(xAxis) passe la sélection au générateur d’axe. Le générateur ajoute des lignes et des textes SVG. Vous pouvez ensuite sélectionner certains éléments produits :

plot.selectAll(".domain").remove();

plot
  .selectAll(".tick line")
  .attr("stroke", "currentColor")
  .attr("opacity", 0.15);

Retirer la ligne extérieure de l’axe peut alléger le dessin, mais ne supprimez pas les repères dont dépend la tâche. Une personne qui doit estimer une différence d’un an a besoin de plus de structure qu’une personne qui cherche seulement la tendance générale.

5.1 Formater pour l’humain

Les données peuvent contenir 2023-01-01T00:00:00Z, 816263414634146 ou un ratio 0.274. Le graphique doit afficher des dates et nombres qui correspondent au contexte :

const formatYear = d3.utcFormat("%Y");
const formatYears = d3.format(".1f");
const formatPercent = d3.format(".0%");

Le formatage ne doit pas créer une fausse précision. Si l’indicateur est une estimation annuelle, afficher six décimales donne une impression de mesure que le phénomène ne possède pas nécessairement.


6. Exemple fil rouge : espérance de vie et rupture récente

Nous allons utiliser l’indicateur SP.DYN.LE00.IN de la Banque mondiale pour le Canada, la France, le Mexique et les États-Unis, de 2000 à 2023. L’API renvoie des observations en ordre antichronologique et peut contenir des valeurs nulles. Avant de dessiner, nous devons normaliser.

const WORLD_BANK_URL =
  "https://api.worldbank.org/v2/country/" +
  "CAN;USA;FRA;MEX/indicator/SP.DYN.LE00.IN" +
  "?format=json&date=2000:2023&per_page=200";

async function loadLifeExpectancy() {
  const response = await fetch(WORLD_BANK_URL);

  if (!response.ok) {
    throw new Error(`Banque mondiale : HTTP ${response.status}`);
  }

  const payload = await response.json();
  const observations = payload?.[1];

  if (!Array.isArray(observations)) {
    throw new Error("Structure de réponse inattendue.");
  }

  return observations
    .filter((row) => row.value !== null)
    .map((row) => ({
      country: row.country.value,
      code: row.countryiso3code,
      date: new Date(`${row.date}-01-01T00:00:00Z`),
      year: Number(row.date),
      lifeExpectancy: Number(row.value),
    }))
    .filter((row) =>
      Number.isFinite(row.lifeExpectancy) &&
      !Number.isNaN(row.date.getTime())
    )
    .sort((a, b) => d3.ascending(a.date, b.date));
}

6.1 Vérifier avant de dessiner

const rows = await loadLifeExpectancy();

console.table(rows.slice(0, 8));
console.log("Pays", new Set(rows.map((d) => d.code)));
console.log("Période", d3.extent(rows, (d) => d.year));
console.log("Valeurs", d3.extent(rows, (d) => d.lifeExpectancy));

Nous attendons quatre codes pays, des années entre 2000 et 2023 et des valeurs plausibles. Cette vérification est plus importante qu’un choix de courbe. Une visualisation élégante d’un tableau mal interprété reste fausse.

Regroupons les observations par pays :

const series = d3.groups(rows, (d) => d.code).map(([code, values]) => ({
  code,
  country: values[0].country,
  values,
}));

d3.groups produit ici un tableau de paires [clé, valeurs]. Nous le transformons en objets explicites pour que le code de rendu reste lisible.

6.2 Construire les échelles

const x = d3
  .scaleUtc()
  .domain(d3.extent(rows, (d) => d.date))
  .range([0, innerWidth]);

const y = d3
  .scaleLinear()
  .domain([68, 84])
  .nice()
  .range([innerHeight, 0]);

const color = d3
  .scaleOrdinal()
  .domain(series.map((d) => d.code))
  .range(["#0072b2", "#d55e00", "#009e73", "#7a5195"]);

Les couleurs servent ici à distinguer quatre séries. Elles ne doivent pas être le seul moyen d’identification : nous ajouterons le nom du pays au bout de chaque ligne.

6.3 Générer les chemins

Un élément <path> reçoit une chaîne dans son attribut d. Le générateur d3.line transforme une suite d’observations en cette chaîne :

const line = d3
  .line()
  .defined((d) => Number.isFinite(d.lifeExpectancy))
  .x((d) => x(d.date))
  .y((d) => y(d.lifeExpectancy));

Puis une jointure crée une ligne par pays :

plot
  .selectAll("path.country-line")
  .data(series, (d) => d.code)
  .join("path")
  .attr("class", "country-line")
  .attr("fill", "none")
  .attr("stroke", (d) => color(d.code))
  .attr("stroke-width", 2.5)
  .attr("d", (d) => line(d.values));

Remarquez les deux niveaux de données. Chaque path reçoit un objet série. Le générateur line parcourt ensuite les observations contenues dans series.values. D3 permet ainsi de lier un élément à un objet complexe, pas seulement à un nombre.

6.4 Étiqueter directement

plot
  .selectAll("text.country-label")
  .data(series, (d) => d.code)
  .join("text")
  .attr("class", "country-label")
  .attr("x", (d) => x(d.values.at(-1).date) + 8)
  .attr("y", (d) => y(d.values.at(-1).lifeExpectancy))
  .attr("dominant-baseline", "middle")
  .attr("fill", (d) => color(d.code))
  .text((d) => d.country);

Une légende isolée oblige à alterner entre le code couleur et les lignes. L’étiquette directe réduit cette recherche. Si deux labels se chevauchent, il faut cependant résoudre le placement : décaler légèrement, ajouter une ligne d’appel ou limiter le nombre de séries. Le code court ne doit pas cacher le problème visuel.

6.5 Ce que le graphique permet réellement d’affirmer

Le graphique montre des trajectoires descriptives : hausse à long terme dans plusieurs pays, recul autour de 2020–2021 et écarts de niveau. Il ne prouve pas la cause de ces changements. Pour écrire un titre causal, il faudrait mobiliser d’autres sources et une méthode adaptée.

Un titre prudent pourrait être :

Après une progression de long terme, l’espérance de vie recule nettement autour de 2020 dans les quatre pays observés.

Un titre trop fort serait :

La pandémie explique tous les écarts d’espérance de vie entre ces pays.

La visualisation est une pièce de preuve; elle n’est pas toute l’enquête.

💡

Une API n’est pas un fichier immuable

La Banque mondiale peut réviser ses séries. Conservez le code de l’indicateur, la date de consultation et, pour un livrable durable, une copie de l’extrait utilisé. Une visualisation reproductible doit pouvoir expliquer quelle version des données elle a représentée.


7. Une fonction de rendu, pas un script qui s’étale

Un premier graphique D3 est souvent écrit de haut en bas. Cela aide à comprendre, mais devient difficile à faire évoluer. Organisons le travail :

function renderLifeChart(container, rows, options = {}) {
  const {
    width = 900,
    height = 520,
    highlightedCodes = [],
    yDomain = [68, 84],
  } = options;

  // 1. dériver les séries
  // 2. calculer les dimensions et les échelles
  // 3. joindre les axes
  // 4. joindre les marques
  // 5. joindre les labels et annotations
  // 6. mettre à jour le résumé textuel
}

Les options ne doivent pas devenir une nouvelle bibliothèque de configuration. Elles expriment seulement les dimensions qui varient entre nos scènes narratives.

Une architecture utile garde quatre responsabilités distinctes :

charger → normaliser → dériver → rendre
  • charger connaît l’URL et le protocole;
  • normaliser connaît le schéma externe;
  • dériver répond à une question analytique;
  • rendre connaît les marques et les échelles.

Cette séparation permet de tester la transformation sans navigateur et de remplacer l’API sans réécrire le dessin.


8. Storytelling : organiser l’attention dans le temps

Un graphique exploratoire aide une personne à poser plusieurs questions. Un graphique explicatif soutient un message déterminé. Le storytelling de données commence lorsqu’on organise une suite de preuves, de contexte et de transitions afin d’aider une audience à construire une interprétation.

Il ne s’agit pas d’ajouter une anecdote au-dessus d’un graphique. Un récit de données comporte au minimum :

  1. une question ou une tension;
  2. une preuve visible;
  3. un contexte qui permet de la juger;
  4. une progression;
  5. une conclusion proportionnée aux données.

8.1 Autorité de l’auteur, liberté du lecteur

Segel et Heer ont étudié des visualisations narratives et décrit plusieurs façons de combiner une séquence guidée à l’exploration. Leur structure dite martini glass commence par un passage étroit, guidé par l’auteur, puis s’ouvre sur une exploration plus libre. Le slideshow interactif progresse par étapes. Le drill-down story commence par une vue d’ensemble et laisse le lecteur choisir les branches à approfondir.

Ces structures posent une question éthique : qui contrôle le chemin ? Trop de guidage transforme la visualisation en démonstration fermée. Trop de liberté peut laisser une personne sans point d’entrée. Un bon dispositif indique ce que l’auteur a constaté tout en laissant assez de contexte pour vérifier ou nuancer.

8.2 Du sujet à l’affirmation

Un titre descriptif nomme le contenu :

Espérance de vie, 2000–2023

Un titre orienté formule une observation :

Les quatre trajectoires reculent autour de 2020, mais à des amplitudes différentes

Un titre causal prétend expliquer :

La crise sanitaire a causé la divergence des quatre pays

Le deuxième titre donne une direction de lecture tout en restant visible dans les données. Le troisième demande des preuves supplémentaires. La bonne question n’est pas « faut-il toujours un titre qui raconte la conclusion ? », mais « quel niveau d’affirmation notre preuve peut-elle soutenir ? »

8.3 Le récit en quatre couches

Pour notre exemple :

  • Contexte : 2000–2023, quatre pays, même indicateur et même unité.
  • Tendance : progression de long terme pour plusieurs séries.
  • Rupture : recul visible autour de 2020–2021.
  • Comparaison : amplitude et récupération diffèrent selon les pays.

Chaque couche doit rester liée à une marque visible. Une phrase qui ne peut être pointée dans le graphique appartient peut-être à une note méthodologique, à une autre source ou à une autre vue.

Dans cette démonstration, les données ne changent pas. Seuls changent le titre, le texte et la saillance de certaines marques. Cela suffit à produire une progression. Une narration n’a donc pas besoin de mouvement spectaculaire : elle peut être une orchestration de l’attention.

8.4 Les annotations sont des ponts

Une annotation relie un fait visuel à une interprétation. Elle contient souvent :

  • un point d’ancrage;
  • une ligne ou un repère léger;
  • une phrase courte;
  • parfois une source ou une incertitude.
function addAnnotation(plot, { x, y, label, detail }) {
  const annotation = plot
    .append("g")
    .attr("class", "annotation")
    .attr("transform", `translate(${x}, ${y})`);

  annotation
    .append("circle")
    .attr("r", 4)
    .attr("fill", "currentColor");

  annotation
    .append("line")
    .attr("x2", 24)
    .attr("y2", -24)
    .attr("stroke", "currentColor");

  annotation
    .append("text")
    .attr("x", 30)
    .attr("y", -28)
    .attr("font-weight", 600)
    .text(label);

  annotation
    .append("text")
    .attr("x", 30)
    .attr("y", -10)
    .text(detail);
}

Le SVG ne fait pas revenir automatiquement le texte à la ligne. Pour une annotation longue, utilisez plusieurs <tspan>, un élément HTML positionné, ou réduisez la phrase. Une annotation utile n’est pas un paragraphe flottant : elle formule le lien minimal entre la marque et l’idée.

8.5 Mettre en évidence sans effacer

Supposons une scène centrée sur le Mexique :

const activeCodes = new Set(["MEX"]);

plot
  .selectAll(".country-line")
  .attr("stroke", (d) => color(d.code))
  .attr("stroke-width", (d) => activeCodes.has(d.code) ? 4 : 1.5)
  .attr("opacity", (d) => activeCodes.has(d.code) ? 1 : 0.22);

Les autres séries restent présentes pour préserver l’échelle et le contexte. Les faire disparaître produirait une comparaison différente, parfois trompeuse. L’atténuation doit demeurer suffisante pour que la personne comprenne que d’autres données existent.

La couleur est doublée par l’épaisseur et l’étiquette directe. Une personne qui distingue mal certaines teintes peut encore suivre la scène.


9. Modéliser une scène comme un état

Évitez d’écrire quatre scripts D3 presque identiques. Décrivez plutôt chaque scène par des données :

const scenes = [
  {
    title: "Une progression de long terme",
    description: "Les quatre séries commencent par une vue d’ensemble.",
    highlightedCodes: [],
    annotation: null,
  },
  {
    title: "La rupture n’a pas la même amplitude partout",
    description: "Le Mexique et les États-Unis reculent plus fortement.",
    highlightedCodes: ["MEX", "USA"],
    annotation: { code: "MEX", year: 2021, label: "Point bas" },
  },
  {
    title: "La reprise ne ramène pas immédiatement toutes les séries à 2019",
    description: "La fin de période doit être comparée au niveau pré-rupture.",
    highlightedCodes: ["CAN", "FRA", "MEX", "USA"],
    annotation: { code: "CAN", year: 2023, label: "Dernière observation" },
  },
];

Le bouton « Suivant » ne dessine pas un nouveau graphique. Il change sceneIndex, puis appelle une fonction de mise à jour :

function updateScene(scene) {
  title.textContent = scene.title;
  summary.textContent = scene.description;

  const active = new Set(scene.highlightedCodes);

  plot
    .selectAll(".country-line")
    .attr("opacity", (d) =>
      active.size === 0 || active.has(d.code) ? 1 : 0.2
    )
    .attr("stroke-width", (d) =>
      active.size === 0 || active.has(d.code) ? 3 : 1.5
    );

  renderAnnotation(scene.annotation);
}

Cette architecture devient essentielle à la prochaine séance : un état stable permet d’animer le passage d’une scène à l’autre, d’honorer la préférence de mouvement réduit et de conserver l’identité des marques.

9.1 Scrollytelling : le défilement comme contrôleur

Le scrollytelling associe des étapes de texte à des états visuels. Le défilement ne devrait pas recalculer le graphique à chaque pixel. Il signale plutôt qu’une étape devient active; la fonction updateScene fait le reste.

Une bibliothèque comme Scrollama simplifie la détection des étapes en s’appuyant sur IntersectionObserver. Le principe reste :

scroller
  .setup({ step: ".story-step" })
  .onStepEnter(({ index }) => updateScene(scenes[index]));

Pointe vers un projet plus grand

Un scrollytelling complet demande du travail de mise en page, de rédaction, de test mobile, de navigation clavier et de mouvement réduit. Dans cette formation de 18 heures, nous construisons la logique de scènes avec des boutons. Le scroll devient ensuite un contrôleur interchangeable, pas le cœur du graphique.


10. Lire un exemple Observable sans se noyer

Les exemples D3 publiés sur Observable sont une immense bibliothèque de patterns. Ils peuvent cependant impressionner : données chargées à distance, cellules réactives, fonctions compactes, générateurs sophistiqués.

Ne commencez pas par lire chaque ligne. Posez six questions :

  1. Quelle est la forme des données ? Cherchez un exemple de ligne normalisée.
  2. Quelle est la marque principale ? rect, circle, path, groupe HTML ?
  3. Quelles sont les échelles ? Leurs domaines répondent souvent à la question analytique.
  4. Quelle est la jointure ? Quel élément correspond à quel niveau de données ?
  5. Quelle est la clé d’identité ? Elle révèle ce qui doit persister.
  6. Qu’est-ce qui varie dans l’interaction ? Domaine, filtre, opacité, position ou données ?

Copiez ensuite le plus petit pattern possible dans votre projet Vite. Un exemple Observable peut utiliser une cellule viewof ou des pièces propres au notebook; votre projet utilise des événements DOM ordinaires. Adaptez le mécanisme, pas l’enveloppe entière.


11. Atelier — du document piloté par les données au récit

Atelier · niveau Observer

Lire une jointure

12 minutes

Résultat visible : Une annotation ligne par ligne d’un pattern D3, formulée en langage courant.

  1. Ouvrez un bar chart de la documentation D3.
  2. Entourez la sélection, le tableau joint, la clé éventuelle et la marque créée.
  3. Identifiez le domaine et la plage de chaque échelle.
  4. Expliquez à une autre personne pourquoi le premier selectAll peut être vide.
  5. Nommez une décision analytique cachée dans le code : tri, domaine, filtre ou agrégation.

Atelier · niveau Modifier

Faire parler le graphique Banque mondiale

25 minutes

Résultat visible : Deux scènes qui conservent les mêmes données, mais guident vers deux observations distinctes.

  1. Construisez la vue d’ensemble avec les quatre séries.
  2. Ajoutez des labels directs à l’extrémité des lignes.
  3. Créez un objet scenes avec un titre, une description et des codes actifs.
  4. Ajoutez deux boutons qui appellent updateScene.
  5. Vérifiez que la scène focalisée conserve les autres séries comme contexte.
  6. Réécrivez le titre si son affirmation dépasse ce qui est visible.

Atelier · niveau Construire

Votre mini-récit vérifiable

35 à 60 minutes, à poursuivre librement

Résultat visible : Une visualisation D3 de trois scènes, déployable et accompagnée de ses sources.

Choisissez un dataset que vous comprenez suffisamment pour en défendre les limites. Construisez :

  • une scène de contexte;
  • une scène qui met en évidence un pattern ou une exception;
  • une scène de comparaison ou de conclusion;
  • au moins une annotation ancrée à une donnée;
  • une note indiquant la source, la période, l’unité et la date de consultation;
  • un résumé textuel qui reste compréhensible sans voir les couleurs.

Partagez le lien Vercel si vous le souhaitez. Il ne s’agit pas d’une remise notée : la discussion porte sur les décisions et les essais, y compris ceux qui n’ont pas fonctionné.

Questions de critique entre pairs

Pour commenter un projet sans rester dans « j’aime / je n’aime pas » :

  • Quelle question ai-je comprise dans les dix premières secondes ?
  • Quelle marque constitue la preuve principale ?
  • Quel contexte me permet de juger cette preuve ?
  • Le passage entre les scènes change-t-il l’idée ou seulement la décoration ?
  • Quelle autre interprétation raisonnable demeure possible ?
  • Quelle source ou définition faudrait-il ouvrir pour vérifier l’affirmation ?

12. Dépannage : quatre erreurs qui enseignent beaucoup

Rien n’apparaît

Vérifiez le parent de la sélection, la présence du conteneur et les dimensions. Inspectez le DOM : le SVG existe-t-il ? Les éléments ont-ils des attributs NaN ?

console.log({ rows, xDomain: x.domain(), yDomain: y.domain() });

Toutes les marques sont au même endroit

La fonction d’attribut retourne peut-être undefined, une chaîne non convertie ou une date invalide. Affichez une donnée et le résultat de l’échelle :

console.log(rows[0], x(rows[0].date), y(rows[0].lifeExpectancy));

Le deuxième rendu duplique tout

Vous utilisez probablement .append() à chaque mise à jour au lieu d’une jointure sur une sélection stable. Les conteneurs structurels peuvent être créés une fois; les marques répétées doivent suivre .selectAll(...).data(...).join(...).

Les lignes relient des points dans le mauvais ordre

Le générateur suit l’ordre du tableau. Triez les observations par date avant de générer le chemin. Une série temporelle n’est pas automatiquement ordonnée parce que sa variable est une date.


Ce que vous devriez emporter

  • D3 compose des primitives : il ne choisit pas le graphique ni le message à votre place.
  • La jointure compare des identités : enter, update et exit décrivent le rapport entre un document existant et de nouvelles données.
  • Une clé est sémantique : elle dit ce qui reste le même lorsque l’ordre ou les valeurs changent.
  • Une échelle est un contrat : elle traduit un domaine de données vers une plage visuelle; son domaine reste une décision éditoriale.
  • Un axe expose l’échelle : ticks, unités et précision doivent servir la tâche.
  • Le storytelling orchestre l’attention : contexte, preuve, progression et conclusion doivent demeurer vérifiables dans les données.
  • Une scène est un état : le texte, les highlights et les annotations peuvent varier sans réécrire le graphique.

À la prochaine séance

Nous garderons les mêmes idées — identité, état, jointure et scène — puis nous ajouterons le temps. Nous distinguerons une transition qui aide à suivre les marques d’une animation qui distrait, et une donnée réellement nouvelle d’une interface qui ne fait que bouger. Enfin, nous brancherons une source météo vivante sans accumuler les requêtes ni perdre les états d’erreur.