Concevoir un tableau de bord qui se lit
Jour 1 · Concevoir et construire · 30 min
Grafana s'apprend en une heure. Produire un écran qu'un exploitant lit en trois secondes demande davantage — et c'est là que se joue l'utilité de tout ce que vous avez construit depuis M1.
Le réflexe est d'afficher tout ce qu'on a. Douze courbes, quatre jauges, deux camemberts. Le résultat est un écran que personne ne regarde — et donc une panne que personne ne voit passer.
La règle d'ordre : la forme d'abord, la couleur en dernier
La plupart des mauvais tableaux de bord commencent par le choix des couleurs. C'est l'inverse qu'il faut faire.
- Quel est le travail de la donnée ? Montrer une grandeur, distinguer des identités, signaler une polarité, donner un chiffre unique, montrer une évolution.
- La forme découle du travail. Une évolution appelle une courbe ; un état courant appelle un chiffre, pas un graphique.
- La couleur vient ensuite, et seulement pour le travail qu'elle fait.
« Nombre de capteurs en ligne : 12 / 12 » n'a pas besoin d'une courbe. Un chiffre suffit, et il se lit plus vite.
Un graphique à une seule barre, ou un camembert à deux parts, sont toujours une erreur : le nombre est le graphique.
La hiérarchie de lecture
Trois niveaux, du plus rapide au plus détaillé. L'exploitant descend seulement s'il a une raison de le faire.
L'erreur numéro un : le double axe
Tracer la température et l'humidité sur un même graphique avec deux échelles verticales est tentant — Grafana le propose. C'est pourtant faux : l'alignement des deux échelles est arbitraire, donc le graphique invente une corrélation qui n'est pas dans les données. Changez une borne, la corrélation disparaît.
La parade : deux graphiques superposés partageant l'axe du temps, ou deux grandeurs ramenées à une base commune sur un seul axe.
La couleur, par le travail qu'elle fait
| Travail | Type de palette | Règle |
|---|---|---|
| Identité — distinguer des capteurs | catégorielle | Teintes attribuées dans un ordre fixe, jamais recyclées |
| Magnitude — une carte de chaleur | séquentielle | Une seule teinte, du clair au foncé. Jamais d'arc-en-ciel |
| Polarité — écart à une consigne | divergente | Deux teintes opposées + un gris neutre au milieu |
| État — normal, alerte, critique | statut | Réservée. Jamais réutilisée pour une série de données |
La couleur suit l'entité, pas son rang. Si un filtre change le nombre de séries affichées, les couleurs des survivantes ne doivent pas bouger. Un exploitant qui a appris « Lyon est bleu » serait trompé.
Le rouge et le vert sont réservés aux états. Les utiliser pour distinguer deux capteurs, c'est faire croire que l'un va mal.
La palette du parcours
Quatre teintes, dans cet ordre. Elle a été vérifiée : bande de luminosité, saturation suffisante, et surtout séparation sous déficience visuelle — la paire la plus proche reste distinguable pour un daltonien de type protanope.
série 1 #2a78d6 bleu
série 2 #eb6834 orange
série 3 #1baf7a vert d'eau
série 4 #eda100 jauneLe vert d'eau et le jaune ont un contraste inférieur à 3:1 sur fond clair. Ce n'est pas bloquant, mais cela oblige à une étiquette visible : légende présente, et libellé direct sur les courbes concernées. La couleur seule ne doit jamais porter l'identité.
Ce qui reste discret
- Traits fins. 2 px pour une courbe, pas davantage.
- Grille en filet léger, jamais en pointillés — le pointillé ajoute du bruit et se confond avec une série.
- Pas de valeur sur chaque point. Étiqueter le dernier point, ou les extrêmes, suffit.
- Le texte garde sa couleur de texte. Une valeur écrite dans la couleur de sa série devient illisible et redondante avec la pastille de légende.
Grafana en pratique
Jour 1 · Concevoir et construire · 20 min
Grafana lit des sources de données et affiche des panneaux. Trois notions suffisent à en faire le tour.
| Notion | Ce que c'est |
|---|---|
| Source de données | La connexion à InfluxDB, déclarée une fois |
| Panneau | Une requête + une représentation. C'est l'unité de base |
| Variable | Une valeur choisie en haut de l'écran qui s'injecte dans les requêtes |
Les types de panneaux utiles en supervision
| Panneau | Pour quoi | Piège |
|---|---|---|
| Time series | Évolution d'une grandeur | Ne pas y empiler deux échelles |
| Stat | Un chiffre d'état : moyenne, dernier relevé | Préciser la fonction employée |
| Gauge | Une valeur par rapport à des bornes | Inutile si les bornes n'ont pas de sens physique |
| Table | Le détail par capteur | Le meilleur choix au-delà de sept séries |
| State timeline | L'historique des états en ligne / hors ligne | — |
Plutôt que douze tableaux de bord, un seul avec une variable site et une variable capteur. Grafana peuple ces listes en interrogeant la base : les nouveaux capteurs apparaissent tout seuls.
Une requête Flux dans Grafana
C'est exactement le Flux de M3, avec les variables injectées par ${...} :
from(bucket: "releves")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "temperature")
|> filter(fn: (r) => r.site == "${{site}}")
|> aggregateWindow(every: v.windowPeriod, fn: mean)v.timeRangeStart et v.timeRangeStop reprennent le sélecteur de période en haut de l'écran. v.windowPeriod s'adapte à la largeur du panneau : le graphique reste lisible qu'on regarde une heure ou six mois, sans rien changer à la requête.
Quiz — Conception et Grafana
Jour 1 · Concevoir et construire · 8 min
1. Vous voulez afficher température et humidité sur la même période. Comment procéder ?
2. « 12 capteurs en ligne sur 12 ». Quelle représentation ?
3. Vous filtrez pour n'afficher que trois capteurs sur huit. Les couleurs des trois restants doivent :
4. Pourquoi ne pas utiliser rouge et vert pour distinguer deux sites ?
5. À quoi sert v.windowPeriod ?
TP 1 — Premiers panneaux
Jour 1 · Concevoir et construire · 1 h
Brancher Grafana sur InfluxDB et construire les quatre chiffres d'état.
Votre pile de M3 doit tourner : broker, capteurs, InfluxDB alimenté par Telegraf.
- Ajoutez Grafana à votre
docker-compose.ymlplutôt que de le lancer à part — la pile doit rester reproductible :grafana: image: grafana/grafana:11.1.0 ports: ["3000:3000"] environment: - GF_SECURITY_ADMIN_PASSWORD=thermeo2026 volumes: - grafana-data:/var/lib/grafana - Ouvrez
http://localhost:3000. Dans Connections → Data sources, ajoutez InfluxDB en choisissant le langage de requête Flux, l'URL de votre conteneur, l'organisationthermeoet votre jeton. - Cliquez sur Save & test : la connexion doit être confirmée avant d'aller plus loin.
- Créez un tableau de bord et, avant de toucher à Grafana, écrivez sur papier les quatre chiffres qu'un exploitant de Thermeo doit voir en trois secondes. Justifiez chacun.
- Construisez le premier panneau Stat : moyenne du parc sur la dernière heure.
from(bucket: "releves") |> range(start: -1h) |> filter(fn: (r) => r._measurement == "temperature") |> mean() - Construisez les trois autres : nombre de capteurs ayant émis dans les dix dernières minutes, nombre d'alertes actives, âge du dernier relevé reçu.
- Disposez-les en une seule rangée en haut de l'écran, comme dans le schéma de la leçon.
- Vérifiez la lisibilité : demandez à quelqu'un de regarder l'écran trois secondes, puis de dire si tout va bien. S'il hésite, la disposition est à revoir.
- Pourquoi préciser la fonction d'agrégation dans le titre d'un panneau Stat ?
- Vos quatre chiffres suffisent-ils à décider s'il faut intervenir ?
TP 2 — Le tableau de bord de supervision
Jour 1 · Concevoir et construire · 1 h 30
Assembler un écran complet, avec variables et palette maîtrisée.
- Ajoutez sous vos chiffres d'état un panneau Time series des températures sur 24 heures, une courbe par site.
- Appliquez la palette de la leçon. Dans Panel → Overrides, fixez la couleur par série nommée, jamais par ordre d'affichage :
Lyon -> #2a78d6 Paris -> #eb6834 Nantes -> #1baf7a Lille -> #eda100 - Vérifiez la règle : filtrez pour masquer Lyon, et confirmez que Paris reste orange au lieu de devenir bleu.
- Réglez l'épaisseur des traits à 2 px, retirez le remplissage sous les courbes, et passez la grille en filet léger — jamais en pointillés.
- Activez la légende et le libellé de la dernière valeur. C'est ce qu'impose l'avertissement de contraste signalé dans la leçon : l'identité ne doit pas reposer sur la seule couleur.
- Créez une variable
sitealimentée par la base, dans Dashboard settings → Variables, de type Query :import "influxdata/influxdb/schema" schema.tagValues(bucket: "releves", tag: "site") - Injectez-la dans vos requêtes avec
${{site}}et vérifiez que le sélecteur apparaît en haut de l'écran. Ajoutez un nouveau capteur sur un nouveau site : il doit apparaître dans la liste sans rien modifier. - Ajoutez en bas un panneau Table avec la dernière valeur de chaque capteur et son ancienneté. Au-delà de sept séries, c'est la table qui reste lisible, pas le graphique.
- Faites délibérément l'erreur : ajoutez un panneau où la température et l'humidité partagent un graphique à deux axes. Regardez-le, puis remplacez-le par deux panneaux empilés. Conservez une capture des deux pour votre rendu.
# a produire puis remplacer : le double axe est l'anti-patron le plus courant - Exportez le tableau de bord en JSON et versionnez-le.
- Sur vos deux captures, qu'est-ce que le double axe suggérait que les deux panneaux ne suggèrent plus ?
- Au-delà de combien de capteurs votre graphique devient-il illisible ?
- Comment un exploitant daltonien lit-il votre écran ?
Point de contrôle — fin du jour 1
Jour 1 · Concevoir et construire · 15 min
docker-compose.yml et lit InfluxDBsite alimente les requêtes depuis la baseAlerter sans noyer
Jour 2 · Alerter et sortir du cadre · 20 min
Vous savez déjà déclencher une alerte : en M2 dans l'objet, en M3 dans Node-RED. Grafana en propose une troisième, et il faut savoir laquelle choisir.
| Où | Réagit en | Voit | Bon pour |
|---|---|---|---|
| Objet (M2) | immédiat | sa seule mesure | Sécurité, réaction locale |
| Node-RED (M3) | quelques secondes | tous les messages qui passent | Corrélation entre capteurs, actions |
| Grafana (M4) | à chaque évaluation, souvent la minute | l'historique en base | Tendances, absence de données, seuils sur durée |
Alerter sur une durée : « la température dépasse 24 °C depuis plus de quinze minutes ». Ni l'objet ni le moteur de règles n'ont l'historique pour cela.
Et alerter sur une absence : « aucune donnée reçue depuis dix minutes » — la condition No Data, qui traite le silence comme un signal.
Les états d'une alerte
| État | Signification |
|---|---|
| Normal | La condition n'est pas remplie |
| Pending | Elle l'est, mais pas encore assez longtemps |
| Alerting | Elle l'est depuis la durée exigée — la notification part |
| No Data | La requête ne renvoie rien : à traiter explicitement |
L'état Pending joue exactement le rôle de l'hystérésis vue en M2 : il empêche qu'un franchissement passager déclenche une alerte. Une condition vraie pendant 5 minutes avant de basculer élimine l'essentiel du bavardage.
Ce qui rend une alerte utile
- Elle est actionnable. Si personne ne peut rien faire en la recevant, elle ne doit pas exister.
- Elle dit où et quoi. « Alerte température » ne vaut rien ; « capt-07, étage 2, Lyon : 26,3 °C depuis 20 min » se traite.
- Elle se referme. Une alerte qui ne signale jamais son retour à la normale laisse croire au problème permanent.
C'est le vrai risque, et il est silencieux. Un exploitant qui reçoit trente notifications par jour finit par toutes les ignorer — y compris celle qui comptait.
Mieux vaut trois alertes justes que trente approximatives. Si une alerte se déclenche souvent sans action derrière, ce n'est pas le seuil qu'il faut ajuster : c'est l'alerte qu'il faut supprimer.
TP 3 — Des alertes qu'on peut suivre
Jour 2 · Alerter et sortir du cadre · 1 h
Créer des règles d'alerte durables et annoter les incidents.
- Créez une première règle dans Alerting → Alert rules : température moyenne supérieure à 24 °C.
from(bucket: "releves") |> range(start: -15m) |> filter(fn: (r) => r._measurement == "temperature") |> group(columns: ["capteur"]) |> mean() - Réglez la condition sur
IS ABOVE 24, l'évaluation toutes les minutes, et surtout « for » à 15 minutes — c'est ce délai qui joue le rôle de l'hystérésis. - Rédigez le résumé de l'alerte avec les étiquettes, pour qu'elle soit actionnable :
Capteur {{{{ $labels.capteur }}}} — {{{{ $values.B }}}} °C depuis 15 min - Faites monter un capteur au-dessus du seuil et observez la bascule Normal → Pending → Alerting. Chronométrez : la notification ne doit pas partir avant les quinze minutes.
- Créez une seconde règle sur l'absence de données : configurez le comportement
No Dataen Alerting, puis coupez un capteur et vérifiez le déclenchement. - Comparez avec les deux autres mécanismes que vous connaissez : le testament MQTT de M1 et la requête de détection de M3. Chronométrez les trois sur la même coupure et consignez les délais.
- Configurez un point de contact — un webhook vers un service de test suffit — et vérifiez qu'une notification part réellement.
- Ajoutez enfin une annotation automatique : les périodes d'alerte doivent apparaître en surbrillance sur vos graphiques, pour relire un incident après coup.
- Vos trois mécanismes de détection ont des délais différents. Lequel garderiez-vous, et faut-il vraiment les trois ?
- Quelle alerte de votre tableau de bord risque de créer de la fatigue, et pourquoi ?
- Une alerte se déclenche la nuit. Qui la reçoit, et que peut-il faire ?
Quand Grafana ne suffit plus
Jour 2 · Alerter et sortir du cadre · 20 min
Grafana est excellent pour superviser. Il l'est beaucoup moins dès qu'on sort de ce cadre.
| Besoin | Grafana | Interface sur mesure |
|---|---|---|
| Superviser, explorer l'historique | idéal | coûteux à refaire |
| Écran public affiché en permanence | possible mais rigide | adapté |
| Piloter — envoyer une consigne | non prévu | nécessaire |
| Intégrer à un logiciel métier | iframe, sans plus | natif |
| Utilisateurs non techniques | interface dense | à leur mesure |
Est-ce que l'utilisateur doit agir depuis l'écran ? S'il ne fait que regarder, Grafana suffit et coûte une journée. S'il doit envoyer une consigne, valider un incident ou déclencher une action, il faut une interface — et un développement.
L'architecture minimale
Trois pièces, pas davantage :
- Une API qui interroge InfluxDB et expose des données prêtes à afficher. Elle ne renvoie jamais la base brute au navigateur.
- Une page web qui les affiche.
- Un canal temps réel pour que la page se mette à jour sans rechargement.
Interrogation périodique ou WebSocket
| Interrogation périodique | WebSocket | |
|---|---|---|
| Mise en œuvre | quelques lignes | un serveur qui maintient les connexions |
| Latence | la période choisie | immédiate |
| Charge | constante, même sans donnée nouvelle | proportionnelle aux événements |
| Quand le retenir | rafraîchissement de quelques secondes | vraie réactivité requise |
Pour un tableau de bord qui se rafraîchit toutes les cinq secondes, l'interrogation périodique est plus simple, plus robuste et se débogue trivialement. Le WebSocket se justifie quand la seconde compte — une alarme, un pilotage.
Le navigateur ne doit jamais recevoir le jeton InfluxDB. C'est l'API qui détient les identifiants et n'expose que ce dont la page a besoin. Un jeton dans du JavaScript est un jeton public.
Quiz — Alertes et interfaces
Jour 2 · Alerter et sortir du cadre · 7 min
1. Quelle alerte seul Grafana peut porter ?
2. À quoi correspond l'état Pending ?
3. Quel est le principal risque d'un système d'alerte mal réglé ?
4. Votre page web doit afficher les relevés. Où mettre le jeton InfluxDB ?
TP 4 — Une interface sur mesure
Jour 2 · Alerter et sortir du cadre · 1 h 30
Construire l'API et la page temps réel, et savoir quand cela se justifie.
- Écrivez l'API avec FastAPI. Elle interroge InfluxDB et n'expose que le nécessaire :
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from influxdb_client import InfluxDBClient import os app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"]) cli = InfluxDBClient(url="http://localhost:8086", token=os.environ["INFLUX_TOKEN"], # jamais en dur org="thermeo") FLUX = ''' from(bucket: "releves") |> range(start: -10m) |> filter(fn: (r) => r._measurement == "temperature") |> group(columns: ["capteur"]) |> last() ''' @app.get("/api/etat") def etat(): tables = cli.query_api().query(FLUX) return [{"capteur": r.values["capteur"], "valeur": r.get_value(), "instant": r.get_time().isoformat()} for t in tables for r in t.records] - Lancez et vérifiez :
INFLUX_TOKEN="<jeton>" uvicorn api:app --reload --port 8000 curl -s http://localhost:8000/api/etat | python3 -m json.tool - Écrivez une page HTML autonome qui interroge cette API toutes les cinq secondes et affiche une tuile par capteur : nom, valeur, ancienneté. Reprenez la palette et la hiérarchie de la leçon du jour 1.
- Traitez le cas de l'erreur : si l'API ne répond pas, la page doit le dire clairement plutôt que d'afficher des valeurs périmées comme si elles étaient fraîches. C'est le défaut le plus dangereux d'un écran de supervision.
- Grisez toute tuile dont la dernière valeur date de plus de deux minutes : une donnée périmée affichée comme actuelle vaut moins que pas de donnée.
- Ajoutez un endpoint
POST /api/consignequi publie une consigne en message MQTT retenu — le motif du jumeau vu en M3 — et un bouton dans la page. C'est ce que Grafana ne sait pas faire. - Ajoutez l'API et la page à votre
docker-compose.yml. - Rédigez une note d'une page : ce que vous mettez dans Grafana, ce que vous mettez dans l'interface sur mesure, et pourquoi. Chiffrez la charge de développement de chaque option.
- Vous avez écrit vingt lignes d'API et une page. Combien de jours pour égaler Grafana ?
- Votre page interroge toutes les cinq secondes. Que change un WebSocket, concrètement ?
- Comment un exploitant distingue-t-il « 21,4 °C il y a 3 s » de « 21,4 °C il y a 3 h » sur votre écran ?
Point de contrôle — fin du module M4
Jour 2 · Alerter et sortir du cadre · 20 min
Votre encadrant regardera votre tableau de bord trois secondes et vous dira ce qu'il en a retenu. Si ce n'est pas ce que vous vouliez montrer, c'est la conception qu'il faut revoir, pas la configuration.
M5 enchaîne mardi 18 : sécurité IoT et déploiement industriel.