Boostez votre application shiny avec des dataviz interactives : une plongée dans la librairie JavaScript Chart.js

Author : Yohann Mansiaux
Categories : golem, javascript, shiny
Tags : javascript, shiny
Date :

Poursuivons notre exploration de l’intégration de code JavaScript au sein d’une application {shiny} ! Nous allons montrer comment sortir des graphiques classiques produits en R base ou avec {ggplot2} pour aller fouiller du côté des librairies JavaScript de production de dataviz interactives, en particulier de la librairie Chart.js.

Si vous avez manqué notre premier article sur l’intégration de librairies JavaScript dans une application {shiny} je vous invite à le lire avant de vous lancer dans celui-ci.

Des notions capitales sont abordées, et ne seront pas reprises ici. On pense notamment à :

  • Comment ajouter les dépendances d’une librairie JavaScript à une application {shiny}
  • Comment appeler du code JavaScript à partir de R

J’ai pas le temps de lire : de quoi ca parle ?

  • Faire des graphiques interactifs qui sortent des dataviz habituellement produites en R c’est possible en intégrant une librairie JavaScript !
    • On prend l’exemple de Chart.js, une librairie JavaScript de dataviz très populaire
    • Des spécificités liés à l’intégration de Chart.js dans une application {shiny} sont abordées, notamment le passage de données de R à JavaScript et la différence de formats de données attendus.
    • Nous verrons comment s’assurer du bon fonctionnement de notre code JavaScript en utilisant la console du navigateur.

Importer Chart.js dans une app {shiny} créée avec {golem}

  • Chart.js est une librairie JavaScript qui permet de réaliser de nombreux types de graphiques (barres, lignes, radar, etc.) et de personnaliser ces graphiques à l’envi
  • Elle est très bien documentée
  • Elle est la librairie JavaScript de dataviz la plus populaire sur GitHub (plus de 60000 “stars” à la date de publication de cet article)

Pour avoir un aperçu des possibilités offertes par Chart.js, rendez-vous sur la page officielle : https://www.chartjs.org/docs/latest/samples/information.html

Ajouter les dépendances nécessaires à l’app {shiny}

Les sections qui vont suivre supposent que vous avez déjà créé une app {shiny} avec {golem}.

Si ce n’est pas encore le cas et que vous souhaitez en savoir plus sur {golem}, je vous invite à consulter la documentation officielle.

Pour ajouter Chart.js à votre app {shiny}, il va falloir trouver un moyen d’incorporer les fichiers nécessaires à son fonctionnement dans votre application. Nous avons vu lors de notre article précédent que deux solutions étaient possibles.

  • Utiliser un “CDN” (Content Delivery Network) pour charger les fichiers depuis un serveur tiers.
  • Télécharger les fichiers nécessaires et les intégrer directement dans l’application.

Nous allons ici utiliser la méthode du “CDN”.

Rendez-vous sur la section “Getting Started” de la documentation officielle de Chart.js .

Nous récupérons l’url du CDN et stockons précieusement cette information pour la suite.

Après avoir créé le squelette d’une application via {golem}, nous allons ajouter la dépendance à Chart.js.

Ouvrons le fichier R/app_ui.R de notre application et ajoutons le lien que nous avons copié précédemment dans le corps de la fonction golem_add_external_resources().

golem_add_external_resources <- function() {
  add_resource_path(
    "www",
    app_sys("app/www")
  )
  tags$head(
    favicon(),
    bundle_resources(
      path = app_sys("app/www"),
      app_title = "chartJS"
    ),
    # Add here other external resources
    # for example, you can add shinyalert::useShinyalert()
    # Chart.js
    tags$script(src = "https://cdn.jsdelivr.net/npm/chart.js")
  )
}

Comment savoir si Chart.js est bien importé dans mon app {shiny} ?

La section “Getting Started” consultée précédemment pour récupérer le lien vers le CDN nous indique qu’il est nécessaire d’incorporer la balise HTML <canvas> dans notre application pour pouvoir afficher un graphique Chart.js. Nous rajoutons cet élément dans le fichier R/app_ui.R de notre application.

app_ui <- function(request) {
  tagList(
    # Leave this function for adding external resources
    golem_add_external_resources(),
    # Your application UI logic
    fluidPage(
      h1("golemchartjs"),
      tags$div(
        tags$canvas(id="myChart")
      )
    )
  )
}

Pour vérifier que Chart.js est bien importé dans notre application, nous lançons notre app avec golem::run_dev(), la suite se passe du côté du navigateur web.

NB : Les captures suivantes ont été réalisées avec le navigateur Google Chrome.

Sur la fenêtre de notre application, nous faisons un clic droit puis choisissons “Inspecter”. Dans la nouvelle fenêtre qui va s’ouvrir, on choisit l’onglet “Console” puis on tape la commande permettant de générer un graphique Chart.js, comme indiqué une nouvelle fois dans la section “Getting Started” entre les balises HTML script.

Le code à copier-coller dans la console est le suivant :

  const ctx = document.getElementById('myChart');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
      datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  });

Le graphique de la page de démonstration apparait comme attendu ! 🎉
Nous pouvons passer à la suite ! 😊

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step1

Créer un diagramme en barres (barchart) avec Chart.js

Le code utilisé précédemment nous a permis de vérifier que Chart.js était bien importé dans notre application. Nous allons maintenant voir comment créer un graphique Chart.js depuis notre application {shiny}. L’objectif étant de pouvoir produire des diagrammes en bes sur des jeux de données variés et avec une paramétrisation possible en fonction des choix des utilisateurs.

Reprenons le code exécuté précédemment :

  const ctx = document.getElementById('myChart');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
      datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  });

On pourrait imaginer vouloir passer en paramètres de fonctions les éléments labels, label, data et borderWidth.

C’est parti ! 🚀

Création d’un script JavaScript utilisable depuis R

Nous avons vu dans notre article précédent que le moyen d’appeler du code JavaScript depuis R est de passer par un “handler JS”. Pour cela, rendez-vous dans le fichier dev/02_dev.R ! Nous ajoutons la ligne suivante dans la section “External Resources” :

golem::add_js_handler("barchartJS")

Nous remplissons le squelette en indiquant “barchartJS” comme nom de notre handler et en ajoutant le code JavaScript que nous avons vu précédemment.

$(document).ready(function () {
  Shiny.addCustomMessageHandler("barchartJS", function (arg) {
    const ctx = document.getElementById("myChart");
    new Chart(ctx, {
      type: "bar",
      data: {
        labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
        datasets: [
          {
            label: "# of Votes",
            data: [12, 19, 3, 5, 2, 3],
            borderWidth: 1,
          },
        ],
      },
      options: {
        scales: {
          y: {
            beginAtZero: true,
          },
        },
      },
    });
  });
});

On remplace les paramètres labels, label, data et borderWidth, ici codés en dur, par les futurs éléments passés en argument de notre handler.
La notation à utiliser ici sera arg.nom_du_parametre pour accéder aux valeurs transmises par notre application {shiny}. La notation avec le .est une convention JavaScript pour accéder aux propriétés d’un objet. Pour faire le parallelle avec R, c’est un peu comme si on faisait arg$nom_du_parametre.

Au début de notre handler, on rajoute un appel à la fonction console.log() pour vérifier le contenu de l’élément arg depuis la console JS. Cela nous permettra de vérifier que les éléments passés depuis R sont bien transmis à notre handler.

$( document ).ready(function() {
  Shiny.addCustomMessageHandler('barchartJS', function(arg) {
    console.log(arg);    
    const ctx = document.getElementById('myChart');
    new Chart(ctx, {
      type: 'bar',
      data: {
        labels: arg.labels,
        datasets: [{
          label: arg.label,
          data: arg.data,
          borderWidth: arg.borderWidth
        }]
      },
      options: {
        scales: {
          y: {
            beginAtZero: true
          }
        }
      }
    }); 
  })
});

Nous allons rajouter du côté du fichier R/app_ui.R des éléments permettant de générer les paramètres à passer en argument de notre handler :

  • arg.labels sera un vecteur de chaînes de caractères de taille 5, tiré aléatoirement parmi les lettres de l’alphabet
  • arg.label sera une chaîne de caractères, tirée aléatoirement parmi les lettres de l’alphabet
  • arg.data sera un vecteur de nombres entiers de taille 5, tiré aléatoirement entre 1 et 100
  • arg.borderWidth sera un nombre entier, tiré aléatoirement entre 1 et 5

L’apparition du graphique sera conditionnée à un clic sur un bouton “Show Barplot”.

Voici le contenu de notre fichier R/app_ui.R :

app_ui <- function(request) {
    tagList(
        # Leave this function for adding external resources
        golem_add_external_resources(),
        # Your application UI logic
        fluidPage(
            h1("golemchartjs"),
            actionButton(
                inputId = "showbarplot",
                label = "Show Barplot"
            ),
            tags$div(
                tags$canvas(id = "myChart")
            )
        )
    )
}

Et le contenu de notre fichier R/app_server.R :

app_server <- function(input, output, session) {
    observeEvent(input$showbarplot, {
        app_labels <- sample(letters, 5)
        app_label <- paste0(sample(letters, 10), collapse = "")
        app_data <- sample(1:100, 5)
        app_borderWidth <- sample(1:5, 1)
        golem::invoke_js(
            "barchartJS",
            list(
                labels = app_labels,
                label = app_label,
                data = app_data,
                borderWidth = app_borderWidth
            )
        )
    })
}

On rappelle les points essentiels :

  • le premier paramètre de l’appel à golem::invoke_js() est le nom du handler JavaScript
  • les paramètres suivants sont les éléments à passer en argument de notre handler. Ils doivent être passée dans une liste nommée dont les noms doivent correspondent aux éléments passés dans l’objet arg de notre handler.

Lançons notre application avec golem::run_dev() et vérifions que tout fonctionne comme attendu !

Bravo ! 👏

En plus du graphique affiché, on peut remarquer la console JavaScript du navigateur nous affiche bien le contenu de l’objet arg, avec ses 4 sous-élements labels, label, data et borderWidth.

Et si clique une nouvelle fois sur le bouton, que se passe t-il ?

Le graphique ne se met pas à jour, il reste bloqué sur le premier graphique ! 😮

La console JavaScript nous indique que l’objet arg a bien été mis à jour, mais le graphique ne se met pas à jour. De plus un message d’erreur apparait dans la console JavaScript : “Error: Canvas is already in use. Chart with ID ‘0’ must be destroyed before the canvas with ID ‘myChart’ can be reused”.

Essayons de comprendre ce qui se passe : dans le fichier R/app_ui.R, nous avons ajouté un élément canvas avec l’id “myChart” (via l’instruction tags$canvas(id = "myChart")). Cet élément est utilisé pour afficher le graphique. Lorsque l’on clique sur le bouton “Show Barplot”, un nouveau graphique est généré et affiché dans cet élément. Mais le graphique précédent n’est pas détruit, et le message d’erreur nous indique que le “canvas” est déjà utilisé.

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step2

Pourquoi le graphique ne se met pas à jour ?

Pour avoir la réponse, il faut retourner sur la documentation de Chart.js. On peut lire dans la section “.destroy()” que pour être capable d’utiliser à nouveau l’élément HTML “canvas” pour afficher un nouveau graphique, il est nécessaire de détruire le graphique précédent.

Il existe aussi visiblement une commande “.update()”, pour mettre à jour un graphique existant. Cette méthode parait plus adaptée ici, en effet nous allons utiliser le même type de graphique, avec seulement quelques paramètres qui changent. Le méthode “.update()” permet de mettre à jour un graphique existant, sans avoir à le détruire et le recréer, ce qui sera moins “brutal” visuellement (avec un graphique qui disparait puis réapparait). La méthode “.destroy()” est néanmoins à garder en tête pour des cas où l’on voudrait changer radicalement le type de graphique par exemple.

Mettre à jour un graphique sous-entend qu’un graphique a déjà été généré une première fois. Il va donc falloir modifier notre handler JavaScript pour prendre en compte cette particularité et trouver un moyen de détecter l’existence d’un graphique sur notre page. Pour cela, nous allons nous tourner à nouveau vers la documentation de Chart.js, et en particulier vers la méthode getChart : https://www.chartjs.org/docs/latest/developers/api.html#static-getchart-key.

La commande à utiliser est de la forme suivante : const chart = Chart.getChart("canvas-id");. D’après la documentation, si le graphique existe, la variable chart contiendra l’objet Chart.js associé à l’élément HTML “canvas”. Si le graphique n’existe pas, la variable chart sera undefined.

Pour que cette commande fonctionne nous devons remplacer “canvas-id” par l’id de notre “canvas”, ici “myChart” : const chart = Chart.getChart("myChart");

Relançons notre application. On constate effectivement que l’objet “chart” est “undefined” tant que le graphique n’a pas été créé, et qu’il perd bien ce statut ensuite.

On pourra ainsi adapter notre code de la façon suivante :

  • si “chart” est “undefined”, on crée un nouveau graphique
  • si “chart” n’est pas “undefined”, on met à jour le graphique existant

Nous adaptons notre handler en nous inspirant de la documentation de la méthode “.update()” : https://www.chartjs.org/docs/latest/developers/api.html#update-mode :

$(document).ready(function () {
  Shiny.addCustomMessageHandler("barchartJS", function (arg) {
    console.log(arg);
    const ctx = document.getElementById("myChart");
    const chart = Chart.getChart("myChart");
    if (chart == undefined) {
      console.log("Creating a new chart");
      new Chart(ctx, {
        type: "bar",
        data: {
          labels: arg.labels,
          datasets: [
            {
              label: arg.label,
              data: arg.data,
              borderWidth: arg.borderWidth,
            },
          ],
        },
        options: {
          scales: {
            y: {
              beginAtZero: true,
            },
          },
        },
      });
    } else {
      console.log("Updating an existing chart");
      chart.data.labels = arg.labels;
      chart.data.datasets[0].label = arg.label;
      chart.data.datasets[0].data = arg.data;
      chart.data.datasets[0].borderWidth = arg.borderWidth;
      chart.update();
    }
  });
});

Cet exemple est un peu plus complexe que ceux vus jusqu’à présent :

  • On récupère l’objet Chart.js associé à l’élément HTML “canvas” avec la méthode Chart.getChart("myChart")
  • On regarde si cet objet est “undefined” : si c’est le cas on utilise le code qui fonctionnait jusqu’à maintenant pour créer un nouveau graphique
  • S’il n’est pas “undefined”, on écrase les éléments de configuration que l’on souhaite mettre à jour, puis on utilise la méthode .update(). Notez ici les spécificités de la manipulation des éléments de configuration : chart.data.labels = arg.labels pour les labels, chart.data.datasets[0].label = arg.label pour le label, etc. On utilise des . pour accéder aux propriétés des objets, chaque nouveau . permettant d’accéder à un niveau de “profondeur” plus important. Il est également important de noter que la numération des éléments dans les tableaux commence à 0 en JavaScript, et pas à 1 comme en R.

Après tous ces efforts, voyons voir si tout est rentré dans l’ordre 😄 !

Ouf tout est OK cette fois ! 🥲

On touche ici un cas un peu plus complexe de l’utilisation d’une librairie JavaScript dans une application {shiny}. Il est important de bien comprendre le fonctionnement de la librairie en allant fouiller dans les profondeurs de la documentation. Par ailleurs un des avantages d’utiliser une librairie très populaire, c’est quoi vous pourrez souvent trouver de l’aide sur StackOverflow 😊 (ici un exemple d’utilisation de la méthode “.destroy()”).

N’hésitez pas à aller plus loin dans la paramétrisation de votre graphique, en changeant par exemple les couleurs des barres : https://www.chartjs.org/docs/latest/general/colors.html et https://www.chartjs.org/docs/latest/charts/bar.html. Le plus simple pour apprendre étant d’essayer de reproduire des exemples de la documentation.

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step3

Créer un graphique en nuage de points (scatterplot) avec Chart.js

Nous allons tenter de créer un graphique en nuage de points avec Chart.js. Pour mettre au point notre code, nous allons nous appuyer sur la documentation de Chart.js : https://www.chartjs.org/docs/latest/charts/scatter.html.

Comme précédemment, notre code sera stocké dans un handler JS. Nous allons donc rajouter un nouveau handler dans le fichier dev/02_dev.R :

golem::add_js_handler("scatterplotJS")

La documentation est légèrement différente de celle fournie pour le diagramme en barres. Nous allons devoir adapter notre handler en conséquence. On identifie un élément config, qui va reprendre les éléments type, data et options que nous avons déjà vu. On a également un élément data qui contient les éléments datasets et labels.

On remplit le squelette de notre handler avec le code JavaScript de la documentation de Chart.js. Dans un premier temps nous laissons de côté la partie “update”.

$(document).ready(function () {
  Shiny.addCustomMessageHandler("scatterplotJS", function (arg) {
    const ctx = document.getElementById("myChart2");
    const data = {
      datasets: [
        {
          label: "Scatter Dataset",
          data: [
            {
              x: -10,
              y: 0,
            },
            {
              x: 0,
              y: 10,
            },
            {
              x: 10,
              y: 5,
            },
            {
              x: 0.5,
              y: 5.5,
            },
          ],
          backgroundColor: "rgb(255, 99, 132)",
        },
      ],
    };
    const config = {
      type: "scatter",
      data: data,
      options: {
        scales: {
          x: {
            type: "linear",
            position: "bottom",
          },
        },
      },
    };
    new Chart(ctx, config);
  });
});

Notre handler JS “scatterplotJS” est prêt ! Il va falloir côté UI rajouter le “div” et le “canvas” pour afficher le graphique produit. Nous avons pensé à modifier l’id HTML de notre “canvas” pour éviter tout conflit avec le graphique en barres. Ici il s’appellera “myChart2”.
On remarque une syntaxe légèrement différente par rapport au code utilisé pour le diagramme en barres, pour lequel l’appel à “new Chart” était directement fait avec les éléments data et options. Ici on stocke ces éléments dans des variables data et config avant de les passer à new Chart.

On rajoute ensuite dans le fichier R/app_ui.R :

h1("Scatterplot"),
actionButton(
    inputId = "showscatterplot",
    label = "Show Scatterplot"
),
tags$div(
    tags$canvas(id = "myChart2")
)

On rajoute dans le fichier R/app_server.R :

  observeEvent(input$showscatterplot, {
    golem::invoke_js(
      "scatterplotJS",
      list(
      )
    )
  })

Notre handler n’utilise aucun élément passé par R. Il est néanmoins nécessaire de passer une liste vide en argument pour assurer le bon fonctionnement de golem::invoke_js().

Lançons votre application avec golem::run_dev() et vérifions que tout fonctionne comme attendu !

Le graphique de la documentation fonctionne ! 🎉

Essayons maintenant d’aller plus loin en passant nos données en entrée.

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step4

Un exemple avec le jeu de données iris

Nous allons utiliser le jeu de données iris pour générer un graphique en nuage de points. Nous allons passer en argument de notre handler JS les données contenues dans les colonnes Sepal.Length et Sepal.Width.

Comme pour le diagramme en barres, nous allons utiliser des éléments passés depuis R en utilisant l’objet arg côté JavaScript.

On modifie l’objet data de telle sorte à pouvoir passer un titre de légende et surtout des données. Pour pouvoir observer les éléments passés depuis R, on rajoute un appel à console.log().

console.log(arg);
const data = {
  datasets: [
    {
      label: arg.label,
      data: arg.data,
      backgroundColor: "rgb(255, 99, 132)",
    },
  ],
};

Pour rappel, dans l’exemple de la documentation, les données sont passées sous forme d’un “tableau de dictionnaires” (du jargon JavaScript). Chaque dictionnaire contient les clés x et y pour les coordonnées du point.

data: [{
    x: -10,
    y: 0
  }, {
    x: 0,
    y: 10
  }, {
    x: 10,
    y: 5
  }, {
    x: 0.5,
    y: 5.5
}]

Essayons de passer le contenu des colonnes Sepal.Length et Sepal.Width via une liste. Nous faisons la modifications suivante côté R/app_server.R :

observeEvent(input$showscatterplot, {
    golem::invoke_js(
        "scatterplotJS",
        list(
            label = "My scatterplot",
            data = list(
                x = iris$Sepal.Length,
                y = iris$Sepal.Width
            )
        )
    )
})

On relance notre application, et malheureusement rien ne s’affiche !

Grâce à l’appel à la fonction console.log() dans notre handler, on peut observer le contenu de l’objet arg dans la console JavaScript du navigateur. On constate que les données passées ne sont pas dans le bon format. Ici on obtient un tableau de deux éléments, le premier contenant les valeurs de Sepal.Length et le second les valeurs de Sepal.Width, ce qui n’est pas le format attendu.

Ici il va falloir faire un peu de travail côté R pour transformer nos données dans le format attendu.

Si on affiche un aperçu au format JSON les données que nous avons passées en entrée, effectivement le rendu n’est pas correct.

jsonlite::toJSON(
  list(
    x = iris$Sepal.Length,
    y = iris$Sepal.Width
  )
)
#> {"x":[5.1,4.9,4.7,4.6,5,5.4,4.6,5,4.4,4.9],"y":[3.5,3,3.2,3.1,3.6,3.9,3.4,3.4,2.9,3.1]}

Pour la manipulation des listes, le package {purrr} est un allié de premier choix.

new_data <- purrr::transpose(
  list(
    x = iris$Sepal.Length,
    y = iris$Sepal.Width
  )
)
jsonlite::toJSON(
  new_data,
  auto_unbox = TRUE
)
#> [{"x":5.1,"y":3.5},{"x":4.9,"y":3},{"x":4.7,"y":3.2},{"x":4.6,"y":3.1},{"x":5,"y":3.6},{"x":5.4,"y":3.9},{"x":4.6,"y":3.4},{"x":5,"y":3.4},{"x":4.4,"y":2.9},{"x":4.9,"y":3.1}]

Le rendu a l’air plus conforme à ce qui est attendu par Chart.js. Nous allons donc modifier notre code pour passer les données de cette manière.

observeEvent(input$showscatterplot, {
    golem::invoke_js(
        "scatterplotJS",
        list(
            label = "My scatterplot",
            data = purrr::transpose(
                list(
                    x = iris$Sepal.Length,
                    y = iris$Sepal.Width
                )
            )
        )
    )
})

Observons le rendu :

Ce coup-ci c’est bon ! 😊 On peut observer dans la console JavaScript que les données ont cette fois-ci été passées au bon format.

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step5

Un petit coup de polish en plus

Les deux axes de notre graphique n’ont pas encore de titre ! Pour savoir comment faire cela, la documentation vient à notre secours encore une fois : https://www.chartjs.org/docs/latest/axes/labelling.html#scale-title-configuration.

Il s’agit a priori de rajouter un objet title dans notre objet scales déjà existant. Chaque axe, “x” et “y”, est un objet dans l’objet scales, et pourra avoir un titre, et ses paramètres associés (couleur, police …).

On va donc rajouter un élément title dans l’objet x de notre objet scales. Un certain nombre d’éléments sont paramétrables, nous allons devoir modifier le paramètre text pour définir le titre de chacun des axes et le paramètre display pour les afficher car ce paramètre est à false par défaut (attention à la manière différente d’écrire les booleens entre JavaScript et R : true/false VS TRUE/FALSE).

La documentation manque parfois d’exemple, encore une fois on peut également compter sur StackOverflow : https://stackoverflow.com/questions/27910719/in-chart-js-set-chart-title-name-of-x-axis-and-y-axis. Il faut néanmoins faire attention à la version de Chart.js utilisée, les paramètres peuvent varier.

Du côté de notre handler JS on va rajouter un paramètre xAxisTitle et un paramètre yAxisTitle.

const config = {
    type: 'scatter',
    data: data,
    options: {
      scales: {
        x: {
          type: 'linear',
          position: 'bottom',
          title: {
            display: true,
            text: arg.xAxisTitle
            }
          },
        y: {
          title: {
            display: true,
            text: arg.yAxisTitle
          }
        }
      }
    }
  };

Attention encore une fois à la différence de syntaxe entre JavaScript et R. Les paramètres sont passés sous la forme display: true et non pas display = TRUE, par exemple. Une confusion entre : et = peut vite arriver, et entrainera un code non fonctionnel.

Du côté de notre fichier R/app_server.R on va rajouter les éléments xAxisTitle et yAxisTitle dans la liste passée en argument de notre handler.

observeEvent(input$showscatterplot, {
    golem::invoke_js(
        "scatterplotJS",
        list(
            label = "My scatterplot",
            data = purrr::transpose(
                list(
                    x = iris$Sepal.Length,
                    y = iris$Sepal.Width
                )
            ),
            xAxisTitle = "Sepal Length",
            yAxisTitle = "Sepal Width"
        )
    )
})

Et voici le rendu du côté de notre application :

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step6

Et pour aller encore plus loin

D’autres modifications ont été apportées pour améliorer le graphique :

  • Modification du titre
  • Modification de la couleur de remplissage des points et de leur bordure

Voici les ressources utilisées pour produire le code que nous allons présenter juste après :

Le titre sera à inclure dans un objet plugins qui lui-même sera inclus dans l’objet options.

La couleurs des points sera gérée dans l’objet datasets.

Nous allons proposer à l’utilisateur de passer le titre du graphique, sa couleur et la couleurs des points via des inputs shiny (afin notamment de retravailler la partie “update” 😉).

Voici ci-dessous un aperçu du graphique réalisé ici (sans “update” fonctionnel pour le moment) :

Le code du handler a été complété pour prendre en compte ces nouveaux éléments:

$(document).ready(function () {
  Shiny.addCustomMessageHandler("scatterplotJS", function (arg) {
    const ctx = document.getElementById("myChart2");
    console.log(arg);
    const data = {
      datasets: [
        {
          label: arg.label,
          data: arg.data,
          borderColor: arg.pointBorderColor,
          backgroundColor: arg.pointBackGroundColor,
        },
      ],
    };
    const plugins = {
      title: {
        display: true,
        text: arg.mainTitle,
        color: arg.mainTitleColor,
      },
    };
    const config = {
      type: "scatter",
      data: data,
      options: {
        plugins: plugins,
        scales: {
          x: {
            type: "linear",
            position: "bottom",
            title: {
              display: true,
              text: arg.xAxisTitle,
            },
          },
          y: {
            title: {
              display: true,
              text: arg.yAxisTitle,
            },
          },
        },
      },
    };
    new Chart(ctx, config);
  });
});

Côté R/app_ui.R, des éléments ont été rajoutés pour permettre à l’utilisateur de passer les paramètres nécessaires :

h1("Scatterplot"),
textInput(
    inputId = "scatterplot_title",
    label = "Scatterplot Title",
    value = "ChartJS rocks !"
),
selectInput(
    inputId = "title_color",
    label = "Title Color",
    choices = c("brown", "orange", "purple"),
    selected = "brown"
),
selectInput(
  inputId = "points_background_color",
    label = "Points Background Color",
    choices = c("red", "blue", "green"),
    selected = "red"
),
actionButton(
  inputId = "showscatterplot",
    label = "Show Scatterplot"
),
tags$div(
    tags$canvas(id = "myChart2")
)

Enfin, côté R/app_server.R, on rajoute les éléments nécessaires pour passer les paramètres à notre handler :

observeEvent(input$showscatterplot, {
        golem::invoke_js(
            "scatterplotJS",
            list(
                label = "My scatterplot",
                data = purrr::transpose(
                    list(
                        x = iris$Sepal.Length,
                        y = iris$Sepal.Width
                    )
                ),
                xAxisTitle = "Sepal Length",
                yAxisTitle = "Sepal Width",
                mainTitle = input$scatterplot_title,
                mainTitleColor = input$title_color,
                pointBorderColor = "black",
                pointBackGroundColor = input$points_background_color
            )
        )
    })

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step7

Il nous manque encore l’inclusion de la méthode .update() pour prendre en compte des mises à jour des inputs shiny relatifs au titre et à la couleur des points.

On s’inspire du travail fait pour le graphique précédent pour modifier notre handler JS.

$(document).ready(function () {
  Shiny.addCustomMessageHandler("scatterplotJS", function (arg) {
    const ctx = document.getElementById("myChart2");
    console.log(arg);
    const chart2 = Chart.getChart("myChart2");
    if (chart2 == undefined) {
      console.log("Creating a new chart");
      const data = {
        datasets: [
          {
            label: arg.label,
            data: arg.data,
            borderColor: arg.pointBorderColor,
            backgroundColor: arg.pointBackGroundColor,
          },
        ],
      };
      const plugins = {
        title: {
          display: true,
          text: arg.mainTitle,
          color: arg.mainTitleColor,
        },
      };
      const config = {
        type: "scatter",
        data: data,
        options: {
          plugins: plugins,
          scales: {
            x: {
              type: "linear",
              position: "bottom",
              title: {
                display: true,
                text: arg.xAxisTitle,
              },
            },
            y: {
              title: {
                display: true,
                text: arg.yAxisTitle,
              },
            },
          },
        },
      };
      new Chart(ctx, config);
    } else {
      console.log("Updating an existing chart");
      chart2.data.datasets[0].backgroundColor = arg.pointBackGroundColor;
      chart2.options.plugins.title.text = arg.mainTitle;
      chart2.options.plugins.title.color = arg.mainTitleColor;
      chart2.update();
    }
  });
});

Observons le résultat :

Bravo ! 🎉

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step8

Modification du tooltip (niveau avancé)

Nous allons chercher à modifier le tooltip qui s’affiche lorsque la souris survole un point du graphique. En plus de modifier son titre, nous souhaitons afficher le numéro de la ligne du jeu de données correspondant au point survolé, ainsi que les valeurs de Sepal.Length et Sepal.Width.

Voici les ressources utilisées ici :

Cette partie va être plus complexe que les précédentes. Mais nous allons y arriver ! 💪

L’objet plugins, utilisé précédemment pour gérer le titre du graphique, contient un élément tooltip qui lui-même contient un élément callbacks. C’est dans cet élément que nous allons pouvoir modifier le titre et le contenu du tooltip.
La plupart des éléments du tooltip vont être configurable via un appel à une fonction prenant en entrée un élément context. Il s’agit d’un objet JavaScript qui contient un certain nombre d’éléments relatif au point survolé. Nous explorerons le contenu de cet objet pour en extraire les informations qui nous intéressent un peu plus tard, quand il s’agira de customiser le contenu du tooltip.

Nous modifions notre handler JS, en incluant un titre fixe (nous aurions également pu le passer en paramètre) :

const tooltip = {
  callbacks: {
    title: function (context) {
      return "Tooltip title";
    },
  },
};
const plugins = {
  title: {
    display: true,
    text: arg.mainTitle,
    color: arg.mainTitleColor,
  },
  tooltip: tooltip
};

Voyons si cela fonctionne:

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step9

Avançons sur la customisation du contenu du tooltip !

Il va s’agir ici de modifier le paramètre label dans l’objet tooltip. Pour mettre au point notre code, nous allons utiliser la fonction debugger que nous n’avons pas encore vue jusqu’à présent !
Si vous êtes familiers de l’utilisation de browser() en R, debugger est l’équivalent en JavaScript. Cela permet de mettre en pause l’exécution du code et d’ouvrir la console du navigateur pour explorer les éléments passés en argument à une fonction.

Modifions notre handler :

const tooltip = {
  callbacks: {
    title: function (context) {
      return "Tooltip title";
    },
    label: function(context) {
      debugger;
    }
  },
};
const plugins = {
  title: {
    display: true,
    text: arg.mainTitle,
    color: arg.mainTitleColor,
  },
  tooltip: tooltip,
};

On rajoute ici un appel au debugger JavaScript dans la fonction label de l’objet callbacks. On relance notre application :

Dès lors que l’on pointe la souris sur un point du graphique, l’exécution du code est stoppée et la console du navigateur s’ouvre. On peut alors explorer le contenu de l’objet context passé en argument de la fonction label.

On peut alors identifier les informations qui vont nous intéresser :

  • Le numéro de ligne dans le jeu de données : context.dataIndex
  • Les valeurs du point : context.formattedValue

On peut dès lors construire un tooltip personnalisé (en pensant à retirer l’appel au debugger 😉) :

const tooltip = {
  callbacks: {
    title: function (context) {
      return "Tooltip title";
    },
    label: function (context) {
      lab =
        "Line number: " +
        context.dataIndex +
        " values: " +
        context.formattedValue;
      return lab;
      },
  },
};
const plugins = {
  title: {
    display: true,
    text: arg.mainTitle,
    color: arg.mainTitleColor,
  },
  tooltip: tooltip,
};

Mission accomplie ! 🚀

Le code de l’application pour cette étape est disponible ici : https://github.com/ymansiaux/golemchartjs/tree/step10

Conclusion

Après avoir fait nos premiers pas sur l’appel de code JavaScript depuis R avec la librairie sweetalert2, nous avons ici exploré l’appel à une librairie de dataviz.

Dans les points à retenir :

  • Toujours essayer de faire fonctionner les éléments de la documentation avant de les adapter à votre application
  • Utiliser jsonlite::toJSON() pour vérifier que les données passées sont bien au format attendu par la librairie
  • Avoir en tête qu’il faut parfois “mettre à jour” ou “détruire” des objets présents sur une page Web
  • Utiliser console.log() ou debugger pour voir ce que contient un objet JavaScript passé en argument d’une fonction

Une fois passés quelques moments un peu difficiles, on aperçoit les possibilités offertes par les librairies JavaScript de dataviz. On peut aller très loin dans la personnalisation des graphiques, et les possibilités offertes par Chart.js sont très nombreuses. La documentation, couplée à de la recherche sur des forums de discussion, permettent de trouver des solutions à des problèmes qui peuvent sembler insurmontables au premier abord.

N’hésitez pas à vous lancer dans l’aventure de l’intégration de librairies JavaScript dans vos applications {shiny}. Cela peut être un excellent moyen de sortir des sentiers battus et de proposer des graphiques interactifs et personnalisés à vos utilisateurs.

A bientôt pour de nouvelles aventures ! 🚀


À propos de l'auteur

Yohann Mansiaux

Yohann Mansiaux

Data Scientist au pays des m'R'veilles


Comments


Also read