.. _ref-viz: ========================================= Chapitre 14 : La visualisation de données ========================================= .. _ref-chartjs: 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 ` 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 ` qui est assez simple à mettre en œuvre tout en donnant un excellent résultat au niveau graphique. `Chart.js ` 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 ` 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`. .. code-block:: html 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 ``. 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. .. code-block:: 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 `#`. .. literalinclude:: /figures/chartjs/simple.html :language: 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. .. figure:: /figures/chartjs/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. .. figure:: /figures/chartjs/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. .. figure:: /figures/chartjs/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. .. only:: html Prenons le fichier :download:`/figures/chartjs/data.csv` qui contient les résultats de 468 étudiants à une interrogation. .. only:: pdf Prénom le fichier `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. .. figure:: /figures/chartjs/stud-1.png :scale: 200% 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. .. figure:: /figures/chartjs/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. .. figure:: /figures/chartjs/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. .. figure:: /figures/chartjs/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. .. _ref-chartjs-flask: 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 }}``. .. warning:: 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 ``"`` 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 : .. code-block:: python # 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 : .. code-block:: python # 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 ````, et on insère les données Python directement dans le code JavaScript : .. code-block:: html My App

Moyenne des étudiants en 1991

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. .. figure:: /figures/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 : .. code-block:: 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 : .. code-block:: html 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. .. figure:: /figures/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 : .. code-block:: html 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 ````. - ``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 : .. code-block:: python # 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 : .. code-block:: python 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 : .. code-block:: html .. figure:: /figures/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 : .. code-block:: python @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 : .. code-block:: html .. 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()`` : .. code-block:: python 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 : .. code-block:: html

Distribution par maison (barres groupées)

Distribution par maison (barres empilées)

.. note:: Lorsque plusieurs graphiques Chart.js apparaissent sur la même page, chaque graphique doit avoir son propre ```` avec un ``id`` différent, et son propre bloc ``