Des graphiques lisibles
Un graphique d'infographie est lu en trois secondes, sur un téléphone, dans un fil d'actualité. Le graphique de la leçon 1 généré par Flash Image avait tout pour échouer : barres doubles, légende, axe gradué, couleurs par année, badge posé sur une barre. Ici, le graphique est construit par le code à partir des données, avec des règles qui tiennent sur une page.
Le mécanisme : de la question au tracé
flowchart LR Q["Question : quel site consomme le plus, et qui a le plus baissé ?"] --> F["Forme : barres horizontales"] F --> T["Tri décroissant"] T --> E["Échelle : zéro à gauche, longueur proportionnelle"] E --> L["Étiquettes directes : valeur + variation"] L --> A["Une mise en avant : plus forte baisse"] A --> C["Contrôle : taille minimale, contraste"]
| Choix | Pourquoi | Ce qu'on évite |
|---|---|---|
| Barres horizontales | les noms de sites se lisent sans rotation | étiquettes à 45° sous des colonnes |
| Tri par valeur décroissante | le classement se lit sans effort | l'ordre du CSV, arbitraire |
| Zéro comme ligne de base | la longueur dit la valeur | un axe tronqué qui exagère les écarts |
| Étiquettes directes | la valeur est au bout de la barre | axe gradué, quadrillage, aller-retour vers une légende |
| Une seule série | 2026, avec la variation en texte | deux barres par site et une légende |
| Une mise en avant | un message, une couleur | six couleurs pour six sites |
La mise en œuvre
def graphique(faits, titre, fmt, x, y, w, h):
sites = sorted(faits.sites, key=lambda s: s.conso_2026, reverse=True)
pas = (y + h - y0) / len(sites)
t = max(10, min(fmt.base, round(pas * 0.5))) # taille des étiquettes, bornée par la base
col = min(w * 0.38, max(police("bold", t).getlength(s.nom) for s in sites) + t)
valeurs = [(fr_nombre(s.conso_2026), fr_pct(s.variation_pct, signe=True)) for s in sites]
lv = max(police("bold", t).getlength(a) + police("regular", t).getlength(" " + b) for a, b in valeurs)
lmax = w - col - lv - t # place réelle laissée aux barres
vmax = max(s.conso_2026 for s in sites)
for i, (s, (val, var)) in enumerate(zip(sites, valeurs)):
long = max(2.0, lmax * s.conso_2026 / vmax)
couleur = ACCENT if s is faits.meilleur_site else BARRE
...Trois détails qui font la différence :
- La place des étiquettes est mesurée avant de tracer.
lmaxest la largeur disponible une fois réservées la colonne des noms et la plus longue étiquette de valeur. Aucune étiquette ne sort du panneau, quel que soit le format. - La taille du texte dépend de la hauteur de ligne, plafonnée par la base du format. Si six barres n'ont pas la place, les étiquettes rétrécissent, et le contrôle
lisibilitede la leçon 6 bloque sous le minimum du format au lieu de laisser passer un graphique illisible. - La valeur et la variation sont deux textes distincts : la valeur en gras (
1 655), la variation en encre secondaire (−23 %). Deux niveaux de lecture sans couleur supplémentaire.
La mise en avant ne repose pas sur la couleur seule
ACCENT (sarcelle) et BARRE (ardoise) ont chacune un bon contraste sur blanc, mais entre elles le rapport n'est que de 1,15:1 : leurs luminances sont presque égales. Un lecteur daltonien, ou une impression en niveaux de gris, ne les distingue pas. Le code ajoute donc un second codage :
# la mise en avant ne repose pas sur la couleur seule (teal/ardoise : 1,15:1 entre elles) : étiquette en gras
graisse = "bold" if s is faits.meilleur_site else "regular"« Hub Centre » est en gras. Le message survit à la perte de la couleur.
Le directeur financier préfère un axe qui commence à 500 MWh : « on voit mieux les différences ». Que répondez-vous ?
Avec un axe à 500, les longueurs deviennent 73 et 1 155 : un rapport de 16 au lieu de 2,9. Le lecteur compare des longueurs, pas des graduations ; une note en bas ne corrige pas la perception. Si les écarts fins comptent, montrez la variation en pourcentage (déjà en étiquette) ou un graphique de points sans barre.
Pourquoi pas de légende ni d'axe gradué
Une seule série : le titre du graphique la nomme (par défaut « Consommation 2026 par site (MWh) »). Les valeurs sont écrites au bout des barres : un axe gradué répéterait l'information en moins précis. La ligne de base reste, en encre secondaire de 2 px : elle matérialise le zéro.
Le titre du graphique est proposé par le modèle (titre_graphique). C'est le seul texte du graphique qui ne vient pas directement des données ; il passe par les mêmes règles que le titre principal. Au run final, le modèle a écrit « Évolution de la consommation par site entre 2025 et 2026 » : acceptable, car les étiquettes de variation portent la comparaison avec 2025, mais un relecteur exigeant préférerait que le titre dise que les barres montrent 2026. C'est le genre de nuance qu'aucune règle automatique ne tranche.
Le CSV de l'an prochain contiendra 11 sites. Que se passe-t-il dans le format story, et quelle est la bonne décision ?
Le code s'adapte en réduisant la police, ce qui est précisément ce que le contrôle refuse sous 24 px. Baisser le minimum produit une story illisible sur téléphone. Le regroupement est une décision de contenu qui garde la lisibilité ; l'affiche peut, elle, montrer les 11 sites.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| Étiquette de valeur coupée au bord du panneau | longueur des barres calculée sur toute la largeur | réserver la largeur de la plus longue étiquette (lv) avant de tracer |
| Deux sites « −23 % » dont un seul mis en avant | égalité à l'arrondi | assumer le départage au centième, ou mettre en avant les deux et l'écrire |
| Valeur négative (production nette d'un site solaire) | barres horizontales partant de zéro vers la droite | traiter explicitement : barres divergentes, ou exclure avec une note |
| Barre invisible pour une petite valeur | longueur de moins d'un pixel | longueur minimale de 2 px, valeur toujours écrite |
Règle de décision
- Barres horizontales triées pour comparer des catégories ; courbe pour une évolution dans le temps ; pas de camembert au-delà de trois parts.
- Une couleur de mise en avant, toujours doublée d'un second codage (gras, annotation).
- Si le graphique ne tient pas lisiblement dans un format, on retire de l'information, on ne réduit pas la police.
Exercice
Ajoutez au graphique une marque pour la consommation 2025 de chaque site (un trait vertical fin au bout de l'ancienne longueur), sans légende. Où placez-vous l'explication ?
Voir la solution
Dans la boucle, après la barre :
x25 = xb + lmax * s.conso_2025 / vmax2 # vmax2 = max(conso_2025, conso_2026) sur tous les sites
rects.append(Rect(x25 - 1, cy - epaisseur * 0.7, 2, epaisseur * 1.4, ENCRE_2))Attention : l'échelle doit maintenant couvrir les valeurs 2025 (2 150 pour Hub Centre), sinon le trait sort du panneau. Et l'étiquette de valeur doit se placer après max(long, x25 - xb).
L'explication va dans le titre du graphique (« Consommation 2026 par site, trait : niveau 2025 ») plutôt que dans une légende séparée. Deux séries sans légende ne passent qu'avec ce texte explicite.
Résultat obtenu
Run final du 2026-10-15 21:06 : sur les quatre formats, le graphique passe les contrôles de contraste, de débordement et de lisibilité.
affiche 1240x1754 6 calques 0 bloquant(s) 0 alerte(s)
carre 1080x1080 6 calques 0 bloquant(s) 1 alerte(s)
story 1080x1920 6 calques 0 bloquant(s) 1 alerte(s)
linkedin 1200x627 6 calques 0 bloquant(s) 1 alerte(s)Je sais faire
- Choisir la forme de graphique à partir de la question posée
- Construire des barres triées à zéro avec étiquettes directes
- Doubler une mise en avant colorée d'un second codage
- Retirer de l'information plutôt que réduire la police