UE5 : Fondamentaux du développement web frontal, niveau 2

Séance 7 : TD: rendre un graphique de manière asynchrone

  1. Initialisation de l'application
    1. Télécharger la base de données
    2. Installer un environnement virtuel et y installer les librairies nécessaires
    3. Indiquer les variables d'environnement
  2. Etude du code de la route concernée
    1. Template templates/pages/graphiques/ressources_pays.html
    2. Route /graphiques/ressources_pays de routes/graphiques.py
    3. Vues schématiques du rendu graphique
  3. Modifier le code

Notes préalables:

new Chart(ctx, {
    type: 'polarArea',
    data: {
        labels: {{labels |tojson}},
        datasets: [{
            data: {{nombres |tojson}}
        }]
    },
    options: {
    }
});
new Chart(ctx, {
    type: 'polarArea',
    data: {
        labels: ["China", "Turkey", "France", "United States", "Canada", "Australia", "Namibia", "Spain", "United Kingdom", "South Africa", "Cote d\u0027Ivoire", "Burundi", "Brazil", "Bosnia and Herzegovina", "Serbia", "Portugal", "Greenland", "Ukraine", "Mali", "Kazakhstan"],
        datasets: [{
            data: [36, 21, 21, 20, 20, 19, 18, 18, 17, 17, 17, 17, 17, 17, 16, 16, 16, 15, 15, 15]
        }]
    },
    options: {
    }
});

Initialisation de l'application

A l'intérieur d'un dossier (par exemple ses Documents), cloner le dépôt git de l'application https://github.com/MaximeChallon/CoursM2TNAH_Flask_app_finale:

git clone https://github.com/MaximeChallon/CoursM2TNAH_Flask_app_finale.git

Cloning into 'CoursM2TNAH_Flask_app_finale'...
remote: Enumerating objects: 62, done.
remote: Counting objects: 100% (62/62), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 62 (delta 15), reused 62 (delta 15), pack-reused 0
Unpacking objects: 100% (62/62), done.

Renommer ce dossier en graphique_asynchrone:

mv CoursM2TNAH_Flask_app_finale graphique_asynchrone

Il faut maintenant configurer l'application pour qu'elle puisse fonctionner:

Télécharger la base de données

La base de données à utiliser se trouve à l'URL suivante: https://github.com/MaximeChallon/CoursM2TNAH_Flask_code/blob/master/factbook_users2.sqlite. Comme depuis le début duc ours, elle sera à placer à la racine de l'application, donc dans le dossier graphique_asynchrone/.

cd graphique_asynchrone/

Pour la télécharger, deux options:

Avec wget, faire ce qui suit:

wget https://github.com/MaximeChallon/CoursM2TNAH_Flask_code/blob/master/factbook_users3.sqlite

--2023-02-25 09:36:13--  https://github.com/MaximeChallon/CoursM2TNAH_Flask_code/blob/master/factbook_users3.sqlite
Resolving github.com... 140.82.121.4
Connecting to github.com|140.82.121.4|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: `factbook_users3.sqlite'        [ <=> ] 138,598     --.-K/s   in 0.04s
2023-02-25 09:36:14 (3.09 MB/s) - `factbook_users3.sqlite' saved [138598]            ✓

Installer un environnement virtuel et y installer les librairies nécessaires

virtualenv env -p python3


created virtual environment CPython3.11.0.final.0-64 in 6970ms
  creator CPython3Windows(dest=\.......\graphique_asynchrone\env, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=\.....\virtualenv)
    added seed packages: pip==22.3.1, setuptools==66.1.1, wheel==0.38.4
  activators BashActivator,BatchActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

Pour toute variante d'OS, voir le cours de la séance 1.

Activer cet environnement avec source env/bin/activate ou source env/Scripts/activate.

Installer les dépendances nécessaires à l'application. Ces dépendances sont listées, avec leur numéro de version, dans le fichier requirements.txt. Pour les installer, exécuter la commande suivante:

pip install -r requirements.txt

Collecting click==8.1.3
  Using cached click-8.1.3-py3-none-any.whl (96 kB)
Collecting colorama==0.4.6
  Using cached colorama-0.4.6-py2.py3-none-any.whl (25 kB)
Collecting Flask==2.2.2
  Using cached Flask-2.2.2-py3-none-any.whl (101 kB)
Collecting Flask-Login==0.6.2
  Using cached Flask_Login-0.6.2-py3-none-any.whl (17 kB)
Collecting Flask-SQLAlchemy==3.0.2
  Using cached Flask_SQLAlchemy-3.0.2-py3-none-any.whl (24 kB)
Collecting Flask-WTF==1.0.1
  Using cached Flask_WTF-1.0.1-py3-none-any.whl (12 kB)
Collecting greenlet==2.0.1
  Using cached greenlet-2.0.1-cp311-cp311-win_amd64.whl (191 kB)
Collecting itsdangerous==2.1.2
  Using cached itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting Jinja2==3.1.2
  Using cached Jinja2-3.1.2-py3-none-any.whl (133 kB)
Collecting MarkupSafe==2.1.1
  Using cached MarkupSafe-2.1.1-py3-none-any.whl
Collecting python-dotenv==0.21.0
  Using cached python_dotenv-0.21.0-py3-none-any.whl (18 kB)
Collecting SQLAlchemy==1.4.44
  Using cached SQLAlchemy-1.4.44-cp311-cp311-win_amd64.whl (1.6 MB)
Collecting Werkzeug==2.2.2
  Using cached Werkzeug-2.2.2-py3-none-any.whl (232 kB)
Collecting WTForms==3.0.1
  Using cached WTForms-3.0.1-py3-none-any.whl (136 kB)
Installing collected packages: python-dotenv, MarkupSafe, itsdangerous, greenlet, colorama, WTForms, Werkzeug, SQLAlchemy, Jinja2, click, Flask, Flask-WTF, Flask-SQLAlchemy, Flask-Login
Successfully installed Flask-2.2.2 Flask-Login-0.6.2 Flask-SQLAlchemy-3.0.2 Flask-WTF-1.0.1 Jinja2-3.1.2 MarkupSafe-2.1.1 SQLAlchemy-1.4.44 WTForms-3.0.1 Werkzeug-2.2.2 click-8.1.3 colorama-0.4.6 greenlet-2.0.1 itsdangerous-2.1.2 python-dotenv-0.21.0

Indiquer les variables d'environnement

Enfin, il manque le fichier .env qui contient les variables d'environnement. CRéer ce fichier à côté de la base de données, dans le dossier graphique_asynchrone, et le remplir avec les variables suivantes:

Il est temps de lancer l'application : python run.py.

L'application doit normalement renvoyer une erreur: Exception: Install 'email_validator' for email validation support.. En lisant l'erreur, on comprend que la librairie email_valdator manque, il suffit donc de l'installer:

pip install email_validator

Collecting email_validator
  Using cached email_validator-1.3.1-py2.py3-none-any.whl (22 kB)
Collecting dnspython>=1.15.0
  Using cached dnspython-2.3.0-py3-none-any.whl (283 kB)
Collecting idna>=2.0.0
  Using cached idna-3.4-py3-none-any.whl (61 kB)
Installing collected packages: idna, dnspython, email_validator
Successfully installed dnspython-2.3.0 email_validator-1.3.1 idna-3.4

python run.py: tout fonctionne, on peut naviguer sur le site. Pour voir la route /graphiques/ressources_pays, il faudra supprimer le @login_required présent dans le code; la page est actuellement protégée par une connexion utilisateur.

Dans le fichier app/routes/graphiques.py, supprimer @login_required. La route sera alors accessible sans avoir besoin d'être connecté.

Etude du code de la route concernée

Avant d'effectuer des modifications dans le code, il faut comprendre l'actuel, en partant du template, puisque c'est le template qui est le plus proche du résultat affiché.

Template templates/pages/graphiques/ressources_pays.html

Le graphique est créé par la librairie Javascript Chart.js. Sans même lire la documentation, on comprend que l'objet Chart n'a pas besoin que de deux variables pour fonctionner. Ces deux variables sont les données à ingérer pour rendre le graphique:

Actuellement, les valeurs arrivent par deux variables Jinja (labels et nombres) provenant directement de la route. En d'autres termes, le chargement de la page se fait de manière synchrone (elle ne s'affiche que quand les données ont été calculées du côté de la route: plus il y aura de données à calculer, plus le temps de chargement sera long). De plus, si on regarde le code source de la page, les données se retrouvent en clair dans le Javascript!

new Chart(ctx, {
    type: 'polarArea',
    data: {
        labels: ["China", "Turkey", "France", "United States", "Canada", "Australia", "Namibia", "Spain", "United Kingdom", "South Africa", "Cote d\u0027Ivoire", "Burundi", "Brazil", "Bosnia and Herzegovina", "Serbia", "Portugal", "Greenland", "Ukraine", "Mali", "Kazakhstan"],
        datasets: [{
            data: [36, 21, 21, 20, 20, 19, 18, 18, 17, 17, 17, 17, 17, 17, 16, 16, 16, 15, 15, 15]
        }]
    },
    options: {
    }
});

Route /graphiques/ressources_pays de routes/graphiques.py

La route récupère les 20 pays qui ont le plus de ressources. Pour remplir les variables Jinja, labels et nombres sont précalculés pour être utilisés directement dans le Chart JS.

En cas de lenteurs sur la base de données, ou de calculs plus longs à effectuer, l'utilisateur n'aura aucune réponse à l'écran.

Vues schématiques du rendu graphique

Actuellement, voici une vue schématique du rendu du graphique depuis l'appel utilisateur jusqu'à l'affichage.

L'objectif est d'obtenir un déroulé qui permette de donner une première réponse à l'utilisateur, et de lui indiquer que les données vont arriver et sont en cours de calcul. Cela évitera de perdre un utilisateur sur le site en raison de temps de réponse trop longs.

Modifier le code

La première étape consiste à sortir l'appel aux données de la route graphiques/ressources_pays, et de mettre ces données dans une nouvelle route. C'est cette nouvelle route qui sera ensuite appelée par JS.

# routes/graphiques.py

from ..app import app, db
from flask import render_template, request, flash
from flask_login import login_required
from ..models.factbook import Country, country_resources
from sqlalchemy import func, text

@app.route("/graphiques/ressources_pays", methods=['GET', 'POST'])
def graphiques_ressources_pays():
    return render_template("pages/graphiques/ressources_pays.html")

@app.route("/graphiques/ressources_pays_donnees", methods=['GET', 'POST'])
def graphiques_ressources_pays_donnees():
    donnees_brutes = db.session.query(Country, func.count(country_resources.c.resource).label('total'))\
        .join(country_resources, )\
        .group_by(Country.id)\
        .order_by(text('total DESC'))\
        .limit(20)

    donnees = []

    for pays in donnees_brutes.all():
        donnees.append({
            "label": pays[0].name,
            "nombre": pays.total
        })

    return donnees

La nouvelle route renvoie les données structurées en une liste d'objets comprenant chacun les deux clés label et nombre. C'est JS qui se chargera de parser les objets et de les utiliser.

Il faut ensuite modifier le JS dans le template templates/pages/graphiques/ressources_pays.html afin d'enlever les variables Jinja et d'afficher dans un premier temps un graphique vide grâce à des listes vides sur labels et data.

const ctx = document.getElementById('graphique');

new Chart(ctx, {
    type: 'polarArea',
    data: {
        labels: [],
        datasets: [{
            data: []
        }]
    },
    options: {
    }
});

Afin d'aller chercher les données, il faut appeler l'URL de la route graphiques_ressources_pays_donnees, puis récupérer les données et les ajouter dans le graphique. En JS, c'est fetch() qui permet d'appeler une URL. On récupère ensuite la réponse avec .then(response) et on peut enfin utiliser les données avec .then(data).

...

fetch('{{url_for("graphiques_ressources_pays_donnees")}}')
    .then((response) => {
        return response.json();
    })
    .then((data) => {
        // calcul des labels et des nombres
        var labels = [];
        var nombres = [];

        // itération sur le retour de l'URL graphiques_ressources_pays_donnees
        for (var i = 0; i < data.length; i++) {
            // comme en Python, on rempli ici les tableaux
            labels.push(data[i].label);
            nombres.push(data[i].nombre);
        }
        
        // ajout des données dans le graphique
        graphe.data.labels = labels; ;
        graphe.data.datasets.forEach((dataset) => {
            dataset.data = nombres;
        });
        // mise à jout du graphique une fois les données calculées et insérées dans le graphique
        graphe.update();
        
    });

Lorsqu'on appele la page, les données sont bien chargées, mais elles sont toutes noires ou grises, ce qui n'est vraiment pas joli.

On va donc terminer par mettre des couleurs sur le graphique. L'update a effacé les couleurs qu'il y avait auparavant. Pour cela, il va falloir utiliser un plugin de Chart.js, et donc retrograder la version de chart.js.

A la place de

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

Mettre ce qui suit pour réimporter Chart.js, et importer deux plugins

<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-deferred@1.0.2/dist/chartjs-plugin-deferred.min.js"></script>
<script type="text/javascript" src="https://github.com/nagix/chartjs-plugin-colorschemes/releases/download/v0.4.0/chartjs-plugin-colorschemes.js"></script>

Enfin, juste avant le update, mettre ce qui suit:

...
    graphe.options.plugins.colorschemes.scheme = 'tableau.RedGold21';
    graphe.update();

Relancer l'application. Les couleurs apparaissent bien.