Chapitre 14 : La visualisation de données

La visualisation de données avec chart.js

Lorsque l'on est face à de grandes quantités d'informations, il est important de pouvoir les visualiser correctement pour pouvoir en tirer des conclusions. De nombreux logiciels permettent de visualiser des données numériques sous différentes formes. Dans le cadre de ce projet, votre objectif est de développer un site web interactif qui présente des données de façon graphique. Une première approche pour construire un tel site web pourrait être de produire ces graphiques directement en python avec matplotlib <https://matplotlib.org/> par exemple et d'intégrer les images produites dans des pages HTML. Malheureusement, une telle approche serait lourde à mettre en œuvre. Il est préférable de passer par des librairies spécialisées dans la visualisation d'information via le web.

Dans le cadre de ce projet, vous utiliserez Chart.js <https://www.chartjs.org> qui est assez simple à mettre en œuvre tout en donnant un excellent résultat au niveau graphique. Chart.js <https://www.chartjs.org> est une librairie codée en JavaScript. JavaScript est un langage de programmation qui est utilisé par les navigateurs pour avoir des pages HTML qui s'adaptent dynamiquement. On peut voir JavaScript comme étant une extension de HTML sur le web. Dans le cadre de ce projet, nous nous concentrerons sur l'utilisation de Chart.js <https://www.chartjs.org> dans une page HTML. L'utilisation complète de JavaScript sort du cadre de ce cours.

Un graphique Chart.js est toujours inclus dans une zone rectangulaire définie par l'élément HTML5 canvas (canevas en français). Celui-ci supporte plusieurs attributs dont les plus importants sont :

  • id qui permet de donner un nom unique au canevas;

  • width qui permet de spécifier la largeur du canevas;

  • height qui permet de spécifier la hauteur du canevas.

L'exemple ci-dessous illustre la déclaration d'un tel canevas. L'identifiant permet de faire référence à ce canevas dans une feuille de style, mais surtout dans le code JavaScript qui utilise la librairie Chart.js.

<canvas id="graphique" width="200" height="100"></canvas>

Vous pouvez maintenant facilement afficher un graphique en bâtonnets dans une page HTML. Pour cela, il faut d'abord charger la librairie Chart.js dans l'entête de votre page HTML en utilisant la balise <script> avec comme référence soit une version de la librairie disponible sur Internet, soit une copie locale de cette libraire. La copie locale est préférable dans le cadre de ce projet. Vous trouverez dans le guide d'installation de Chart.js <https://www.chartjs.org/docs/latest/getting-started/installation.html> toutes les références nécessaires.

Une fois la librairie chargée, vous devez y faire appel dans le corps de la page HTML. Cela se fait en écrivant un petit script en JavaScript entre deux balises <script> et </script>. Il n'est pas nécessaire de connaître JavaScript pour pouvoir utiliser cette librairie. Il suffit de consulter les nombreux exemples disponibles dans la documentation de Chart.js. La création d'un graphique prend en général deux instructions JavaScript.

var ctx = document.getElementById('graphique').getContext('2d');

La première est la ligne qui indique à JavaScript d'associer à la variable ctx le canevas dont l'identifiant est passé en argument à la méthode document.getElementById(). Cette méthode permet de récupérer dans la page HTML courante l'élément dont l'identifiant est fourni. Toutes les lignes en JavaScript se terminent par le caractère ; contrairement à python. En JavaScript, les caractères // marquent le début d'un commentaire là où le Python utilise #.

   <!DOCTYPE html>
   <html>
    <head>
     <meta charset="utf-8">
     <!-- Chargement de la librairie Javascript à utiliser -->
     <!-- depuis Internet -->
     <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
     <!-- Localement
     <script src="chart.min.js"></script>
     -->
     <title>Un graphique en batonets</title>
    </head>
    <body>
      
      <canvas id="graphique" width="600" height="400"></canvas>
      <script>
	// l'identifiant est celui du canevas
	var ctx = document.getElementById('graphique').getContext('2d');
	// création du graphique
	var myChart = new Chart(ctx, {
	type: 'bar',   // le type du graphique
	data: {        // les données
        labels: ['Jean', 'Martine', 'Michel', 'Jules', 'Louise', 'Dominique'],
        datasets: [{
                    label: 'votes',
                    data: [12, 19, 3, 5, 2, 3]
                   }]
	       }
         }
	);
      </script>

    </body>
   </html>

La seconde ligne du script crée le graphique new Chart(ctx, ...); et l'associe au canevas identifié par la variable ctx définie à la ligne précédente. Le deuxième argument spécifie le type du graphique type: 'bar', les données numériques à afficher (data) et les étiquettes à utiliser. Dans cet exemple, nous affichons les votes reçus par six étudiants. Le code HTML complet est repris ci-dessous ainsi que sa visualisation dans un navigateur.

_images/batonnets.png

Exemple de diagramme en bâtonnets avec Chart.js

Lorsque l'on écrit ses premiers scripts en JavaScript, on peut parfois faire des erreurs de syntaxe difficiles à identifier et corriger. Heureusement, les navigateurs modernes comprennent des outils qui facilitent la vie des développeurs et leur permettent de corriger rapidement ces erreurs. Prenons Chrome comme exemple, mais Firefox ou Safari supportent les mêmes fonctionnalités. Vous pouvez activer les outils pour développeurs de Chrome en cliquant sur les trois points verticaux en haut à droite de la fenêtre puis More Tools et enfin Developer Tools. Ce menu est aussi disponible en tapant Ctrl+Shift+I. Vous verrez alors apparaître différents outils dont la liste des éléments contenus dans la page HTML, une console avec les éventuels messages d'erreur, un accès aux sources de la page, ...

Ajoutons dans la page HTML ci-dessus une erreur dans les étiquettes en oubliant la première apostrophe avant le prénom Jean. Chrome n'affiche rien car il y a une erreur de syntaxe dans le JavaScript à la ligne 23.

_images/chrome-err.png

Erreur affichée dans la console de Chrome

En cliquant sur la ligne en erreur, Chrome affiche plus de détails qui en facilitent sa correction.

_images/chrome-err2.png

Plus d'informations sur l'erreur dans la console de Chrome

La librairie Chart.js supporte de très nombreux types de graphes. Chaque type de graphique supporte des dizaines d'options que vous pouvez combiner à votre guise.

Lorsque l'on doit visualiser de grandes quantités de données, comme des points d'un examen, il peut être intéressant de regrouper ces données.

Prenons le fichier /figures/chartjs/data.csv qui contient les résultats de 468 étudiants à une interrogation.

La question est maintenant d'arriver à passer les données depuis Flask.

Choix de la visualisation des données

Une solution naïve est de simplement afficher les points de chaque étudiant dans l'ordre de leur numéro d'inscription. Le résultat est complètement illisible et n'apporte aucune information utile.

_images/stud-1.png

Une mauvaise visualisation des points des étudiants

Une deuxième approche est de regarder le nombre d'étudiants qui ont obtenu chacune des cotes. Avec potentiellement 100 cotes différentes, cela rend un graphique qui reste difficile à interpréter.

_images/stud-2.png

Le nombre d'étudiants pour chaque cote

Une meilleure approche est de regrouper les points obtenus par les étudiants en classes, par exemple de 0-9, de 10 à 19, ... et de compter le nombre d'étudiants dans chaque classe.

_images/stud-3.png

Le nombre d'étudiants pour chaque classe

Une dernière approche est de trier les cotes de façon croissante. Cela permet de facilement visualiser le nombre d'étudiants qui ont plus ou moins qu'une certaine cote.

_images/stud-4.png

Visualisation des cotes en ordre croissant

Les exemples ci-dessus sont illustratifs. Vous pouvez certainement faire beaucoup mieux que ces exemples entièrement gris. Dans le cadre de ce projet, vous avez toute la liberté pour proposer une solution de visualisation qui permet aux visiteurs de votre site de visualiser les données.

Vous trouverez de l'aide sur comment faire un bon graphique dans le livre "Fundamentals of Data Visualization" disponible à l'adresse https://clauswilke.com/dataviz/.

Si vous ne maitrisez pas l'anglais, vous pouvez lire la version traduite par Google Translate.

Les chapitres suivants sont les plus importants :

  • 1 - introduction

  • 2 - associer des visualisations à des données

  • 3.1 et 3.2 (pas 3.3) - axes et coordonnées

  • 4 - le choix des couleurs, nous en avons déjà discuté, mais une rapide relecture est intéressante.

  • 5 - direction des visualisations

  • 6 - visualisation des quantités

  • 7 - visualiser des distributions, histogrammes

  • 17 - principe de proportionnalité des zones

  • 22 - le texte et les axes

  • 23 - balance du texte et du contenu

  • 25 - évitez les lignes

Le chapitre 24 se résume en : adaptez la taille du texte du graphique au site web ! Le texte des axes ne peut pas être trop petit ou trop grand.

Si vous êtes paresseux, jetez au moins un coup d'œil rapide aux nombreuses illustrations du livre pour vous inspirer. Une fois avant de faire vos graphes, et une fois après.

chart.js et Flask

Maintenant que nous savons afficher un graphique Chart.js dans une page HTML statique, la question est de savoir comment passer des données dynamiques depuis Flask vers Chart.js. Le défi est que Chart.js est une librairie JavaScript qui s'exécute dans le navigateur, tandis que Flask est un framework Python qui s'exécute sur le serveur. Il faut donc trouver un moyen de transmettre les données du serveur au navigateur.

La solution est simple : lors de la génération du template Jinja2, Flask insère les données directement dans le code JavaScript. Puisque les listes Python et les tableaux JavaScript ont une syntaxe très similaire (les deux utilisent des crochets [ ]), il suffit de passer les listes Python au template et de les insérer à l'aide de la syntaxe {{ variable }}.

Avertissement

Pour éviter que Jinja2 n'échappe les caractères spéciaux comme les crochets [ et ], il faut utiliser le filtre | safe lorsque l'on insère des listes qui contiennent des chaînes de caractères : {{ etudiants | safe }}. Sans ce filtre, les guillemets seraient transformés en &quot; et le JavaScript ne fonctionnerait pas. Pour les listes de nombres, le filtre | safe n'est pas nécessaire.

Première intégration (branche viz)

Le code de cette étape est disponible dans la branche viz du dépôt poudlard.

L'application poudlard gère des étudiants répartis dans quatre maisons, avec des notes enregistrées par cours. La vue Flask interroge la base de données via la couche modèle pour récupérer le prénom, le nom et la moyenne de chaque étudiant :

# poudlard/models/etudiant.py
from poudlard.db import get_db

def get_note_etudiant():
    db = get_db()
    notes = db.execute("""SELECT E.nom, E.prenom, AVG(CE.note) AS note
        FROM etudiant E
        NATURAL JOIN inscription I
        NATURAL JOIN cours_etudiant CE
        GROUP BY E.etudiant_id""")
    return notes.fetchall()

La vue Flask prépare ensuite deux listes Python — les étiquettes (noms d'étudiants) et les valeurs (notes) — puis les passe au template :

# poudlard/etudiant.py
from flask import Blueprint, render_template
from poudlard.models.etudiant import get_note_etudiant

bp = Blueprint('etudiant', __name__)

@bp.route('/')
def etudiant_list():
    results = get_note_etudiant()
    etudiants = []
    notes = []
    for e in results:
        etudiants.append(e["prenom"] + " " + e["nom"])
        notes.append(e["note"])

    return render_template("base.html",
                           etudiants=etudiants,
                           notes=notes)

Dans le template Jinja2, on charge la librairie Chart.js depuis un CDN, on crée un canevas <canvas>, et on insère les données Python directement dans le code JavaScript :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My App</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
</head>
<body>
    <h1>Moyenne des étudiants en 1991</h1>
    <canvas id="graphique" width="600" height="400"></canvas>
    <script>
      // l'identifiant est celui du canevas
      var ctx = document.getElementById('graphique').getContext('2d');
      // création du graphique
      var myChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: {{ etudiants | safe }},
            datasets: [{
                label: 'Notes',
                data: {{ notes }}
            }]
        }
      });
    </script>
</body>
</html>

Les points clés de ce template :

  • {{ etudiants | safe }} insère la liste Python des noms directement comme un tableau JavaScript. Le filtre | safe est indispensable car les noms contiennent des guillemets.

  • {{ notes }} insère la liste de nombres directement (pas de | safe nécessaire pour des nombres).

  • Chart.js reconnaît immédiatement la syntaxe ["Alice", "Bob"] comme un tableau JavaScript valide.

_images/viz.png

Visualisation de la moyenne des étudiants avec Chart.js

Histogramme coloré (branche viz-bins)

Le code de cette étape est disponible dans la branche viz-bins du dépôt poudlard.

Afficher une barre par étudiant produit un graphique illisible lorsque le nombre d'étudiants est grand. Il est plus pertinent de regrouper les notes en histogramme (bins) et de compter le nombre d'étudiants dans chaque bin. Dans ce cas précis, on va avoir une bin pour chaque note, donc de 0 à 20 inclus, ce qui fait 21 bins. On peut également coloriser les barres selon la cote obtenue.

La vue Flask calcule l'histogramme et la liste de couleurs en Python :

@bp.route('/')
def etudiant_list():
    results = get_note_etudiant()
    notes = [e["note"] for e in results]

    # Axe des abscisses : les notes de 0 à 19
    etudiants = list(range(0, 20))
    # Comptage des étudiants dans chaque classe
    vals = [0] * 21
    for n in notes:
        vals[round(n)] += 1

    # Couleur rouge si < 10, orange si < 12, vert sinon
    colors = [("red" if v < 10 else "orange") if v < 12 else "green"
              for v in etudiants]

    return render_template("base.html",
                           notes=vals,
                           etudiants=etudiants,
                           colors=colors)

Dans le template, on ajoute simplement le paramètre backgroundColor au dataset pour appliquer les couleurs :

datasets: [{
    label: 'Notes',
    data: {{ notes }},
    backgroundColor: {{ colors | safe }}
}]

Note

La liste colors contient des chaînes de caractères ("red", "orange", "green"), ce qui nécessite d'utiliser | safe pour éviter l'échappement des guillemets par Jinja2.

_images/viz_hist_color.png

Histogramme coloré des notes des étudiants

Options avancées avec Chart.js (branche viz-labels)

Le code de cette étape est disponible dans la branche viz-labels du dépôt poudlard.

Un bon graphique doit toujours comporter des titres sur ses axes et une taille de texte adaptée. Ces options se configurent dans le dictionnaire options du graphique :

<script>
    var ctx = document.getElementById('graphique').getContext('2d');
    // Taille globale du texte pour tous les graphiques de la page
    Chart.defaults.font.size = 16;
    var myChart = new Chart(ctx, {
        type: 'bar',
        options: {
            responsive: false,
            scales: {
                y: {
                    title: {
                        text: "Nombre d'étudiants",
                        display: true,
                    }
                },
                x: {
                    title: {
                        text: "Note",
                        display: true,
                    }
                },
            }
        },
        data: {
            labels: {{ etudiants | safe }},
            datasets: [{
                label: "Moyenne des étudiants",
                data: {{ notes }},
                backgroundColor: {{ colors | safe }}
            }]
        }
    });
</script>

Les principaux paramètres d'options utilisés ici :

  • responsive: false fixe la taille du canevas aux dimensions définies dans l'attribut width et height de la balise <canvas>.

  • Chart.defaults.font.size définit la taille de police globale pour tous les graphiques de la page.

  • options.scales.x.title et options.scales.y.title ajoutent des titres sur les axes. display: true est indispensable pour les afficher.

Graphique en anneau par maison (branche viz-pie)

Le code de cette étape est disponible dans la branche viz-pie du dépôt poudlard.

Il est parfois utile de représenter des proportions plutôt que des valeurs absolues. Un graphique en anneau (donut) est idéal pour visualiser comment les étudiants sont répartis entre les maisons de Poudlard.

On ajoute une nouvelle fonction dans le modèle pour compter les étudiants par maison :

# poudlard/models/etudiant.py
def get_nb_etudiant():
    db = get_db()
    notes = db.execute("""SELECT E.maison, COUNT(*) AS nb
        FROM etudiant E
        GROUP BY E.maison""")
    return notes.fetchall()

La vue Flask utilise un dictionnaire de couleurs officielles des maisons et prépare les trois listes nécessaires au graphique :

from poudlard.models.etudiant import get_note_etudiant, get_nb_etudiant

@bp.route('/')
def etudiant_list():
    nb_students = get_nb_etudiant()

    colors = {
        "Serpentard":  "#2a623d",
        "Gryffondor":  "#ae0001",
        "Poufsouffle": "#f0c75e",
        "Serdaigle":   "#222f5b"
    }

    return render_template("base.html",
        maisons=[r["maison"] for r in nb_students],
        nb_etudiants=[r["nb"] for r in nb_students],
        colors=[colors[r['maison']] for r in nb_students])

Le template utilise le type 'doughnut' (anneau) de Chart.js :

<canvas id="graphique" width="300" height="300"></canvas>
<script>
  var ctx = document.getElementById('graphique').getContext('2d');
  var myChart = new Chart(ctx, {
    type: 'doughnut',
    responsive: false,
    data: {
        labels: {{ maisons | safe }},
        datasets: [{
            data: {{ nb_etudiants }},
            backgroundColor: {{ colors | safe }}
        }]
    }
  });
</script>
_images/viz_pie.png

Graphique en anneau de la répartition des étudiants par maison

CDF (branche viz-cdf)

Le code de cette étape est disponible dans la branche viz-cdf du dépôt poudlard.

Jusqu'ici, chaque graphique ne comportait qu'un seul dataset. Chart.js permet d'en afficher plusieurs simultanément, ce qui est très utile pour comparer les maisons entre elles. L'astuce est d'utiliser une boucle Jinja2 dans le template pour générer autant de datasets qu'il y a de maisons.

Dans cet exemple, on calcule la distribution cumulative (CDF) des notes pour chaque maison. Pour chaque maison, on compte combien d'étudiants ont obtenu chaque note, puis on calcule la proportion cumulée :

@bp.route('/')
def etudiant_list():
    results = get_note_etudiant()

    etudiants = list(range(0, 20))
    vals = dict()
    for e in results:
        maison = e["maison"]
        if not maison in vals:
            vals[maison] = [0] * 21
        vals[maison][round(e["note"])] += 1

    # Calcul de la CDF : proportion cumulée des étudiants
    for maison, v in vals.items():
        t = 0
        tot = []
        for n in v:
            t += n
            tot.append(t / sum(v))
        vals[maison] = tot

    colors = {
        "Serpentard":  "#2a623d",
        "Gryffondor":  "#ae0001",
        "Poufsouffle": "#f0c75e",
        "Serdaigle":   "#222f5b"
    }

    return render_template("base.html",
                           notes=vals,
                           etudiants=etudiants,
                           colors=colors)

Dans le template, la boucle {% for maison, n in notes.items() %} génère un dataset JavaScript par maison. On utilise {{maison}} et {{colors[maison]}} pour accéder aux valeurs du dictionnaire Python :

<script>
  var ctx = document.getElementById('graphique').getContext('2d');
  var myChart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: {{ etudiants | safe }},
        datasets: [
            {% for maison, n in notes.items() %}
            {
                label: '{{ maison }}',
                data: {{ n }},
                backgroundColor: '{{ colors[maison] }}',
                borderColor: '{{ colors[maison] }}',
            },
            {% endfor %}
        ]
    }
  });
</script>

Note

La boucle {% for %} de Jinja2 peut être utilisée à l'intérieur du code JavaScript du template pour générer dynamiquement du contenu. Flask évalue le template côté serveur avant de l'envoyer au navigateur. Le navigateur ne reçoit que le JavaScript final, sans aucune trace de Jinja2.

Graphiques empilés et paramètres d'URL (branche viz-stack)

Le code de cette étape est disponible dans la branche viz-stack du dépôt poudlard.

Cette branche illustre deux fonctionnalités supplémentaires : les graphiques empilés (stacked) et la lecture de paramètres depuis l'URL.

On peut rendre la taille des classes (bins) configurable par l'utilisateur en lisant un paramètre step dans l'URL. Par exemple, /?step=2 regroupe les notes deux à deux (0-1, 2-3, ...). Flask récupère ce paramètre via request.args.get() :

from flask import Blueprint, render_template, request

@bp.route('/')
def etudiant_list():
    results = get_note_etudiant()
    step = int(request.args.get('step', 1))

    if step == 1:
        etudiants = list(range(0, 20))
    else:
        etudiants = ["%d - %d" % (r, r + step) for r in range(0, 20, step)]

    vals = dict()
    for e in results:
        maison = e["maison"]
        if not maison in vals:
            vals[maison] = [0] * int((21 / step) + 1)
        vals[maison][round(e["note"] / step)] += 1

    colors = {
        "Serpentard":  "#2a623d",
        "Gryffondor":  "#ae0001",
        "Poufsouffle": "#f0c75e",
        "Serdaigle":   "#222f5b"
    }

    return render_template("base.html",
                           notes=vals,
                           etudiants=etudiants,
                           colors=colors)

La fonction request.args.get('step', 1) retourne la valeur du paramètre step dans l'URL (partie après le ?), ou 1 par défaut si le paramètre est absent.

Pour afficher un graphique empilé, il faut ajouter l'option stacked: true sur les deux axes dans options.scales. On peut ainsi afficher deux graphiques sur la même page — un normal et un empilé — pour comparer les deux représentations :

<h1>Distribution par maison (barres groupées)</h1>
<canvas id="graphique" width="600" height="400"></canvas>
<script>
  var ctx = document.getElementById('graphique').getContext('2d');
  new Chart(ctx, {
    type: 'bar',
    responsive: false,
    data: {
        labels: {{ etudiants | safe }},
        datasets: [
            {% for maison, n in notes.items() %}
            {
                label: '{{ maison }}',
                data: {{ n }},
                backgroundColor: '{{ colors[maison] }}',
            },
            {% endfor %}
        ]
    }
  });
</script>

<h1>Distribution par maison (barres empilées)</h1>
<canvas id="graphique2" width="600" height="400"></canvas>
<script>
  var ctx2 = document.getElementById('graphique2').getContext('2d');
  new Chart(ctx2, {
    type: 'bar',
    responsive: false,
    options: {
        scales: {
            x: { stacked: true },
            y: { stacked: true }
        }
    },
    data: {
        labels: {{ etudiants | safe }},
        datasets: [
            {% for maison, n in notes.items() %}
            {
                label: '{{ maison }}',
                data: {{ n }},
                backgroundColor: '{{ colors[maison] }}',
            },
            {% endfor %}
        ]
    }
  });
</script>

Note

Lorsque plusieurs graphiques Chart.js apparaissent sur la même page, chaque graphique doit avoir son propre <canvas> avec un id différent, et son propre bloc <script> avec une variable ctx distincte.

_images/viz_hist_maison.png

Graphiques empilés pour la distribution des étudiants par maison

_images/viz_bin_large.png

Graphiques groupés pour la distribution des étudiants par maison avec des bins de 5

Récapitulatif des branches

Les six branches du dépôt poudlard illustrent une progression pas à pas :

  • viz : intégration de base Chart.js + Flask, une barre par étudiant.

  • viz-bins : regroupement en classes et coloration (rouge/orange/vert).

  • viz-labels : Chart.js v3, titres d'axes, taille de police.

  • viz-pie : graphique en anneau par maison avec couleurs officielles.

  • viz-cdf : courbes multiples par maison avec boucle Jinja2 et distribution cumulative.

  • viz-stack : barres empilées, paramètre step dans l'URL, deux graphiques par page.