Séance 6 — Faire vivre les données sans perdre le sens
Question directrice
Comment montrer qu’une donnée change sans transformer l’interface en spectacle, en horloge inutile ou en fuite de mémoire ?
Cette dernière séance relie la perception du mouvement, l’identité des marques, les transitions D3 et une source météo vivante. Vous allez apprendre à choisir entre polling, SSE et WebSocket, à distinguer fraîcheur des données et fréquence d’affichage, à borner une fenêtre temporelle et à concevoir les états chargement, prêt, périmé et erreur. Le résultat doit être vivant parce que les données le sont — pas parce que tout bouge.
Point de départ : votre graphique D3 possède des données normalisées, des clés stables, des échelles et une fonction de rendu. Ce sont précisément les conditions qui permettent d’ajouter du mouvement sans perdre l’identité ni la structure.
Comment utiliser cette leçon
Cette séance est un finale expérimental, pas un concours d’effets. On commence par comparer des états statiques et animés. On construit ensuite une transition complète sur une jointure. Enfin, on remplace les données préparées par une source qui évolue et on traite tout ce qui entoure le graphique : rythme, statut, arrêt, reprise, erreur, ancienneté et nettoyage.
En direct, gardez l’onglet Réseau et la console ouverts. Le comportement du système — requêtes, intervalles, erreurs, réponses — fait partie du cours autant que la courbe visible.
| Temps en direct | Activité | Résultat |
|---|---|---|
| 0:00–0:20 | Comparer saut, transition et petits multiples | Savoir quand le mouvement aide |
| 0:20–0:55 | Clés, object constancy et transition() | Une mise à jour dont les identités restent lisibles |
| 0:55–1:25 | Entrée, mise à jour, sortie et axes animés | Un pattern D3 complet et interrompable |
| 1:25–1:35 | Pause | — |
| 1:35–2:00 | Polling, SSE, WebSocket et fraîcheur | Choisir un transport proportionné au besoin |
| 2:00–2:35 | Open-Meteo, états et fenêtre glissante | Une visualisation vivante robuste |
| 2:35–3:00 | Atelier final et partage | Une démo publiable, avec limites explicites |
1. Le mouvement est un encodage, pas une récompense
Une animation attire l’attention. C’est précisément ce qui la rend utile et dangereuse. Lorsqu’une marque se déplace d’une position à une autre, le mouvement peut aider à comprendre qu’il s’agit du même objet dont la valeur change. Lorsqu’une douzaine d’éléments rebondissent, pulsent et apparaissent en cascade, le mouvement devient un concurrent du message.
Dans une visualisation, l’animation peut remplir plusieurs fonctions :
- préserver la continuité entre deux états;
- signaler l’arrivée ou le retrait d’une observation;
- montrer un processus qui possède réellement une dimension temporelle;
- guider l’attention vers une étape d’un récit;
- confirmer une action de l’utilisateur.
Elle remplit moins bien d’autres fonctions :
- comparer précisément plusieurs moments éloignés;
- conserver un état pour inspection détaillée;
- remplacer une légende ou des unités;
- rendre intéressant un graphique sans question;
- prouver une causalité.
Une animation est transitoire : ce qui est passé n’est plus entièrement visible. Une vue statique, une trace ou des petits multiples permettent de comparer plusieurs états côte à côte. Le choix dépend donc de la tâche.
1.1 Trois questions avant d’animer
- Qu’est-ce qui reste le même ? Un pays, une station, une mesure, une catégorie ?
- Qu’est-ce qui change ? La valeur, le rang, l’échelle, le filtre, le type de marque ?
- Que doit comprendre la personne pendant le passage ? Une continuité, une entrée, une sortie ou une réorganisation ?
Si vous ne pouvez pas répondre, la transition risque de n’exprimer aucune structure.
2. Object constancy : suivre la même chose à travers le changement
L’expression object constancy désigne ici un objectif de conception : aider la personne à reconnaître qu’une marque avant et après une mise à jour représente la même entité. La continuité spatiale, la forme, la couleur, le label et le mouvement peuvent y contribuer.
Dans le code D3, cette continuité commence par une fonction-clé correcte.
Avec une association par index, le premier ancien cercle reçoit la première nouvelle donnée. Si A disparaît et que B devient le premier objet du tableau, A semble se transformer en B. La transition peut être parfaitement fluide tout en racontant une identité fausse.
Avec une association par identifiant :
- B, C et D persistent;
- A suit le chemin de sortie;
- E suit le chemin d’entrée.
La différence est conceptuelle avant d’être animée.
const marks = plot
.selectAll("circle.station")
.data(nextData, (d) => d.stationId);
La clé stationId ne dit pas où dessiner la marque. Elle dit ce qu’elle est.
2.1 La change blindness : une prudence, pas une formule magique
Des changements brusques peuvent être manqués, surtout lorsque l’attention est ailleurs, qu’une interruption visuelle survient ou que plusieurs éléments changent à la fois. On évoque souvent la change blindness pour justifier l’animation. Il faut rester prudent : toute transition n’améliore pas automatiquement la détection, et trop de mouvement peut précisément disperser l’attention.
Une transition utile :
- part d’un état visible;
- arrive à un état stable;
- maintient les correspondances pertinentes;
- limite les changements simultanés;
- laisse le temps d’inspecter le résultat;
- peut être réduite ou désactivée.
2.2 Animation pour présentation, statique pour analyse ?
Une autre étude a comparé une animation de tendances de type Gapminder à deux vues statiques, dont des petits multiples. L’animation était engageante, mais les vues statiques se sont révélées plus efficaces pour plusieurs tâches d’analyse. La leçon n’est pas « ne jamais animer ». Elle est plus intéressante : présenter une évolution et analyser plusieurs évolutions ne sont pas la même tâche.
3. Le modèle D3 d’une transition
Une transition D3 décrit comment des propriétés passent de leurs valeurs actuelles à de nouvelles valeurs pendant une durée. D3 interpole les nombres, couleurs, transformations et plusieurs chaînes structurées.
marks
.transition()
.duration(600)
.ease(d3.easeCubicInOut)
.attr("cx", (d) => x(d.value))
.attr("r", (d) => size(d.volume));
L’ordre est important : la sélection contient les éléments; .transition() crée une transition sur ces éléments; les attributs suivants décrivent l’état final.
3.1 Ne pas animer le premier rendu par réflexe
Faire pousser toutes les barres depuis zéro à l’ouverture est courant. Pourtant, la personne n’a pas encore vu l’état initial et ne compare rien. L’intro ralentit simplement l’accès à l’information. Préférez un premier rendu stable, puis animez une transformation déclenchée par une action ou une arrivée de données.
3.2 La durée n’est pas une constante scientifique
Des durées entre quelques centaines de millisecondes et environ une seconde fonctionnent souvent pour une transition locale, mais il n’existe pas de valeur universelle. La distance parcourue, le nombre de marques, la complexité du changement, l’appareil et la tâche modifient le besoin.
Testez au moins trois questions :
- Le passage est-il assez long pour suivre l’identité ?
- Est-il assez court pour ne pas interrompre le travail ?
- La personne peut-elle agir de nouveau sans attendre une chorégraphie ?
Une transition de 500 ms répétée à chaque frappe dans un filtre devient pénible. Une transformation complexe de plusieurs axes en 150 ms devient illisible.
3.3 L’easing porte une signification
L’easing règle la vitesse au cours du temps :
d3.easeLinear
d3.easeCubicInOut
d3.easeCubicOut
easeCubicInOut accélère puis ralentit; il rend souvent un déplacement facile à suivre. easeLinear convient lorsque la vitesse elle-même doit sembler constante. Les easings avec rebond ou dépassement peuvent suggérer une propriété physique inexistante. Une valeur de température n’a pas besoin de rebondir pour être « vivante ».
3.4 Interrompre avant d’empiler
Si la personne change rapidement de filtre, plusieurs transitions peuvent se mettre en file. Interrompez l’ancienne avant d’en lancer une nouvelle :
marks
.interrupt("filter")
.transition("filter")
.duration(motionDuration)
.attr("cx", (d) => x(d.value));
Le nom filter permet de gérer cette famille de transitions sans nécessairement interrompre une autre animation indépendante.
4. Enter, update et exit en mouvement
Reprenons des stations identifiées :
function updateStations(nextData) {
const transition = svg
.transition("stations")
.duration(motionDuration)
.ease(d3.easeCubicInOut);
plot
.selectAll("circle.station")
.data(nextData, (d) => d.stationId)
.join(
(enter) => enter
.append("circle")
.attr("class", "station")
.attr("cx", (d) => x(d.value))
.attr("cy", innerHeight)
.attr("r", 0)
.call((selection) => selection
.transition(transition)
.attr("cy", (d) => y(d.category))
.attr("r", 7)
),
(update) => update
.call((selection) => selection
.transition(transition)
.attr("cx", (d) => x(d.value))
.attr("cy", (d) => y(d.category))
),
(exit) => exit
.call((selection) => selection
.transition(transition)
.attr("r", 0)
.remove()
)
);
}
L’entrée part d’un état qui exprime « pas encore présent » : rayon zéro, près de la base. La mise à jour part de la position actuelle. La sortie réduit la marque avant de la retirer.
Évitez d’utiliser seulement l’opacité pour tout. Un fondu ne montre ni direction, ni changement de rang, ni identité spatiale. Il convient lorsque l’apparition elle-même est l’information; un déplacement convient lorsque la position change.
4.1 Mettre à jour les axes avec la même transition
Si le domaine change, les marques et l’axe doivent partager le même rythme :
x.domain([0, d3.max(nextData, (d) => d.value)]).nice();
xAxisGroup
.transition(transition)
.call(d3.axisBottom(x).ticks(5));
Sinon, les points peuvent se déplacer vers une échelle dont les ticks restent temporairement anciens. La transition ne sert plus à préserver la continuité; elle crée une contradiction.
4.2 Mettre en scène un changement complexe
Lorsque plusieurs propriétés changent, les faire toutes évoluer simultanément peut être difficile à suivre. Une transition en étapes peut séparer :
- le filtrage et la sortie des éléments;
- le changement d’échelle;
- le déplacement des éléments persistants;
- l’entrée des nouveaux éléments.
Mais chaque étape ajoute du temps. La mise en scène doit servir une transformation conceptuellement complexe, pas étirer une mise à jour simple.
const t1 = svg.transition().duration(250);
const t2 = t1.transition().duration(450);
exiting.transition(t1).attr("opacity", 0).remove();
xAxisGroup.transition(t2).call(d3.axisBottom(x));
updating.transition(t2).attr("cx", (d) => x(d.value));
5. Mouvement réduit : concevoir un état équivalent
Certaines personnes demandent à leur système de réduire les animations. Le média CSS prefers-reduced-motion permet de détecter cette préférence. Il ne suffit pas de ralentir énormément : une transition longue peut être plus inconfortable. Fournissez un passage immédiat ou très court.
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const motionDuration = reduceMotion ? 0 : 550;
En CSS :
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Cette règle globale est une garde de dernier recours; le meilleur comportement est décidé dans la logique du composant. Le résultat final, les labels et les contrôles doivent rester identiques avec ou sans mouvement.
6. « Live » ne veut pas dire la même chose pour tout
Une température « actuelle » issue d’un modèle météo, une position de véhicule, un prix boursier et une mesure de capteur n’ont ni la même cadence, ni la même latence, ni le même risque.
Avant de choisir une technologie, distinguez quatre temps :
- Temps du phénomène : quand l’événement s’est produit.
- Temps de la source : quand la source l’a mesuré ou calculé.
- Temps de réception : quand votre application l’a obtenu.
- Temps d’affichage : quand l’interface l’a rendu visible.
Une interface peut se redessiner soixante fois par seconde avec une donnée vieille d’une heure. Elle semble vivante, mais n’est pas fraîche. À l’inverse, une donnée mise à jour toutes les quinze minutes n’a pas besoin d’être requêtée chaque seconde.
6.1 Le budget de fraîcheur
Définissez un besoin avant la fréquence :
Pour cette décision, une observation vieille de [durée] reste utile; au-delà, l’interface doit la signaler comme périmée.
Exemples :
- une alerte de sécurité peut demander quelques secondes;
- une disponibilité de vélo peut tolérer une minute;
- une prévision météo peut tolérer plusieurs minutes ou davantage selon la source;
- un indicateur annuel ne devient pas plus vrai s’il est interrogé chaque minute.
Cette phrase évite de choisir WebSocket par prestige.
7. Polling, SSE ou WebSocket ?
Le navigateur ne peut recevoir que ce que le serveur offre. Si une API propose uniquement HTTP, votre front-end ne peut pas la transformer magiquement en WebSocket. Le protocole est une décision partagée avec la source ou votre propre backend.
| Pattern | Direction | Bon cas d’usage | Coût ou limite |
|---|---|---|---|
Polling avec fetch | Client → serveur, réponses ponctuelles | API REST, cadence lente ou modérée | Requêtes même lorsque rien ne change |
| Server-Sent Events | Serveur → client | Notifications et mesures unidirectionnelles | Demande un endpoint SSE; communication surtout à sens unique |
| WebSocket | Bidirectionnelle | Collaboration, jeu, télémétrie interactive | Connexion et reconnexion plus complexes; gestion du débit nécessaire |
7.1 Polling
Le client demande périodiquement une nouvelle valeur :
const response = await fetch(url);
const payload = await response.json();
C’est souvent le meilleur premier choix pour une API publique dont la donnée change lentement. Le principal risque est de lancer une nouvelle requête alors que la précédente n’est pas terminée.
7.2 Server-Sent Events
Le serveur maintient un flux HTTP et pousse des événements :
const source = new EventSource("/api/measurements");
source.addEventListener("message", (event) => {
const measurement = JSON.parse(event.data);
update(measurement);
});
source.addEventListener("error", () => {
setStatus("reconnexion");
});
SSE convient lorsque le navigateur reçoit des événements sans avoir besoin d’envoyer continuellement des messages sur la même connexion.
7.3 WebSocket
Une connexion WebSocket permet au client et au serveur d’envoyer des messages :
const socket = new WebSocket("wss://example.org/stream");
socket.addEventListener("message", (event) => {
update(JSON.parse(event.data));
});
socket.addEventListener("close", scheduleReconnect);
Le WebSocket classique n’impose pas de mécanisme de backpressure au producteur. Si les messages arrivent plus vite que l’application ne les traite, mémoire et CPU peuvent s’épuiser. Il faut borner le buffer, échantillonner, agréger ou négocier le débit.
8. Exemple guidé : météo de Montréal avec Open-Meteo
Open-Meteo expose une API de prévisions. Pour Montréal, demandons la température, l’humidité relative et le vent actuels :
const WEATHER_URL = new URL(
"https://api.open-meteo.com/v1/forecast"
);
WEATHER_URL.search = new URLSearchParams({
latitude: "45.5019",
longitude: "-73.5674",
current: [
"temperature_2m",
"relative_humidity_2m",
"wind_speed_10m",
].join(","),
timezone: "America/Toronto",
});
La documentation indique que les conditions actuelles sont fondées sur des données de modèle à résolution de quinze minutes. Pour la classe, nous pouvons déclencher manuellement une actualisation et utiliser une simulation locale pour voir plusieurs transitions. En production, interroger cette source chaque seconde serait inutile.
8.1 Normaliser et valider la réponse
async function fetchWeather(signal) {
const response = await fetch(WEATHER_URL, { signal });
if (!response.ok) {
throw new Error(`Open-Meteo : HTTP ${response.status}`);
}
const payload = await response.json();
const current = payload?.current;
const units = payload?.current_units;
if (
!current ||
!Number.isFinite(current.temperature_2m) ||
typeof current.time !== "string"
) {
throw new Error("Réponse météo incomplète.");
}
return {
id: current.time,
observedAt: new Date(current.time),
receivedAt: new Date(),
temperature: current.temperature_2m,
humidity: current.relative_humidity_2m,
windSpeed: current.wind_speed_10m,
units: {
temperature: units.temperature_2m,
humidity: units.relative_humidity_2m,
windSpeed: units.wind_speed_10m,
},
};
}
Nous conservons observedAt et receivedAt. Leur différence permet de dire depuis combien de temps la source considère la mesure valide et quand notre interface l’a reçue.
La clé id utilise le timestamp de la source. Si deux requêtes retournent la même observation, nous ne devons pas ajouter deux points identiques à la série.
8.2 Un état d’interface explicite
let state = {
status: "idle", // idle | loading | ready | stale | error
points: [],
error: null,
lastSuccessAt: null,
};
Un graphique vivant n’alterne pas seulement entre « données » et « pas de données ». Il doit distinguer :
- loading sans données : premier chargement;
- ready : dernière requête réussie et fraîche;
- refreshing : anciennes données encore visibles pendant la requête;
- stale : données visibles, mais plus vieilles que le budget annoncé;
- error sans données : rien de fiable à montrer;
- error avec données : conserver la dernière valeur et signaler l’échec.
Effacer le graphique au moindre échec rend une panne réseau plus grave qu’elle ne l’est. Afficher l’ancienne valeur comme si elle était actuelle est tout aussi problématique. La bonne interface conserve la valeur et marque son ancienneté.
8.3 Dédupliquer et borner la fenêtre
const MAX_POINTS = 48;
function addPoint(points, nextPoint) {
const withoutDuplicate = points.filter(
(point) => point.id !== nextPoint.id
);
return [...withoutDuplicate, nextPoint]
.sort((a, b) => d3.ascending(a.observedAt, b.observedAt))
.slice(-MAX_POINTS);
}
La fenêtre glissante borne la mémoire et maintient une densité lisible. Une archive complète appartient à une base de données ou à un fichier; le navigateur affiche un horizon adapté à la tâche.
La démonstration utilise un flux simulé pour que le mécanisme soit visible en classe. Une simulation n’est pas une source réelle; elle doit être nommée comme telle. Elle permet de tester les transitions, les pics et le nettoyage sans dépendre d’une API distante.
9. Un polling qui ne se chevauche pas
setInterval déclenche selon l’horloge, même si la requête précédente n’est pas terminée. Un setTimeout récursif peut planifier la prochaine requête après la fin de la précédente.
const POLL_MS = 15 * 60 * 1000;
let pollTimer = null;
let activeController = null;
let running = false;
async function refreshWeather() {
activeController?.abort();
activeController = new AbortController();
const hadData = state.points.length > 0;
state = {
...state,
status: hadData ? "refreshing" : "loading",
error: null,
};
render(state);
try {
const point = await fetchWeather(activeController.signal);
state = {
...state,
status: "ready",
points: addPoint(state.points, point),
lastSuccessAt: new Date(),
};
} catch (error) {
if (error.name === "AbortError") return;
state = {
...state,
status: hadData ? "stale" : "error",
error,
};
} finally {
render(state);
if (running) {
pollTimer = window.setTimeout(refreshWeather, POLL_MS);
}
}
}
Le contrôleur annule une requête devenue inutile. C’est important si la personne change de ville, met le flux en pause ou quitte le composant.
9.1 Démarrer, mettre en pause, nettoyer
function startPolling() {
if (running) return;
running = true;
refreshWeather();
}
function stopPolling() {
running = false;
window.clearTimeout(pollTimer);
activeController?.abort();
}
startButton.addEventListener("click", startPolling);
pauseButton.addEventListener("click", stopPolling);
window.addEventListener("pagehide", stopPolling);
Dans un composant React, le cleanup de useEffect joue le même rôle. Dans une application avec routage, le composant doit fermer la connexion ou arrêter le timer lorsqu’il est démonté.
9.2 Respecter la visibilité de la page
Les navigateurs limitent déjà certains timers en arrière-plan, mais l’application peut exprimer clairement son comportement :
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
});
Au retour, une actualisation immédiate est généralement préférable à rejouer toutes les mises à jour manquées.
10. Mettre à jour la ligne sans réécrire le graphique
Notre fonction render(state) dérive les échelles du tableau courant puis met à jour l’axe, le chemin et le dernier point.
function render(state) {
renderStatus(state);
if (state.points.length === 0) {
plot.attr("hidden", true);
return;
}
plot.attr("hidden", null);
x.domain(d3.extent(state.points, (d) => d.observedAt));
const [min, max] = d3.extent(state.points, (d) => d.temperature);
y.domain([min - 1, max + 1]).nice();
const t = svg
.transition("weather-update")
.duration(motionDuration)
.ease(d3.easeCubicInOut);
xAxisGroup.transition(t).call(d3.axisBottom(x).ticks(5));
yAxisGroup.transition(t).call(d3.axisLeft(y).ticks(5));
plot
.selectAll("path.temperature-line")
.data([state.points])
.join("path")
.attr("class", "temperature-line")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("stroke-width", 3)
.transition(t)
.attr("d", line);
plot
.selectAll("circle.latest-point")
.data(state.points.slice(-1), (d) => d.id)
.join(
(enter) => enter
.append("circle")
.attr("class", "latest-point")
.attr("r", 0)
.attr("cx", (d) => x(d.observedAt))
.attr("cy", (d) => y(d.temperature))
.call((selection) => selection.transition(t).attr("r", 6)),
(update) => update.call((selection) => selection
.transition(t)
.attr("cx", (d) => x(d.observedAt))
.attr("cy", (d) => y(d.temperature))
),
(exit) => exit.remove()
);
}
Pour une ligne dont le nombre de points change, l’interpolation du chemin peut parfois produire un passage étrange. Une solution simple est d’animer surtout l’axe et le dernier point, puis de mettre le chemin à jour sans chercher une morphologie spectaculaire. Une solution avancée consiste à resampler les chemins afin qu’ils aient des structures compatibles. Dans ce cours, le choix pédagogique est clair : la continuité utile prime sur l’effet technique.
10.1 requestAnimationFrame n’est pas une minuterie de données
requestAnimationFrame demande au navigateur d’exécuter un callback avant un prochain rafraîchissement visuel. Il convient aux animations image par image et se met généralement en pause dans les onglets cachés. Il ne doit pas servir à interroger une API à la fréquence de l’écran.
function animateFrame(timestamp) {
// calcul de l’état visuel à partir du temps écoulé
requestId = requestAnimationFrame(animateFrame);
}
D3 gère déjà son propre timing pour les transitions. Utilisez requestAnimationFrame lorsque vous contrôlez une animation sur Canvas ou un rendu très fréquent, pas comme substitut à setTimeout pour le réseau.
11. Rendre la fraîcheur visible
Un voyant vert « Live » ne suffit pas. Il peut rester vert après une panne. Affichez plutôt des faits :
- heure de validité de la dernière observation;
- heure de la dernière réception réussie;
- statut de connexion ou d’actualisation;
- horizon visible;
- bouton pause/reprise;
- message d’erreur non destructif.
function renderStatus(state) {
const last = state.points.at(-1);
statusElement.textContent = last
? `Observation ${formatTime(last.observedAt)} · ` +
`reçue ${formatTime(last.receivedAt)} · ` +
`${statusLabel(state.status)}`
: statusLabel(state.status);
}
11.1 Calculer l’ancienneté
const STALE_AFTER_MS = 30 * 60 * 1000;
function isStale(lastPoint, now = new Date()) {
return now - lastPoint.receivedAt > STALE_AFTER_MS;
}
Le seuil doit venir du budget de fraîcheur, pas d’une couleur arbitraire. Une prévision météo et une alerte incendie n’ont pas le même seuil acceptable.
11.2 Ne pas dépendre seulement de la couleur
Utilisez un texte explicite — « à jour », « actualisation », « données possiblement périmées », « connexion interrompue » — et éventuellement une icône ou une forme. Le statut doit être annoncé avec mesure aux technologies d’assistance. Une région aria-live="polite" convient aux changements occasionnels; annoncer chaque point d’un flux rapide serait insupportable.
Le graphique doit aussi disposer d’un résumé textuel mis à jour à un rythme raisonnable :
Température actuelle estimée : 25,7 °C à 15 h 45. Sur les huit dernières observations visibles, la valeur varie de 23,9 à 26,4 °C.
Une table des dernières observations peut offrir une inspection exacte. Il n’est pas nécessaire d’exposer chaque cercle SVG au focus clavier si une représentation textuelle équivalente répond mieux à la tâche.
12. Le débit : recevoir, traiter et dessiner sont trois rythmes
Un système vivant possède au moins trois cadences :
source → traitement → rendu
La source peut produire 100 mesures par seconde. Le traitement peut les agréger par seconde. Le rendu peut se mettre à jour dix fois par seconde ou moins. Il n’est pas nécessaire de dessiner chaque événement pour préserver l’information utile.
12.1 Fenêtrer
Conservez les derniers N points ou les dernières N minutes. La fenêtre répond à une question : « que se passe-t-il récemment ? » Elle n’est pas une archive.
12.2 Échantillonner
Si le signal est très dense, choisissez des points représentatifs. Un échantillonnage naïf peut manquer des pics; des techniques spécialisées préservent mieux la forme. Dans un cours de 18 heures, retenez surtout que « prendre une ligne sur dix » est une décision analytique, pas seulement technique.
12.3 Agréger
Par fenêtre temporelle, calculez minimum, maximum, moyenne et nombre d’observations. Une bande min–max peut montrer les pointes qu’une moyenne seule cacherait.
12.4 Dégrader volontairement
Lorsque la charge augmente :
- réduire la fréquence de rendu;
- réduire le nombre de points visibles;
- désactiver certaines transitions;
- déplacer le calcul lourd dans un Worker;
- passer de SVG à Canvas ou WebGL lorsque le profilage le justifie.
Il n’existe pas un seuil universel où SVG « cesse de fonctionner ». Le nombre d’éléments, leur complexité, le navigateur, l’appareil et la fréquence de mise à jour comptent. Mesurez avec les outils de performance avant de changer toute l’architecture.
Pointe performance, pas nouveau cours
Performance, Canvas, WebGL, Workers et algorithmes de réduction pourraient remplir plusieurs séances. Ici, on retient trois gestes immédiatement applicables : borner les données en mémoire, ne pas redessiner plus vite que nécessaire et nettoyer timers ou connexions. Le reste doit être exploré à partir d’un problème mesuré.
13. Résilience : concevoir la panne avant la démo
Une API publique peut être lente, indisponible, limitée ou changer de schéma. Une démo de cours dépend aussi du Wi-Fi et de politiques CORS. Préparez un mode de repli transparent.
13.1 Une fixture locale
Conservez un petit fichier JSON avec quelques réponses normalisées :
import demoPoints from "./data/weather-demo.json";
const sourceMode = new URLSearchParams(location.search).get("demo")
? "demo"
: "live";
Affichez « données simulées » ou « extrait enregistré ». Ne laissez jamais croire que des données locales sont encore en direct.
13.2 Reconnexion avec attente progressive
Pour une connexion durable, évitez une boucle de reconnexion agressive :
let retryCount = 0;
function nextDelay() {
const base = Math.min(30_000, 1_000 * 2 ** retryCount);
const jitter = Math.random() * 500;
retryCount += 1;
return base + jitter;
}
Après une réussite, remettez retryCount à zéro. Le jitter évite que des milliers de clients reconnectent exactement au même moment.
13.3 Réponses hors ordre
Deux requêtes simultanées peuvent terminer dans un ordre différent de leur lancement. Notre polling séquentiel et AbortController réduisent ce risque. Une autre stratégie conserve un numéro de requête et ignore une réponse plus ancienne :
let latestRequest = 0;
async function refresh() {
const requestId = ++latestRequest;
const point = await fetchWeather();
if (requestId !== latestRequest) return;
commit(point);
}
14. Atelier final — une visualisation vivante et honnête
Atelier · niveau Observer
Audit d’un système live
Résultat visible : Un diagramme des cadences, des timestamps et des états visibles d’une application existante.
- Choisissez une interface : météo, transport, finance, opérations ou sport.
- Identifiez le temps du phénomène, de la source, de réception et d’affichage.
- Estimez le budget de fraîcheur nécessaire à la décision.
- Notez comment l’interface signale une panne ou une donnée périmée.
- Proposez une représentation statique qui compléterait l’animation.
Atelier · niveau Modifier
Brancher Open-Meteo proprement
Résultat visible : Une valeur actuelle et un historique borné, avec pause, reprise, dernière mise à jour et mode erreur.
- Ajoutez l’URL Open-Meteo et la fonction
fetchWeather. - Affichez les unités fournies par la réponse au lieu de les supposer.
- Dédupliquez les observations par timestamp.
- Conservez au plus 48 points.
- Ajoutez les états
loading,ready,staleeterror. - Ajoutez un bouton d’actualisation manuelle; n’accélérez pas artificiellement l’API.
- Testez une erreur en modifiant temporairement l’URL.
Atelier · niveau Construire
Votre actif vivant
Résultat visible : Une visualisation D3 partageable dont le mouvement, la cadence et les limites sont justifiés.
Choisissez une source autorisée et documentée. Votre prototype devrait inclure :
- une phrase expliquant la décision ou la question servie;
- un transport proportionné : polling, SSE ou WebSocket;
- une clé stable et une jointure enter–update–exit;
- une transition qui aide à suivre un changement précis;
- un état équivalent lorsque le mouvement est réduit;
- une fenêtre ou une agrégation qui borne la mémoire;
- pause, reprise, statut, dernier succès et erreur;
- un mode fixture ou simulation clairement étiqueté;
- la source, la cadence annoncée et la date de consultation.
Déployez sur Vercel si vous souhaitez partager le résultat. Le partage est une invitation à expliquer vos décisions, pas une évaluation.
Critique collective : montrer aussi l’envers
Lors de la présentation, ne montrez pas seulement la belle minute où tout fonctionne. Ouvrez le panneau Réseau, mettez en pause, provoquez une erreur et activez le mouvement réduit. Expliquez :
- pourquoi cette donnée mérite d’être actualisée;
- pourquoi cette cadence suffit;
- ce que la transition rend plus lisible;
- ce que la vue statique permet de mieux comparer;
- ce que l’interface fait lorsque la source n’est plus fiable.
Une démo robuste devient plus intéressante lorsqu’on voit son raisonnement.
15. Une méthode à emporter après les six séances
Le cours a progressé des données jusqu’au temps, mais la méthode reste cyclique :
question
↓
données et grain
↓
tâche et audience
↓
encodage perceptuel
↓
outil et architecture
↓
prototype
↓
critique, mesure, révision
Quand un projet bloque, revenez au niveau précédent. Un tooltip compliqué cache parfois un encodage mal choisi. Une transition illisible cache parfois une identité instable. Un dashboard saturé cache parfois six questions qui devraient devenir six vues. Une API trop fréquente cache parfois un besoin de fraîcheur jamais formulé.
Ce que nous avons seulement pointé
En 18 heures, nous avons intégré des garde-fous d’accessibilité, de performance et d’éthique, mais pas épuisé ces domaines. Les prochaines explorations naturelles sont :
- tests avec lecteurs d’écran et navigation clavier avancée pour SVG;
- Canvas, WebGL, Workers et profilage sur grands volumes;
- cartographie, projections et tuiles vectorielles;
- uncertainty visualization et données probabilistes;
- scrollytelling complet avec mesure de compréhension;
- design de systèmes de visualisation réutilisables;
- visualisation collaborative et provenance des transformations.
Pour continuer, choisissez un projet dont vous connaissez le contexte. Une visualisation devient experte moins par sa complexité formelle que par la qualité de ses définitions, de ses limites et de ses décisions.
Le finale, sans feux d’artifice inutiles
- Le mouvement doit encoder un changement : continuité, entrée, sortie ou processus — pas simplement signaler que la technologie fonctionne.
- L’identité précède l’animation : une clé stable permet à D3 et au regard de suivre le même objet.
- Animation et analyse ne sont pas synonymes : petits multiples, traces et états statiques restent souvent meilleurs pour comparer.
- La source impose sa cadence : redessiner plus vite ne rend pas une donnée plus fraîche.
- Polling, SSE et WebSocket répondent à des contrats différents : choisissez le mécanisme le plus simple compatible avec le serveur et le besoin.
- Un système vivant possède des états : chargement, prêt, actualisation, périmé et erreur doivent être visibles.
- La mémoire et le débit doivent être bornés : fenêtre, déduplication, agrégation et cleanup font partie du design.
- Le dernier mot revient à la question : données, perception, narration et code ne sont que des moyens de permettre une lecture juste et utile.
Bibliothèque de continuation
Pour approfondir : la documentation D3, les collections Learn D3, le livre ouvert Fundamentals of Data Visualization, les travaux de l’Interactive Data Lab et les exemples éditoriaux de The Pudding. Lisez-les avec la même grille : question, grain, tâche, encodage, preuve, interaction et limite.