Séance 4 — Faire travailler plusieurs vues

Séance 4 sur 63 heures

Question directrice

Comment plusieurs graphiques, filtres et annotations peuvent-ils former un seul outil de compréhension plutôt qu’une collection de widgets ?

Vous allez structurer un mini-dashboard Chart.js, choisir entre superposition et petits multiples, construire un graphique mixte, ajouter une référence avec un plugin, mettre les vues à jour avec des contrôles HTML et relier une sélection à une vue de détail. La séance se termine par une page cohérente, partageable et raisonnablement robuste.

Point de départ : vous savez créer, mettre à jour et détruire une instance Chart.js. Vous disposez d’un bar chart des stations et d’une courbe des départs par heure.

Le contrat de cette séance

Un dashboard n’est pas « trois graphiques sur la même page ». C’est une interface où :

  • chaque vue répond à une sous-question identifiable;
  • les vues emploient des unités, couleurs et filtres cohérents;
  • les interactions modifient clairement le périmètre de l’analyse;
  • l’état courant est visible même sans survol;
  • la page possède un ordre de lecture et une sortie utile.
Temps en directActivitéRésultat
0:00–0:20Décomposer une décision en sous-questionsUne architecture d’information sur papier
0:20–0:50Superposition et petits multiplesUne comparaison adaptée au nombre de séries
0:50–1:20Graphique mixte + moyenne mobileDeux marques, une seule unité défendable
1:20–1:30Pause
1:30–2:00Annotation et titre dynamiqueUn seuil visible et contextualisé
2:00–2:30Filtrer, mettre à jour, synchroniserUn état partagé entre données et interface
2:30–3:00Atelier dashboardUne page cohérente ou un prototype argumenté

1. Commencer par une décision, pas par une grille

Les dashboards naissent souvent d’une liste : « il nous faut le total, la tendance, la répartition, une carte et un tableau ». Cette liste décrit des objets d’interface, pas une situation d’usage. Le résultat accumule des vues sans expliquer comment elles se répondent.

Prenons une situation BIXI : une personne responsable de la répartition des vélos ouvre la page le matin. Elle veut savoir où concentrer son attention. On peut décomposer :

  1. Vue d’ensemble — le volume s’écarte-t-il du fonctionnement habituel ?
  2. Localisation — quelles stations concentrent les départs ou les retours ?
  3. Explication — à quels moments et pour quels profils l’écart apparaît-il ?
  4. Détail — quelles observations faut-il vérifier ou exporter ?

Cette séquence produit une hiérarchie : signal, contexte, détail. Elle ne demande pas nécessairement quatre graphiques. Un titre dynamique, une vue principale et un tableau filtré peuvent suffire.

Why, how, what

Brehmer et Munzner proposent de décrire une tâche de visualisation en séparant :

  • pourquoi la personne agit : découvrir, présenter, confirmer, décider;
  • comment elle agit : sélectionner, naviguer, filtrer, agréger, comparer;
  • sur quoi elle agit : attributs, objets, liens, chemins, ensembles.

Appliqué à notre cas :

Pourquoi : découvrir une station qui demande une intervention
Comment  : filtrer une période, comparer les stations, sélectionner une station
Quoi     : trajets agrégés par station et par heure

Ce langage déplace la conversation. « Ajoutons une carte » devient « la position géographique aide-t-elle vraiment la décision ? ». « Ajoutons un filtre » devient « quelle modification du périmètre la personne doit-elle contrôler ? ».

Architecture minimale

Pour le mini-dashboard du cours, nous retiendrons :

  1. un titre qui nomme la période et le filtre actifs;
  2. un petit nombre de contrôles HTML natifs;
  3. une vue principale de comparaison;
  4. une vue temporelle ou de détail;
  5. une description de l’observation et un accès aux données.

Le nombre de cartes n’est pas un indicateur de qualité. Une page plus courte qui répond au besoin est un dashboard plus abouti.


2. Superposer ou répéter

Lorsque plusieurs séries partagent un axe, deux stratégies fréquentes s’offrent à nous : les superposer dans un graphique ou les séparer en petits multiples utilisant la même grammaire.

Superposition

La superposition facilite certaines comparaisons directes : à la même date, quelle série est la plus élevée ? Où les lignes se croisent-elles ? Elle économise de l’espace et partage naturellement les axes.

Elle devient coûteuse quand :

  • les lignes sont nombreuses;
  • elles se croisent fréquemment;
  • leurs couleurs sont difficiles à distinguer;
  • les écarts sont faibles;
  • les labels reposent sur une légende éloignée;
  • une série domine l’échelle et écrase les autres.

Le « spaghetti chart » n’est pas défini par un seuil universel de cinq lignes. Il apparaît lorsque la tâche de suivi d’une série ou de comparaison de formes devient trop coûteuse.

Petits multiples

Les petits multiples répètent le même graphique pour différents sous-ensembles. Ils libèrent chaque série des croisements et permettent de comparer les formes par alignement spatial. Trois conditions maintiennent la comparaison :

  1. mêmes axes;
  2. même taille de panneau;
  3. même encodage et même ordre.

Si chaque panneau recalcule son propre domaine vertical, toutes les courbes peuvent sembler également variables. Cette échelle locale peut servir l’étude interne de chaque série, mais détruit la comparaison des magnitudes. Il faut l’indiquer explicitement ou fournir les deux modes.

Implémentation avec Chart.js

Chart.js ne possède pas un « type small multiples » central. On crée plusieurs canvas à partir d’une configuration partagée :

<div id="neighborhood-grid" class="small-multiples"></div>
.small-multiples {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

.small-multiple {
  min-height: 14rem;
}
const sharedOptions = {
  responsive: true,
  maintainAspectRatio: false,
  animation: false,
  scales: {
    y: {
      min: 0,
      max: 1000,
    },
  },
  plugins: {
    legend: { display: false },
  },
};

function renderSmallMultiples(series) {
  const grid = document.querySelector("#neighborhood-grid");
  grid.replaceChildren();

  return series.map((item) => {
    const figure = document.createElement("figure");
    const caption = document.createElement("figcaption");
    const container = document.createElement("div");
    const canvas = document.createElement("canvas");

    caption.textContent = item.name;
    container.className = "small-multiple";
    canvas.setAttribute("role", "img");
    canvas.setAttribute("aria-label", item.description);

    container.append(canvas);
    figure.append(caption, container);
    grid.append(figure);

    return new Chart(canvas, {
      type: "line",
      data: {
        labels: item.labels,
        datasets: [{ data: item.values }],
      },
      options: structuredClone(sharedOptions),
    });
  });
}

Nous retournons les instances pour pouvoir les détruire ou les mettre à jour. structuredClone évite que des modifications locales partagent par erreur le même objet imbriqué.


3. Organiser le code comme le raisonnement

Une seule fonction de 300 lignes fonctionne jusqu’au premier filtre. Séparons le dashboard en quatre couches :

src/
├── app.js
├── data/
│   ├── loadTrips.js
│   └── transformTrips.js
├── charts/
│   ├── stationChart.js
│   ├── timelineChart.js
│   └── commonOptions.js
└── ui/
    ├── filters.js
    └── summary.js
  • loadTrips.js connaît le réseau et le CSV.
  • transformTrips.js connaît le grain, les filtres et agrégations.
  • charts/ connaît la grammaire Chart.js.
  • ui/ connaît les contrôles et le texte visible.
  • app.js coordonne l’état.

Cette séparation correspond au modèle du cours : données → tâche → encodage → interaction. Elle facilite également les tests. Une fonction aggregateByStation(rows) peut être vérifiée sans navigateur.

Un état explicite

const state = {
  riderType: "all",
  startDate: null,
  endDate: null,
  selectedStation: null,
};

Chaque interaction modifie l’état, puis un seul chemin met à jour les vues. Évitez que le bouton « Membres » modifie directement trois tableaux et deux titres dans des endroits différents.

function setState(patch) {
  Object.assign(state, patch);
  updateDashboard();
}

Pour un petit projet, cet objet suffit. Il n’est pas nécessaire d’ajouter une bibliothèque de gestion d’état. La cohérence vient d’une source de vérité claire, pas du nombre de dépendances.


4. Graphique mixte : deux marques, une question

Chart.js autorise un type par dataset. Un graphique peut donc combiner des barres quotidiennes et une ligne de moyenne mobile :

function movingAverage(values, windowSize = 7) {
  return values.map((_, index) => {
    if (index < windowSize - 1) return null;

    const window = values.slice(index - windowSize + 1, index + 1);
    const total = window.reduce((sum, value) => sum + value, 0);
    return total / window.length;
  });
}
const dailyCounts = days.map((day) => day.count);

const mixedData = {
  labels: days.map((day) => day.label),
  datasets: [
    {
      type: "bar",
      label: "Départs quotidiens",
      data: dailyCounts,
      backgroundColor: "rgba(184, 134, 11, 0.35)",
      borderColor: "#b8860b",
      borderWidth: 1,
    },
    {
      type: "line",
      label: "Moyenne mobile sur 7 jours",
      data: movingAverage(dailyCounts, 7),
      borderColor: "#2f3338",
      borderWidth: 3,
      pointRadius: 0,
      spanGaps: false,
    },
  ],
};
const timelineChart = new Chart(canvas, {
  type: "bar",
  data: mixedData,
  options: {
    interaction: {
      mode: "index",
      intersect: false,
    },
    scales: {
      y: {
        beginAtZero: true,
        title: { display: true, text: "Nombre de départs" },
      },
    },
  },
});

Les deux marques utilisent la même unité et la même échelle. Les barres montrent la variation quotidienne; la ligne réduit le bruit pour révéler une tendance. La moyenne mobile n’est pas une deuxième donnée brute : c’est une transformation. Son nom et sa fenêtre doivent être visibles.

Pourquoi éviter l’axe double par défaut

Un exemple courant combine des ventes en dollars et un taux en pourcentage avec deux axes verticaux. Le résultat peut être utile, mais la forme des deux séries dépend fortement des domaines choisis. En modifiant les axes, on peut faire apparaître ou disparaître une corrélation.

Avant un double axe, essayez :

  • deux panneaux alignés sur le même axe temporel;
  • une normalisation clairement annoncée;
  • un indice base 100;
  • une annotation textuelle de la relation;
  • une interaction qui affiche une mesure à la fois.

Utilisez un axe double seulement lorsque la comparaison simultanée sert une tâche claire, que les unités sont fortement distinguées et que la relation n’est pas suggérée par un alignement arbitraire.


5. Annoter une référence avec un plugin

Une ligne de seuil, une période exceptionnelle ou une cible peut être dessinée manuellement dans un plugin local. Pour gagner du temps et disposer de labels, nous utiliserons chartjs-plugin-annotation, compatible avec Chart.js 4.

npm install chartjs-plugin-annotation
import Chart from "chart.js/auto";
import annotationPlugin from "chartjs-plugin-annotation";

Chart.register(annotationPlugin);

Puis dans les options :

const target = 600;

options: {
  plugins: {
    annotation: {
      annotations: {
        targetLine: {
          type: "line",
          yMin: target,
          yMax: target,
          borderColor: "#2f3338",
          borderWidth: 2,
          borderDash: [6, 4],
          label: {
            display: true,
            content: `Référence : ${target} départs`,
            position: "end",
          },
        },
      },
    },
  },
}

Une annotation doit répondre à une question : pourquoi 600 ? Est-ce une capacité, une cible, une moyenne historique ou un seuil réglementaire ? Sans source, une ligne de référence ressemble à une vérité institutionnelle.

Mettre à jour l’annotation avec le filtre

Si le filtre passe de tous les trajets aux membres, la référence peut changer :

function updateReference(chart, value, label) {
  const line = chart.options.plugins.annotation.annotations.targetLine;
  line.yMin = value;
  line.yMax = value;
  line.label.content = label;
}

La ligne, son label et le texte d’accompagnement doivent changer ensemble. Une référence périmée est plus dangereuse qu’une référence absente.


6. Filtrer signifie redéfinir le périmètre

Manipulez l’exemple. Observez que les barres ne sont pas seules à changer : ordre, titre, référence et description suivent le filtre.

Contrôle HTML natif

Pour une liste courte de choix exclusifs, des boutons ou un <select> conviennent. Utilisons un groupe de boutons :

<fieldset class="filter-group">
  <legend>Population affichée</legend>
  <button type="button" data-rider-type="all" aria-pressed="true">
    Tous
  </button>
  <button type="button" data-rider-type="member" aria-pressed="false">
    Membres
  </button>
  <button type="button" data-rider-type="casual" aria-pressed="false">
    Occasionnels
  </button>
</fieldset>

Le fieldset et la légende rendent le groupe compréhensible. aria-pressed communique l’état des boutons bascule.

document.querySelectorAll("[data-rider-type]").forEach((button) => {
  button.addEventListener("click", () => {
    setState({ riderType: button.dataset.riderType });

    document.querySelectorAll("[data-rider-type]").forEach((candidate) => {
      candidate.setAttribute(
        "aria-pressed",
        String(candidate === button)
      );
    });
  });
});

Une mise à jour centralisée

function updateDashboard() {
  const filteredTrips = filterTrips(allTrips, state);
  const stations = topStations(filteredTrips, 10);
  const daily = countByDay(filteredTrips);

  updateStationChart(stationChart, stations);
  updateTimelineChart(timelineChart, daily);
  updateSummary({ state, stations, daily });
  renderDataTable(stations);
}
function updateStationChart(chart, stations) {
  chart.data.labels = stations.map((row) => row.station);
  chart.data.datasets[0].data = stations.map((row) => row.count);

  chart.options.plugins.title.text = titleForState(state);
  updateReference(
    chart,
    referenceForState(state),
    referenceLabelForState(state)
  );

  chart.update();
}

Chart.js interpole les anciennes marques vers les nouvelles lorsque update() est appelé. Cette transition peut aider à suivre les changements d’ordre ou de valeur. Si le changement est purement stylistique, update("none") évite une animation sans information.

L’état vide n’est pas une erreur technique

Un filtre peut produire zéro ligne. Ne laissez pas un graphique vide avec des axes mystérieux :

function updateEmptyState(container, rows) {
  const empty = rows.length === 0;
  container.dataset.empty = String(empty);
  container.querySelector("canvas").hidden = empty;
  container.querySelector("[data-empty-message]").hidden = !empty;
}

Le message doit aider : « Aucun trajet dans cette période et pour ce profil. Élargissez les dates ou choisissez Tous. »


7. Relier une vue d’ensemble à un détail

Une interaction utile réduit la distance entre une question et son détail. Cliquer une station dans le bar chart peut filtrer la série temporelle et le tableau.

stationChart.options.onClick = (event) => {
  const [element] = stationChart.getElementsAtEventForMode(
    event,
    "nearest",
    { intersect: true },
    true
  );

  if (!element) return;

  const station = stationChart.data.labels[element.index];
  setState({ selectedStation: station });
};

Ce mécanisme fonctionne à la souris ou au toucher, mais les barres dessinées dans le canvas ne deviennent pas automatiquement des boutons clavier. Il faut fournir un contrôle HTML équivalent :

<label for="station-select">Station détaillée</label>
<select id="station-select"></select>
stationSelect.addEventListener("change", () => {
  setState({ selectedStation: stationSelect.value || null });
});

Les deux entrées doivent partager le même état. Une sélection dans le graphique met à jour le <select>; une sélection clavier met à jour le graphique.

Montrer l’état sélectionné

Ne comptez pas uniquement sur la couleur d’une barre. Ajoutez un texte :

<p id="selection-summary" aria-live="polite">
  Toutes les stations sont affichées.
</p>
selectionSummary.textContent = state.selectedStation
  ? `Vue détaillée : ${state.selectedStation}`
  : "Toutes les stations sont affichées.";

L’état devient perceptible, copiable et annoncé par les technologies d’assistance.


8. Animation : conserver l’identité, pas occuper le temps

Chart.js anime les mises à jour par défaut. Une transition est utile lorsqu’elle aide à répondre :

  • quelle barre a changé de rang ?
  • quel point est devenu cette nouvelle position ?
  • la série monte-t-elle progressivement ou change-t-elle de régime ?

Elle nuit lorsqu’elle retarde une consultation répétée, fait rebondir des barres sans sens ou anime des dizaines de vues à chaque filtre.

const reducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

const commonOptions = {
  animation: reducedMotion
    ? false
    : {
        duration: 350,
        easing: "easeOutCubic",
      },
};

La durée de 350 ms est un point de départ, pas une norme. Testez le rythme avec la fréquence réelle d’utilisation. Une page consultée toutes les trente secondes tolère moins de cérémonie qu’une présentation guidée.

La séance 6 reviendra sur l’object constancy, les transitions D3 et les données vivantes. Ici, retenez seulement : l’animation doit rendre un changement plus facile à suivre.


9. Plugins : étendre avec une raison

Un plugin peut :

  • dessiner avant ou après les datasets;
  • modifier les options;
  • créer une légende HTML;
  • annoter une valeur;
  • afficher des labels;
  • intervenir dans le cycle de mise à jour.

Avant d’ajouter une dépendance, vérifiez :

  1. compatibilité avec Chart.js 4;
  2. date et activité de maintenance;
  3. taille et coût de configuration;
  4. comportement avec le responsive et le mode sombre;
  5. accessibilité de ce qui est dessiné sur canvas;
  6. possibilité d’obtenir le même résultat avec du HTML voisin.

chartjs-plugin-datalabels peut afficher des valeurs sur les marques. Il est utile pour un petit bar chart, mais il produit vite des collisions et les labels restent dessinés sur le canvas. Un tableau, un label HTML direct ou un tooltip peut être plus robuste.

Un petit plugin local

Pour comprendre le mécanisme, voici un plugin qui écrit « Données insuffisantes » au centre lorsque le dataset est vide :

const emptyStatePlugin = {
  id: "emptyState",
  afterDraw(chart, _args, options) {
    const hasData = chart.data.datasets.some((dataset) => {
      return dataset.data.some((value) => value !== null);
    });

    if (hasData) return;

    const { ctx, chartArea } = chart;
    ctx.save();
    ctx.fillStyle = options.color ?? "#666";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(
      options.message ?? "Aucune donnée",
      (chartArea.left + chartArea.right) / 2,
      (chartArea.top + chartArea.bottom) / 2
    );
    ctx.restore();
  },
};
new Chart(canvas, {
  // ...
  plugins: [emptyStatePlugin],
  options: {
    plugins: {
      emptyState: {
        message: "Aucune donnée pour ces filtres",
      },
    },
  },
});

Pour un produit, préférez quand même un message HTML visible et accessible. Ce plugin sert à comprendre le cycle de dessin, pas à remplacer l’interface.


10. Performance : une pointe, pas un chapitre caché

En 18 heures, nous ne pouvons pas traiter en profondeur les workers, la virtualisation ou le profilage GPU. Quelques décisions préviennent néanmoins les problèmes les plus courants.

Ne pas dessiner plus de points que de pixels utiles

Un graphique large de 800 pixels ne peut pas montrer distinctement 100 000 points temporels. Agrégez selon la question ou utilisez la décimation.

Chart.js fournit un plugin de décimation pour certaines courbes :

options: {
  parsing: false,
  normalized: true,
  plugins: {
    decimation: {
      enabled: true,
      algorithm: "min-max",
    },
  },
}

Les conditions sont strictes : ligne, axe x linéaire ou temporel, données déjà dans le format interne et parsing: false. La documentation doit être vérifiée avant de copier l’option.

Réutiliser l’instance

Pour un filtre, modifiez chart.data et appelez update() au lieu de détruire et recréer le canvas. Détruisez seulement lorsque la vue disparaît ou change d’architecture.

Calculer une fois

Les agrégations coûteuses peuvent être mises en cache par combinaison de filtres. Mais commencez par mesurer dans DevTools. Une optimisation prématurée complexifie l’atelier et peut cacher le véritable goulot : un CSV trop gros chargé dans le navigateur.


11. Responsive : hiérarchie avant rétrécissement

Le layout principal peut rester simple :

.dashboard {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr);
  gap: 1.25rem;
}

.dashboard__overview {
  grid-column: 1 / -1;
}

@media (max-width: 52rem) {
  .dashboard {
    grid-template-columns: 1fr;
  }
}

Le minmax(0, ...) empêche un contenu interne trop large de forcer la grille. À moins de 52 rem, les vues s’empilent dans l’ordre du DOM. Cet ordre doit déjà avoir un sens sans CSS : titre, contrôles, vue principale, détail, table.

Sur mobile, testez :

  • les labels de station;
  • la hauteur de chaque canvas;
  • le geste nécessaire pour ouvrir un tooltip;
  • la visibilité du filtre actif;
  • l’ordre de tabulation;
  • le coût de cinq graphiques animés;
  • la possibilité d’atteindre la table sans parcourir une page interminable.

Un dashboard mobile n’a pas à conserver toute la densité du desktop. Prioriser est une décision de design, pas un échec du responsive.


12. Partager un état, pas seulement une page

Un dashboard interactif peut montrer quelque chose de très précis — « occasionnels, station 7029, jours ouvrables » — tout en conservant une URL identique à la vue initiale. Si une personne copie le lien, son collègue voit un autre périmètre. La visualisation devient difficile à discuter et à reproduire.

Pour les filtres non sensibles et raisonnablement compacts, encodez l’état utile dans l’URL :

/?rider=casual&station=7029&period=weekday

La query string n’est pas une base de données. Elle sert de contrat partageable pour les choix qui définissent la vue.

12.1 Lire l’état en validant les valeurs

Ne copiez pas aveuglément les paramètres dans l’application. Définissez les valeurs autorisées :

const RIDER_TYPES = new Set(["all", "member", "casual"]);
const PERIODS = new Set(["all", "weekday", "weekend"]);

function stateFromUrl(url = new URL(window.location.href)) {
  const riderParam = url.searchParams.get("rider");
  const periodParam = url.searchParams.get("period");
  const stationParam = url.searchParams.get("station");

  return {
    riderType: RIDER_TYPES.has(riderParam) ? riderParam : "all",
    period: PERIODS.has(periodParam) ? periodParam : "all",
    selectedStation: stationParam || null,
  };
}

La validation empêche une faute de frappe de placer l’interface dans un état que les contrôles ne savent pas représenter. Pour une station, vérifiez aussi que l’identifiant existe dans les données chargées avant de l’utiliser.

12.2 Écrire l’état sans recharger

function syncUrl(state) {
  const url = new URL(window.location.href);

  setOrDelete(url.searchParams, "rider", state.riderType, "all");
  setOrDelete(url.searchParams, "period", state.period, "all");
  setOrDelete(
    url.searchParams,
    "station",
    state.selectedStation,
    null
  );

  history.replaceState(state, "", url);
}

function setOrDelete(params, key, value, defaultValue) {
  if (value === defaultValue || value == null) {
    params.delete(key);
  } else {
    params.set(key, value);
  }
}

replaceState met à jour l’URL sans créer une entrée d’historique à chaque clic. Si chaque étape doit pouvoir être retrouvée avec le bouton Retour, pushState peut être pertinent, mais un filtre manipulé dix fois ne devrait généralement pas exiger dix retours.

Appelez syncUrl(state) dans setState, puis dérivez graphiques, titres, résumé et table depuis ce même état :

function setState(patch) {
  Object.assign(state, patch);
  syncUrl(state);
  updateDashboard();
}

12.3 La reproductibilité est aussi narrative

Une URL partageable permet de formuler :

Ouvrez cette vue : les départs occasionnels de fin de semaine à la station 7029 dépassent la référence affichée.

Le destinataire voit exactement le périmètre de l’affirmation. La source, la période et les unités doivent évidemment rester visibles; l’URL ne remplace pas ces informations.

N’encodez jamais de données personnelles, de jetons ou d’information confidentielle dans la query string. Les URL peuvent apparaître dans l’historique, les journaux, les captures et les outils d’analyse. Pour un état volumineux ou sensible, utilisez un identifiant côté serveur avec les permissions appropriées.

Enfin, testez le rechargement : ouvrez directement l’URL dans un nouvel onglet. Les contrôles, titres, annotations et graphiques doivent tous refléter le même état. Si seul le canvas change, le dashboard reste incohérent.

Si vous choisissez pushState, écoutez aussi la navigation Retour / Suivant :

window.addEventListener("popstate", () => {
  Object.assign(state, stateFromUrl());
  updateDashboard();
});

Ne rappelez pas syncUrl depuis ce chemin, sinon l’application peut remplacer immédiatement l’entrée que la personne tente de retrouver. Ce détail montre pourquoi l’URL appartient au modèle d’état : navigation, contrôles et visualisations doivent converger vers la même représentation, sans se déclencher en boucle.

Un bouton « Copier cette vue » peut simplement copier window.location.href, puis confirmer l’action dans un texte visible. Avant de l’ajouter, ouvrez le lien dans une fenêtre privée : vous découvrirez rapidement les valeurs par défaut implicites, les dépendances au stockage local ou les filtres que l’URL ne restaure pas encore. Partager devient ainsi un test de reproductibilité très concret.


13. Exemple d’orchestration complète

Voici un squelette de app.js qui relie chargement, état, graphiques et contrôles :

import Chart from "chart.js/auto";
import annotationPlugin from "chartjs-plugin-annotation";
import { loadTrips } from "./data/loadTrips.js";
import {
  filterTrips,
  countByDay,
  topStations,
} from "./data/transformTrips.js";
import { createStationChart } from "./charts/stationChart.js";
import { createTimelineChart } from "./charts/timelineChart.js";
import { bindFilters } from "./ui/filters.js";
import { updateSummary } from "./ui/summary.js";

Chart.register(annotationPlugin);

const state = {
  riderType: "all",
  selectedStation: null,
};

const allTrips = await loadTrips("/data/bixi-sample.csv");

const stationChart = createStationChart(
  document.querySelector("#station-chart")
);
const timelineChart = createTimelineChart(
  document.querySelector("#timeline-chart")
);

function setState(patch) {
  Object.assign(state, patch);
  updateDashboard();
}

function updateDashboard() {
  const filtered = filterTrips(allTrips, state);
  const stations = topStations(filtered, 10);
  const daily = countByDay(filtered, state.selectedStation);

  updateStationChart(stationChart, stations, state);
  updateTimelineChart(timelineChart, daily, state);
  updateSummary({ state, filtered, stations, daily });
  renderStationTable(stations);
}

bindFilters({ state, setState });
updateDashboard();

L’exemple n’est pas « l’architecture officielle de Chart.js ». C’est une architecture de cours qui rend les responsabilités visibles. Dans un framework, le cycle de vie se branchera sur des composants; le raisonnement restera le même.


14. Atelier — composer un outil, pas remplir une page

Atelier · niveau Observer

Faire l’inventaire des questions

15 à 20 min

Résultat visible : Une carte du dashboard où chaque vue possède une question, une tâche et une décision associée.

Choisissez un dashboard professionnel. Pour chaque graphique ou indicateur, écrivez la question à laquelle il répond. Tracez une flèche entre deux vues seulement si une personne a réellement besoin de passer de l’une à l’autre.

Marquez les éléments qui ne répondent à aucune question claire. Ce sont vos candidats à la suppression, au regroupement ou à la reformulation.

Atelier · niveau Modifier

Ajouter un filtre cohérent

25 à 35 min

Résultat visible : Le dashboard de la séance 3 filtre membres / occasionnels et met à jour valeurs, titres, références, description et table.

  1. Ajoutez un groupe de boutons natifs avec aria-pressed.
  2. Centralisez l’état dans un objet.
  3. Mettez à jour les deux graphiques via chart.data et chart.update().
  4. Traitez l’état vide.
  5. Vérifiez que le texte visible annonce le périmètre actif.

Atelier · niveau Construire

Mini-dashboard de décision

40 à 55 min

Résultat visible : Une page comprenant une vue principale, une vue de détail, une interaction justifiée, une annotation sourcée et une alternative structurée.

Partez d’une décision réelle : surveiller une capacité, comparer des segments, expliquer un changement ou choisir une priorité. Utilisez au maximum trois vues. Si vous avez besoin de plus, justifiez la progression entre elles.

Testez le layout à 320 px, la navigation clavier, l’absence de données et une valeur extrême. Publiez sur une branche Vercel si vous souhaitez partager; ce n’est pas évalué.

Revue croisée

La personne qui reçoit le lien ne demande pas « quelles options Chart.js avez-vous utilisées ? ». Elle tente trois actions :

  1. expliquer en une phrase ce que montre la page;
  2. changer un filtre et dire ce qui a changé;
  3. trouver la source, la période et une limite.

Le retour le plus utile décrit un comportement observé : « je n’ai pas compris que la référence changeait », « j’ai cru que la couleur signalait un problème », « je n’ai pas trouvé la période ». Ces observations valent plus qu’un verdict « joli / pas joli ».


Ce que vous devriez emporter

  1. Un dashboard coordonne des sous-questions; il n’accumule pas des types de graphiques.
  2. Why / how / what aide à traduire une situation professionnelle en tâches de visualisation.
  3. La superposition sert la comparaison directe; les petits multiples servent le suivi de formes et réduisent les croisements.
  4. Des petits multiples comparables partagent axes, taille, ordre et encodage.
  5. Un graphique mixte est plus défendable lorsque ses marques partagent une unité et répondent ensemble à la question.
  6. Une annotation exige une source et doit suivre les filtres.
  7. Un état centralisé permet aux données, titres, références, descriptions et tables de rester cohérents.
  8. Une sélection dans le canvas demande un contrôle HTML équivalent pour le clavier.
  9. L’animation doit aider à suivre l’identité; update("none") convient aux changements qui n’ont pas besoin de transition.
  10. Performance et accessibilité ne sont qu’effleurées ici, mais les habitudes de base doivent déjà être présentes.

Checklist de publication

[ ] La page nomme son public et sa décision principale.
[ ] Chaque vue répond à une sous-question distincte.
[ ] Le filtre actif apparaît dans le texte, pas seulement par couleur.
[ ] Les unités, périodes et dénominateurs sont visibles.
[ ] Les petits multiples partagent une échelle ou annoncent le contraire.
[ ] Toute annotation possède une justification ou une source.
[ ] Le canvas a un nom et une alternative structurée.
[ ] Les états chargement, vide et erreur sont traités.
[ ] La page fonctionne à 320 px et au clavier.
[ ] Les instances sont mises à jour ou détruites proprement.

Notes de lecture et documentation

À la prochaine séance, nous quitterons le modèle « configurer un graphique existant ». D3 nous obligera à rendre explicites les étapes que Chart.js prenait en charge : sélectionner le DOM, lier les données, calculer les échelles, dessiner les marques et orchestrer le récit.