Séance 3 — Encoder avec Chart.js

Séance 3 sur 63 heures

Question directrice

Comment transformer une question en marques, axes et interactions cohérentes — sans laisser la bibliothèque décider à notre place ?

Vous allez construire un premier graphique Chart.js dans le repo Vite, comprendre l’architecture type / data / options / plugins, produire des barres, une courbe et un nuage de points, puis rendre le canvas responsive et accompagné d’une alternative textuelle. Chaque bloc de code restera relié à une décision de lecture.

Point de départ : vous disposez de deux objets préparés en séance 2 — les départs par station et les départs par heure. Si ce n’est pas le cas, le repo starter fournit un échantillon BIXI et une version de référence du pipeline.

Comment utiliser cette leçon

Ce chapitre fonctionne comme un atelier de construction commenté. Lisez une section, exécutez le bloc correspondant, vérifiez le résultat dans le navigateur, puis faites une petite variation. Évitez de coller tout le chapitre d’un coup : la structure de Chart.js devient claire lorsque chaque couche arrive pour résoudre un problème visible.

Temps en directActivitéRésultat
0:00–0:15Ouvrir le starter et vérifier les donnéesUne page Vite et un CSV lisible
0:15–0:45Premier bar chart, de l’installation au renduUne instance Chart.js comprise, pas seulement copiée
0:45–1:20type, data, options, scalesUn bar chart horizontal justifié
1:20–1:30Pause
1:30–2:05Ligne, scatter et compositionTrois contrats de données distincts
2:05–2:30Axes, tooltips, responsive et accessibilitéUn graphique lisible au clavier et sur petit écran
2:30–3:00Atelier à trois niveauxUne variation publiée ou prête à partager

1. Chart.js ne pense pas à votre place

Chart.js est une bibliothèque de graphiques rendus dans un élément HTML <canvas>. Elle fournit des contrôleurs de graphique, des échelles, des animations, des tooltips et un système de plugins. Son principal avantage pédagogique est la vitesse avec laquelle une structure de données peut devenir un graphique interactif.

Cette facilité crée aussi un risque : obtenir rapidement quelque chose de joli et supposer que le travail de visualisation est terminé. La bibliothèque peut dessiner des barres, mais elle ne sait pas :

  • si une ligne du CSV représente un trajet ou une personne;
  • si les catégories doivent être triées;
  • si une échelle doit commencer à zéro;
  • si le rouge signifie réellement un danger;
  • si un top 10 masque un groupe important;
  • si le titre affirme plus que les données;
  • si un donut convient à la comparaison demandée.

Le code de cette séance doit donc être lu dans les deux sens : que demande la configuration à Chart.js, et que demande le graphique à la personne qui le lit ?

Une règle de travail

Avant d’écrire new Chart(...), complétez une phrase :

Pour permettre à [public] de [tâche], je vais représenter [variable] par [encodage], en gardant [contexte] visible.

Exemple :

Pour permettre à une responsable des opérations de comparer les stations, je vais représenter le nombre de départs par la longueur de barres horizontales, en gardant la période et le fait qu’il s’agit d’un échantillon visibles.

Cette phrase prend trente secondes. Elle évite souvent trente minutes de configuration inutile.


2. Installer et reconnaître l’environnement

Le cours utilise une seule chaîne : Vite + JavaScript en modules ES + GitHub + Vercel. StackBlitz exécute le même projet dans le navigateur pour les personnes qui ne peuvent pas installer Node localement.

Dans le starter :

420-333-vite-starter/
├── index.html
├── package.json
├── public/
│   └── data/bixi-sample.csv
└── src/
    ├── main.js
    └── style.css

Vérifiez d’abord les versions réellement installées :

node --version
npm --version
npm list chart.js vite

Le starter mis à jour pour cette formation utilise Chart.js 4.5.1 et Vite 8. Vite 8 demande une version récente de Node; si votre environnement institutionnel est plus ancien, utilisez StackBlitz ou conservez une version de Vite compatible au lieu de mettre à jour en plein atelier.

Installation dans un projet vide

npm create vite@latest 420-333-lab -- --template vanilla
cd 420-333-lab
npm install
npm install chart.js papaparse
npm run dev

Si vous partez du starter, npm install suffit.

Deux façons d’importer Chart.js

Pour apprendre et prototyper :

import Chart from "chart.js/auto";

Le point d’entrée auto enregistre tous les contrôleurs, éléments, échelles et plugins intégrés. Il est plus simple et réduit les erreurs de type « scale is not registered ».

Pour optimiser un produit, on peut importer seulement les pièces utilisées :

import {
  BarController,
  BarElement,
  CategoryScale,
  Chart,
  Legend,
  LinearScale,
  Tooltip,
} from "chart.js";

Chart.register(
  BarController,
  BarElement,
  CategoryScale,
  LinearScale,
  Legend,
  Tooltip
);

Chart.js est tree-shakeable : un bundler peut retirer les modules non importés. Pour un premier cours, la lisibilité du code vaut davantage que quelques kilo-octets. Nous commencerons avec chart.js/auto, puis reviendrons à l’import sélectif comme piste d’optimisation.


3. Premier graphique : de la balise au rendu

Nous allons construire le top des stations préparé en séance 2. Commencez par ajouter une section claire dans index.html :

<section aria-labelledby="stations-title">
  <h2 id="stations-title">Stations comptant le plus de départs</h2>
  <p>Échantillon BIXI fourni avec le cours.</p>

  <div class="chart-container">
    <canvas
      id="station-chart"
      role="img"
      aria-label="Barres horizontales comparant les départs de dix stations BIXI"
    >
      Les stations et leurs valeurs sont décrites dans le tableau suivant.
    </canvas>
  </div>
</section>

Le <canvas> est une surface bitmap pilotée par JavaScript. Les barres ne deviennent pas des éléments HTML individuels. Cette différence avec SVG sera importante lorsque nous passerons à D3.

Dans src/style.css, donnez à Chart.js un conteneur dédié :

.chart-container {
  position: relative;
  width: 100%;
  min-height: 28rem;
}

Puis dans src/main.js :

import Chart from "chart.js/auto";

const canvas = document.querySelector("#station-chart");

const stationData = {
  labels: [
    "de la Commune / Place Jacques-Cartier",
    "Berri / de Maisonneuve",
    "Mackay / de Maisonneuve",
    "Métro Mont-Royal",
    "Parc La Fontaine",
  ],
  datasets: [
    {
      label: "Départs observés",
      data: [820, 735, 650, 520, 460],
    },
  ],
};

const stationChart = new Chart(canvas, {
  type: "bar",
  data: stationData,
  options: {
    indexAxis: "y",
    responsive: true,
    maintainAspectRatio: false,
  },
});

Si cinq barres apparaissent, le rendu fonctionne. Mais cette version n’est pas encore un résultat éditorial : elle manque d’unité explicite, de formatage et d’un style cohérent avec la question.

3.1 Les quatre couches de configuration

Une instance Chart.js se lit ainsi :

new Chart(canvas, {
  type,      // famille de marques et contrôleur
  data,      // labels, datasets et valeurs
  options,   // échelles, interactions, apparence et comportement
  plugins,   // extensions propres à cette instance
});

type

Le type choisit un contrôleur et des valeurs par défaut. Un bar utilise notamment une échelle catégorielle et une échelle linéaire. Un scatter attend des points { x, y }. Le type ne constitue pas un simple thème : il change le contrat de données et la tâche perceptuelle.

data

data.labels contient ici les catégories. Chaque entrée de datasets représente une série. La position i dans dataset.data correspond au label i. Si ces deux tableaux sont décalés, le graphique peut rester valide techniquement tout en attribuant une valeur à la mauvaise station.

options

Les options configurent les échelles, titres, légendes, tooltips, interactions, animations et propriétés des éléments. Elles sont imbriquées; lire la documentation officielle avec le chemin exact évite de placer une option au mauvais niveau.

plugins

Un plugin intervient dans le cycle de vie du graphique. Certains sont intégrés — légende, tooltip, titre, décimation — et se configurent sous options.plugins. D’autres sont externes ou écrits dans le projet.

3.2 Ajouter l’unité et formater les valeurs

const number = new Intl.NumberFormat("fr-CA");

stationChart.options.scales = {
  x: {
    beginAtZero: true,
    title: {
      display: true,
      text: "Nombre de départs dans l’échantillon",
    },
    ticks: {
      callback: (value) => number.format(value),
    },
  },
  y: {
    grid: {
      display: false,
    },
  },
};

stationChart.options.plugins.legend = {
  display: false,
};

stationChart.update();

La légende n’apporte rien lorsqu’une seule série est déjà nommée par le titre et l’axe. La retirer améliore le data-ink ratio sans retirer de contexte. Le formatage fr-CA ajoute les séparateurs appropriés sans transformer les nombres en chaînes dans la donnée source.

3.3 Couleur et état

const styles = getComputedStyle(document.documentElement);
const accent = styles.getPropertyValue("--accent").trim();
const foreground = styles.getPropertyValue("--fg").trim();
const border = styles.getPropertyValue("--border").trim();

stationChart.data.datasets[0].backgroundColor = accent;
stationChart.options.color = foreground;
stationChart.options.borderColor = border;
stationChart.update("none");

En lisant les variables CSS, le graphique reste cohérent avec le thème de la page. update("none") applique le changement sans animation; cette mise à jour stylistique ne porte aucun changement de donnée qu’une transition devrait aider à suivre.


4. Une fonction de rendu plutôt qu’un bloc monolithique

Le premier exemple mélange données et création. Pour continuer, séparons les responsabilités :

function createStationChart(canvas, rows) {
  const data = {
    labels: rows.map((row) => row.key),
    datasets: [
      {
        label: "Départs observés",
        data: rows.map((row) => row.count),
        backgroundColor: "#b8860b",
      },
    ],
  };

  return new Chart(canvas, {
    type: "bar",
    data,
    options: {
      indexAxis: "y",
      responsive: true,
      maintainAspectRatio: false,
      scales: {
        x: {
          beginAtZero: true,
          title: {
            display: true,
            text: "Nombre de départs dans l’échantillon",
          },
        },
        y: {
          grid: { display: false },
        },
      },
      plugins: {
        legend: { display: false },
      },
    },
  });
}

Puis :

const stationChart = createStationChart(
  document.querySelector("#station-chart"),
  topStations
);

Cette séparation facilite trois choses : tester la transformation sans canvas, créer une autre vue à partir d’un autre tableau et détruire proprement l’instance lorsqu’on remonte le composant.

Le piège « Canvas is already in use »

En développement avec rechargement à chaud ou dans une interface qui remonte la vue, on peut créer deux instances sur le même canvas. Gardez une référence et détruisez l’ancienne :

let stationChart;

function renderStations(rows) {
  stationChart?.destroy();
  stationChart = createStationChart(
    document.querySelector("#station-chart"),
    rows
  );
}

destroy() retire les écouteurs et libère les ressources associées à l’instance. Ce détail deviendra important dans les dashboards et les frameworks.


5. Transformer des lignes en contrat graphique

Chart.js accepte plusieurs formes de données. Cette souplesse devient un problème si le code de transformation reste implicite. Entre le CSV et new Chart, construisons un modèle de vue : un petit objet qui contient exactement les labels, valeurs et métadonnées nécessaires au graphique.

Partons des lignes BIXI normalisées à la séance 2 :

{
  tripId: "2025-000041",
  startedAt: new Date("2025-06-03T08:17:00-04:00"),
  startStationId: "7029",
  startStationName: "Métro Mont-Royal",
  riderType: "member"
}

Le graphique en barres ne devrait pas connaître les noms de colonnes du CSV original. Il reçoit une vue dérivée :

function buildStationBarModel(trips, limit = 10) {
  const counts = new Map();

  for (const trip of trips) {
    if (!trip.startStationId || !trip.startStationName) continue;

    const current = counts.get(trip.startStationId) ?? {
      id: trip.startStationId,
      label: trip.startStationName,
      value: 0,
    };

    current.value += 1;
    counts.set(trip.startStationId, current);
  }

  const rows = [...counts.values()]
    .sort((a, b) => b.value - a.value)
    .slice(0, limit);

  return {
    rows,
    labels: rows.map((row) => row.label),
    values: rows.map((row) => row.value),
    totalTrips: trips.length,
    representedTrips: rows.reduce((sum, row) => sum + row.value, 0),
  };
}

Le modèle conserve rows, même si Chart.js utilise surtout labels et values. Cela permet au tooltip, à la table et à une interaction future de retrouver l’identifiant de station sans tenter de le reconstruire depuis le label.

5.1 Éviter les tableaux parallèles qui dérivent

Cette forme est fragile :

const labels = stations.map((d) => d.name).sort();
const values = stations.map((d) => d.count);

Les labels sont triés, mais les valeurs restent dans l’ordre original. Le graphique fonctionne techniquement et associe silencieusement de mauvaises valeurs aux stations.

La règle pratique est : trier et filtrer les objets, puis dériver les tableaux parallèles à partir du même résultat.

const sorted = stations
  .filter((d) => Number.isFinite(d.count))
  .toSorted((a, b) => b.count - a.count);

const labels = sorted.map((d) => d.name);
const values = sorted.map((d) => d.count);

toSorted retourne une copie et évite de modifier le tableau source. Si votre environnement ne le prend pas en charge, utilisez [...stations].sort(...). Éviter la mutation rend le pipeline plus prévisible : un autre graphique ne voit pas son ordre changer parce qu’une vue a trié le tableau partagé.

5.2 Rendre une série temporelle dense

Supposons qu’aucun trajet n’ait été observé à 3 h. Une agrégation naïve produit :

[
  { hour: 1, count: 12 },
  { hour: 2, count: 8 },
  { hour: 4, count: 15 },
]

Si les labels sont catégoriels, Chart.js place 2 h et 4 h côte à côte. L’absence de 3 h n’occupe aucun espace; la série suggère un intervalle régulier qui n’existe pas dans le tableau. Pour une journée complète, créez explicitement les 24 heures :

function countTripsByHour(trips) {
  const counts = Array.from({ length: 24 }, (_, hour) => ({
    hour,
    count: 0,
  }));

  for (const trip of trips) {
    if (!(trip.startedAt instanceof Date)) continue;
    if (Number.isNaN(trip.startedAt.getTime())) continue;

    counts[trip.startedAt.getHours()].count += 1;
  }

  return counts;
}

Une valeur zéro et une valeur manquante ne signifient toutefois pas la même chose. Zéro signifie qu’on a observé la période et compté aucun événement. Une valeur manquante signifie qu’on ne sait pas. Si le fichier est incomplet entre 3 h et 4 h, ne remplissez pas automatiquement avec zéro : conservez null, interrompez la ligne ou annotez la lacune.

5.3 Préserver le grain pour un scatter plot

Un bar chart agrège plusieurs trajets en un compte par station. Un scatter plot température × trajets peut conserver un point par jour. Le grain change :

function buildWeatherScatterModel(days) {
  const points = days
    .filter((day) =>
      Number.isFinite(day.temperatureC) &&
      Number.isFinite(day.tripCount)
    )
    .map((day) => ({
      x: day.temperatureC,
      y: day.tripCount,
      date: day.date,
      precipitationMm: day.precipitationMm,
    }));

  return { points };
}

Chart.js utilise x et y; le tooltip peut utiliser date et precipitationMm. Ces champs supplémentaires ne sont pas décoratifs : ils permettent d’examiner une observation atypique et d’éviter de réduire son explication à deux variables.

5.4 Valider le modèle de vue

Avant le rendu :

const model = buildStationBarModel(trips);

console.table(model.rows);
console.assert(
  model.labels.length === model.values.length,
  "Chaque label doit posséder une valeur."
);
console.assert(
  model.values.every(Number.isFinite),
  "Les valeurs du graphique doivent être numériques."
);
console.assert(
  new Set(model.rows.map((row) => row.id)).size === model.rows.length,
  "Les stations du modèle doivent être uniques."
);

Ces assertions ne remplacent pas des tests automatisés, mais elles placent le diagnostic au bon endroit. Si une valeur est NaN, corrigez le pipeline au lieu de bricoler une option d’axe.

5.5 Mesurer ce que le top 10 exclut

Un top 10 semble complet parce qu’il remplit le graphique. Calculez la part qu’il représente :

const share = model.representedTrips / model.totalTrips;

Affichez par exemple « Les dix stations montrées regroupent 31 % des départs de l’échantillon ». Le lecteur comprend alors que le graphique est un classement partiel, pas une image de tout le réseau.

Cette étape illustre le rôle du modèle de vue : il ne prépare pas seulement les pixels. Il conserve les dénominateurs, les exclusions et les identifiants nécessaires pour expliquer ce que la vue représente.


6. Quatre types, quatre formes de données

6.1 Barres : comparer et classer

Le bar chart convient lorsque chaque catégorie possède une valeur et que la tâche principale est la comparaison. Pour de longs labels, utilisez indexAxis: "y". Triez les catégories par valeur lorsque leur ordre n’a pas de sens propre; conservez l’ordre naturel pour une variable ordinale.

new Chart(canvas, {
  type: "bar",
  data: {
    labels: topStations.map((d) => d.key),
    datasets: [{ data: topStations.map((d) => d.count) }],
  },
  options: {
    indexAxis: "y",
    scales: { x: { beginAtZero: true } },
  },
});

Ne confondez pas bar chart et histogramme. Les barres représentent ici des catégories distinctes et peuvent être séparées. Un histogramme regroupe une variable continue en intervalles contigus; la largeur des classes fait partie de l’analyse.

6.2 Ligne : suivre un ordre continu

new Chart(document.querySelector("#hour-chart"), {
  type: "line",
  data: {
    labels: departuresByHour.map((d) => `${d.key} h`),
    datasets: [
      {
        label: "Départs observés",
        data: departuresByHour.map((d) => d.count),
        tension: 0,
        pointRadius: 3,
      },
    ],
  },
  options: {
    scales: {
      y: { beginAtZero: true },
    },
  },
});

La ligne relie les points et suggère une continuité. tension: 0 conserve des segments droits; une courbe lissée peut faire apparaître des maxima intermédiaires qui n’ont jamais été mesurés. Les points horaires doivent rester dans l’ordre chronologique. Trier par valeur transformerait la question « quand ? » en classement.

Pour des dates réelles, Chart.js propose des échelles time et timeseries, mais elles demandent un adaptateur de date. Dans ce cours court, des timestamps numériques ou des labels déjà préparés suffisent jusqu’à ce qu’une vraie gestion du zoom temporel soit nécessaire.

Interrompre une ligne lorsque les données manquent

Chart.js ne doit pas relier automatiquement deux observations séparées par une période inconnue. Utilisez null pour représenter une lacune et gardez spanGaps: false :

const dailyValues = [128, 141, null, null, 173, 168];

const dataset = {
  label: "Départs quotidiens",
  data: dailyValues,
  spanGaps: false,
};

La coupure dans la ligne devient une information : aucun point exploitable n’est disponible pour ces dates. spanGaps: true relierait les valeurs connues de part et d’autre. Ce choix peut convenir si l’on veut seulement montrer une tendance très longue et que la lacune est clairement documentée, mais il suggère une continuité non observée.

Ne remplacez pas machinalement null par zéro. Dans une mesure de fréquentation, zéro signifie « la station était ouverte et aucun départ n’a été compté ». null signifie « la mesure est absente ou invalide ». Visuellement, l’un touche l’axe; l’autre interrompt la preuve.

Si plusieurs périodes sont manquantes, ajoutez une note ou une bande sur la zone concernée. Le tooltip ne suffit pas : par définition, une période sans point n’offre aucune marque à survoler. Cette petite décision relie directement le nettoyage des données de la séance 1 à l’intégrité graphique de la séance 3.

Testez donc trois cas avant de considérer la série terminée : une valeur réellement nulle, une valeur absente au milieu de la période et une valeur absente à la fin. Vérifiez le tracé, le résumé textuel et la table. Le même état doit conserver le même sens dans les trois représentations; sinon, le problème se situe dans le modèle de vue plutôt que dans le style du graphique.

6.3 Scatter : préserver les observations

Le nuage de points attend des objets { x, y } :

const temperatureAndTrips = dailyRows.map((day) => ({
  x: day.temperatureC,
  y: day.tripCount,
}));

new Chart(document.querySelector("#weather-chart"), {
  type: "scatter",
  data: {
    datasets: [
      {
        label: "Jours observés",
        data: temperatureAndTrips,
        pointRadius: 5,
      },
    ],
  },
  options: {
    scales: {
      x: {
        type: "linear",
        title: { display: true, text: "Température moyenne (°C)" },
      },
      y: {
        title: { display: true, text: "Nombre de trajets" },
      },
    },
  },
});

Chaque point représente un jour. Une tendance visuelle n’établit pas que la température cause les trajets : saison, pluie, jour de semaine et disponibilité des vélos peuvent intervenir. Le tooltip devrait afficher la date pour permettre de retourner à l’observation réelle.

6.4 Doughnut : une composition très simple

new Chart(document.querySelector("#membership-chart"), {
  type: "doughnut",
  data: {
    labels: ["Membres", "Occasionnels"],
    datasets: [
      {
        data: [72, 28],
        backgroundColor: ["#0072b2", "#e69f00"],
      },
    ],
  },
  options: {
    cutout: "60%",
    plugins: {
      tooltip: {
        callbacks: {
          label: (context) => `${context.label} : ${context.parsed} %`,
        },
      },
    },
  },
});

Ce graphique est défendable si les catégories sont exclusives, si leur total est clair et si la tâche consiste à communiquer une majorité nette. Pour comparer membres et occasionnels mois après mois, des barres empilées à 100 % offrent une base commune beaucoup plus lisible.


7. Échelles honnêtes : le cas du zéro

Une échelle transforme une valeur en position ou en longueur. Modifier son domaine modifie l’image du phénomène.

Pour une barre, la longueur part de l’origine. Si le domaine commence à 90, une valeur de 99 produit une barre plusieurs fois plus longue qu’une valeur de 92, alors que l’écart réel n’est que de 7 points. C’est la raison derrière la recommandation de commencer les barres à zéro.

La règle n’est pas universelle pour tous les graphiques. Dans une courbe de température variant entre 18 et 22 °C, un axe de 0 à 100 écraserait le changement que l’on veut examiner. Une ligne encode principalement la position et la pente; on peut resserrer le domaine à condition de montrer clairement l’échelle et de ne pas transformer une petite variation en catastrophe rhétorique.


8. Responsive : le canvas suit son conteneur

Chart.js est responsive par défaut, mais le canvas ne peut pas déduire correctement une hauteur relative depuis tous les styles. La documentation recommande un conteneur dédié et positionné.

.chart-container {
  position: relative;
  width: 100%;
  height: 28rem;
}

@media (max-width: 40rem) {
  .chart-container {
    height: 34rem;
  }
}
options: {
  responsive: true,
  maintainAspectRatio: false,
}

Une hauteur plus grande sur mobile peut être nécessaire pour des barres horizontales avec de nombreux labels. « Responsive » ne signifie pas seulement que le dessin rapetisse. Sur un petit écran, il faut parfois :

  • réduire le nombre de catégories;
  • abréger ou envelopper des labels;
  • déplacer une légende sous le graphique;
  • passer d’une vue côte à côte à une pile verticale;
  • remplacer un tooltip au survol par une sélection au toucher;
  • afficher une observation principale et laisser le détail dans une table.

Testez à 320 px, au clavier et sur un vrai appareil si le graphique doit être publié.


9. Tooltips : du contexte, pas une cachette

Les tooltips sont utiles pour les valeurs secondaires et les détails d’une observation. Ils ne doivent pas cacher l’unité ou le message principal.

options: {
  interaction: {
    mode: "nearest",
    intersect: false,
  },
  plugins: {
    tooltip: {
      callbacks: {
        title: ([item]) => item.label,
        label: (context) => {
          return ` ${number.format(context.parsed.x)} départs`;
        },
        afterLabel: () => "Échantillon du cours",
      },
    },
  },
}

Pour un bar chart horizontal, la valeur quantitative est souvent context.parsed.x; pour une barre verticale, elle est généralement y. Inspectez context.parsed dans la console lorsque vous changez de type.

Questions à poser :

  • La personne peut-elle comprendre la vue sans survoler chaque marque ?
  • Le tooltip répète-t-il seulement ce que l’axe dit déjà ?
  • Sur écran tactile, comment l’ouvre-t-on et le ferme-t-on ?
  • Au clavier ou avec un lecteur d’écran, l’information existe-t-elle ailleurs ?

10. Accessibilité : un canvas n’est pas un document

Le contenu dessiné dans <canvas> n’entre pas automatiquement dans l’arbre d’accessibilité. Chart.js confie donc à l’auteur la responsabilité de fournir un nom et une alternative.

Une première couche :

<canvas
  id="station-chart"
  role="img"
  aria-label="Barres horizontales. de la Commune est première avec 820 départs, suivie de Berri avec 735 et Mackay avec 650."
>
  Graphique décrit dans le texte et le tableau ci-dessous.
</canvas>

Pour une vue complexe, le W3C recommande une description courte qui identifie le graphique et une description longue qui transmet les tendances, valeurs et relations essentielles. Une table HTML peut fournir l’accès exact aux données :

<details>
  <summary>Consulter les données du graphique</summary>
  <table>
    <caption>Départs observés par station</caption>
    <thead>
      <tr><th scope="col">Station</th><th scope="col">Départs</th></tr>
    </thead>
    <tbody id="station-table-body"></tbody>
  </table>
</details>
function renderStationTable(rows, tbody) {
  tbody.replaceChildren(
    ...rows.map((row) => {
      const tr = document.createElement("tr");
      const station = document.createElement("th");
      const count = document.createElement("td");

      station.scope = "row";
      station.textContent = row.key;
      count.textContent = number.format(row.count);
      tr.append(station, count);
      return tr;
    })
  );
}

La table n’est pas un « mode handicap ». Elle sert aussi à copier une valeur, vérifier une catégorie ou imprimer les données. L’accessibilité améliore ici le produit pour plusieurs usages.


11. Déboguer par couches

Quand rien ne s’affiche, ne changez pas dix options. Vérifiez de l’extérieur vers l’intérieur.

1. Le module est-il chargé ?

console.log("main.js chargé");

Si le message n’apparaît pas, vérifiez le <script type="module" src="/src/main.js"> et la console réseau.

2. Le canvas existe-t-il ?

const canvas = document.querySelector("#station-chart");
console.assert(canvas, "Canvas #station-chart introuvable");

3. Les données ont-elles la forme attendue ?

console.table(topStations);
console.assert(topStations.length > 0, "Aucune station à afficher");
console.assert(
  topStations.every((row) => Number.isFinite(row.count)),
  "Une valeur n’est pas numérique"
);

4. L’instance est-elle créée une seule fois ?

Une erreur mentionnant un canvas déjà utilisé indique souvent qu’une ancienne instance n’a pas été détruite.

5. Le conteneur a-t-il une hauteur ?

Avec maintainAspectRatio: false, un parent sans hauteur utile peut produire une zone invisible ou instable.

6. L’option est-elle au bon chemin ?

plugins.legend.display placé directement sous options.legend ne fonctionne pas dans Chart.js 4. Lisez le chemin complet dans la documentation et comparez l’objet.

Cette méthode de diagnostic est transférable : existence du DOM, forme de la donnée, cycle de vie, géométrie CSS, configuration.


12. Atelier — voir, modifier, construire

Atelier · niveau Observer

Lire une configuration comme une décision

15 à 20 min

Résultat visible : Une configuration annotée où chaque bloc est relié à une tâche de lecture.

Ouvrez l’exemple du bar chart. Pour chaque ligne de options, ajoutez un commentaire qui commence par « permet de… » ou « évite de… ». Supprimez temporairement l’option, observez le changement, puis rétablissez-la.

Terminez avec quatre annotations : type, orientation, domaine de l’axe et légende.

Atelier · niveau Modifier

Une donnée, deux tâches

25 à 35 min

Résultat visible : Deux graphiques utilisant le même CSV mais répondant à deux questions différentes.

  1. Conservez le bar chart des stations.
  2. Ajoutez une courbe des départs par heure.
  3. Écrivez un titre orienté et l’unité de chaque vue.
  4. Masquez les légendes redondantes et réduisez les grilles secondaires.
  5. Ajoutez une description textuelle de l’observation principale.

Atelier · niveau Construire

Choisir et défendre un troisième contrat visuel

35 à 45 min

Résultat visible : Un nuage de points ou une composition simple, avec transformation, tooltip et alternative textuelle.

Choisissez une relation entre deux mesures ou une composition réellement additive dans votre dataset. Préparez la forme de données attendue, construisez le graphique et ajoutez une table ou une description structurée.

Dans le repo, créez un court fichier DECISIONS.md contenant la question, le grain, le type choisi, l’échelle et une limite d’interprétation.

Partage en groupe

Présentez une seule décision, pas tout le code. Le groupe peut répondre avec trois questions :

  • Quelle comparaison devient facile ?
  • Quelle information reste dans un tooltip ou une table ?
  • Quel changement de données rendrait ce graphique trompeur ?

Ce que vous devriez emporter

  1. Chart.js exécute un contrat; la question, le grain et l’encodage restent des décisions humaines.
  2. type, data, options et plugins forment les quatre couches principales de configuration.
  3. chart.js/auto favorise l’apprentissage; l’enregistrement sélectif sert ensuite l’optimisation.
  4. Barres, lignes, scatter et doughnut demandent des structures et des tâches différentes.
  5. Une échelle honnête dépend de la marque : la barre demande généralement une origine zéro; la ligne peut utiliser un domaine resserré explicitement.
  6. Le canvas responsive suit un conteneur dédié; une vue mobile peut exiger une autre composition.
  7. Un tooltip complète la vue, mais ne doit pas contenir seul l’information essentielle.
  8. Un canvas doit recevoir un nom accessible et une description ou représentation structurée équivalente.
  9. Conserver puis détruire l’instance évite les fuites et les conflits de canvas.

Référence rapide

import Chart from "chart.js/auto";

const chart = new Chart(document.querySelector("#chart"), {
  type: "bar",
  data: {
    labels: [],
    datasets: [{ label: "", data: [] }],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    scales: {},
    plugins: {},
  },
});

// Modifier les données, puis :
chart.update();

// Avant de retirer ou recréer le canvas :
chart.destroy();

Notes de lecture et documentation

La prochaine séance ne cherchera pas à ajouter un maximum d’options. Elle abordera un problème plus difficile : faire travailler plusieurs vues ensemble, les mettre à jour avec une interaction et construire une hiérarchie de dashboard qui reste compréhensible.