Intégrer Chart.js avec des données dynamiques JSON dans une application Grails 3

C'est le Gist Responsive Chart.js Example with AJAX Callback qui a inspiré cet article : cet extrait de page HTML, qui inclut du code JavaScript, montre comment afficher un graphe de la librairie Chart.js, avec des données JSON qui sont obtenues de manière asynchrone à l'aide de jQuery.

Voyons comment intégrer un tel graphe dans une application Grails 3, en passant en revue les différents moyens de produire les données JSON dont le graphe a besoin, du côté serveur donc, y compris avec une vue JSON.

L'application Grails dont des extraits de code sont présentés par la suite, est disponible dans mon projet odelia-grails-chart.

Cette application comprend le contrôleur ChartController associé à la vue GSP par défaut index.gsp, qui permet de générer une page HTML qui affiche des graphes Chart.js de type doughnut.
Chaque graphe est affiché grâce à un bouton qui déclenche l'exécution d'une fonction JavaScript définie dans le fichier doughtnut.js ; par exemple, on utilise ce qui suit pour le premier graphe :

1
2
3
4
5
6
7
8
9
10
<div class="panel panel-default">
    <div class="panel-heading">Doughnut chart using displayChart() and data_1 controller action</div>
    <div class="panel-body">
        <button type="button" class="btn"
          onClick="displayChart('/chart/data_1', '#invoice_status_chart_1')">Display Chart</button>
        <div id="content" role="main" style="padding: 20px">
            <canvas id="invoice_status_chart_1" width="200" height="200"></canvas>
        </div>
    </div>
</div>

La fonction JavaScript displayChart est définie dans le fichier JavaScript doughtnut.js (dans le répertoire grails-app/assets/javascripts avec Chart.min.js) qui est intégré en tant que dépendance du fichier application.js, lui-même ajouté dans le layout Grails main.
Notez que l'on transmet l'URL servant à récupérer les données JSON dans le premier argument de la fonction displayChart, tandis que le second argument définit le sélecteur jQuery permettant d'identifier l'élément canvas dans lequel le graphe va s'afficher.

Le fichier JavaScript doughtnut.js comprend les fonctions JavaScript displayChart et displayChart1, ainsi que la fonction createChart qu'elles appellent :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function createChart(selector, data) {
    var ctx = jQuery(selector).get(0).getContext("2d");                  
    new Chart(ctx).Doughnut(data);
}

function displayChart(urldata, selector) {
    jQuery.get(urldata, function(data) {
        var invoice_status_data = [
                {
                    value: data.status_temp,
                    color: "#26292C",
                    highlight: "#363B3F",
                    label: "Temp"
                },
                {
                    value: data.status_pending,
                    color: "#FFA500",
                    highlight: "#FAD694",
                    label: "Pending"
                },
                {
                    value: data.status_partial,
                    color: "#E14D43",
                    highlight: "#FF5A5E",
                    label: "Partial"
                },
                {
                    value: data.status_complete,
                    color: "#76AB48",
                    highlight: "#86BC4A",
                    label: "Complete"
                }
            ];
            createChart(selector, invoice_status_data);
        }
    )
}

function displayChart1(urldata, selector) {
    jQuery.get(urldata, function(data) {
            createChart(selector, data);
        }
    )
}

C'est la fonction de rappel passée lors de l'appel jQuery.get() qui construit les informations dont le graphe a besoin, à partir des données JSON reçues du contrôleur Grails.

Voyons de quelles différentes manières on peut générer ces données JSON.

Méthode 1 : utiliser le convertisseur Grails JSON

Pour cette première approche et l'affichage du graphe, on utilise l'appel JavaScript displayChart('/chart/data_1', '#invoice_status_chart_1') lors du clic sur le bouton.

Il y a un document JSON à générer qui permet de définir quatre valeurs ; la manière la plus simple de procéder est d'employer le convertisseur grails.converters.JSON de Grails à partir d'une Map Groovy :

1
2
3
def data_1() {
    render([status_temp: 25, status_pending: 25, status_partial: 25, status_complete: 25] as JSON)
}

L'URL /chart/data_1 correspondant à l'action data_1 produit le document JSON suivant :

1
{"status_temp":25,"status_pending":25,"status_partial":25,"status_complete":25}

Méthode 2 : utiliser un builder JSON

Pour cette deuxième méthode et l'affichage du graphe, on utilise l'appel JavaScript displayChart('/chart/data_2', '#invoice_status_chart_2') lors du clic sur le bouton.

La méthode de contrôleur Grails render accepte aussi des arguments qui permettent la construction d'un document JSON grâce à un builder JSON interne, avec donc une approche plus déclarative.
L'action data_2 ci-dessous produit le même document JSON que précédemment :

1
2
3
4
5
6
7
8
def data_2() {
    render(contentType: 'application/json') {            
        status_temp 10
        status_pending 35
        status_partial 30
        status_complete 20
    }
}

Méthode 3 : utiliser une vue JSON

Le troisème doughnut est affiché au moyen de l'appel JavaScript displayChart('/chart/data_3_1', '#invoice_status_chart_3_1').

Le projet Grails Views permet d'utiliser des technologies de vues supplémentaires dans Grails 3, à savoir des vues XML et JSON (au moment de l'écriture de cet article). Tout comme une vue GSP, une vue JSON prend la forme d'un fichier à créer dans le répertoire grails-app/views/chart (chart indiquant le contrôleur concerné), avec l'extension .gson.

On a donc une action de contrôleur nommée data_3_1 :

1
def data_3_1() { }

Ecrite de cette manière, par convention, Grails va automatiquement rechercher une vue qui porte le nom de l'action dans le réperoire grails-app/views/chart, afin de générer une réponse JSON.
Dans le projet Grails odelia-grails-chart, la vue JSON data_3_1.gson contient ceci :

1
2
3
4
5
6
json {
    status_temp 10
    status_pending 35
    status_partial 30
    status_complete 20
}

Il s'agit d'un script Groovy dans lequel on utilise la référence json, de type StreamingJsonBuilder, qui permet construire la réponse JSON.
Par défaut, et pour des raisons de performance, les vues JSON sont compilées statiquement.

Plus loin avec la vue JSON

Jusqu'à présent, le document JSON construit sert à transmettre uniquement les différentes valeurs à utiliser dans la définition complète du graphe qui figure dans le code JavaScript de la fonction displayChart.
Par ailleurs, dans le cas de la vue JSON, ces données ne devraient pas y figurer "en dur".

Utilisons une nouvelle vue JSON de telle sorte qu'elle génère la définition complète du graphe, et utilise des valeurs passées par le contrôleur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
model {
    Map data
}
json([
    [
        value: data.status_temp,
        color: "#26292C",
        highlight: "#363B3F",
        label: "Temp"
    ],
    [
        value: data.status_pending,
        color:"#FFA500",
        highlight: "#FAD694",
        label: "Pending"
    ],
    [
        value: data.status_partial,
        color:"#E14D43",
        highlight: "#FF5A5E",
        label: "Partial"
    ],
    [
        value: data.status_complete,
        color: "#76AB48",
        highlight: "#86BC4A",
        label: "Complete"
    ]
])

Procéder ainsi demande à ce que l'action liée du contrôleur Grails transmette les valeurs au travers d'une variable data du modèle :

1
2
3
def data_3_2() {
    [data: [status_temp: 70, status_pending: 10, status_partial: 10, status_complete: 10]]
}

Dans la vue JSON qui est compilée par Grails, il est nécessaire de préciser le type de la variable data dans la construction model {}, sous peine d'un échec de compilation.

Avec la définition JSON complète produite dans la vue, on utilise maintenant la fonction JavaScript displayChart1 qui est beaucoup plus simple que la fonction displayChart utilisée jusqu'ici.

Dans le projet odelia-grails-chart, cela est donné en démonstration avec le quatrième graphe, qui est affiché avec l'appel displayChart1('/chart/data_3_2', '#invoice_status_chart_3_2'.

D'autre part, la définition du graphe utilise des libellés fixes en anglais ("Temp", "Pending", "Partial" et "Complete"), ce qu'il vaudrait mieux éviter si l'on écrit une application multilingue.

Pour cela, on peut utiliser dans la vue, la méthode g.message qui fait partie du support i18n de Grails et qui permet de récupérer un message traduit dans la Locale en cours, en fonction d'un code (par exemple g.message(code: 'chart.status.temp')).
On peut également créer des vues spécifiques en ajoutant le code de la Locale au nom de la vue ; par exemple data_3_2_fr.gson pour le français ou data_3_2_en.gson pour l'anglais. Grails choisira automatiquement la vue adéquate en fonction de la langue.
Pour tester cet aspect-là, vous pouvez passer le code langue dans le paramètre d'URL lang (par exemple : http://localhost:8080/chart?lang=fr).

N'hésitez pas à consulter la documentation du projet Grails Views pour en savoir plus sur les vues JSON ou XML !