Applications interactives avec Shiny

Charles Martin

Avril 2021

Introduction

Aujourd’hui dans cet atelier, nous verrons comment créer des applications interactives dans R, à l’aide de la librairie Shiny. Vous apprendrez comment créer des environnements dans lesquels vos lecteurs pourront intéragir avec les données, plutôt que de simplement utiliser un produit fini statique.

Pour vous donner une idée des possibilités, n’hésitez pas à consuler la galerie d’applications Shiny sur le site officiel : https://shiny.rstudio.com/gallery/

En conséquence, cet atelier demandera à peu près aucune habilité statistique, mais impliquera beaucoup de programmation.

Avant de se lancer dans la programmation de notre première application Shiny, il importe de bien comprendre la structure d’une telle application. Nous devrons définir deux fonctions principales pour faire fonctionner notre code.

Une première fonction définira l’interface que l’on présentera à l’utilisateur (User Interface;ui). L’interface sera présentée à l’utilisateur sous la forme d’une page Web dans son navigateur. Vous n’aurez heureusement pas à écrire de HTML/Javascript/CSS pour y arriver. Vous définirez ce que vous voulez dans votre interface, et Shiny se chargera de transformer vos idées en HTML.

Une seconde fonction définira les actions à prendre lorsqu’un utilisateur utilisera l’interface, que l’on appelle la fonction de serveur (Server en anglais).

Après avoir défini ces deux fonctions, nous devrons créer un objet d’application qui connectera l’interface au serveur, puis simplement lancer l’application.

Cela peut vous sembler inutilement compliqué, mais séparer l’interface du code de serveur est une pratique qui a prouvé son efficacité en informatique depuis plusieurs décénnies. Même que dans d’autres contextes, on parle souvent de MVC (Model-View-Controller) où l’on sépare l’application en trois morceaux : la gestion de la base de données, l’interface et la gestion de action faites par l’utilisateur. En fait, les auteurs de Shiny insistent tellement sur cette structure que dans les premières versions de la librairie, le code de l’interface et du serveur devaient être définis dans deux fichiers différents.

Une première application

Ceci étant dit, attaquons-nous à notre première petite application Shiny. Je vous propose pour commencer de créer un petit histogramme interactif où l’utilisateur pourra choisir un nombre d’échantillons à piger dans une loi normale et un nombre de classes pour son histogramme.

D’abord notre code doit activer la librairie Shiny et nous aurons aussi besoin de ggplot2, dplyr et tidyr, comme à l’habitude.

library(shiny)
library(ggplot2)
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(tidyr)

Ensuite, nous définirons l’interface que nous présenterons à l’utilisateur. Voici d’abord le code qui structurera la page :

interface <- fluidPage(
  titlePanel("Notre première application Shiny"),
  sidebarPanel(),
  mainPanel()
)

On demande donc à R de nous créer une interface avec un panneau de titre, une barre latérale, dans laquelle nous pourrons mettre les contrôles et un panneau principal dans lequel nous pourrons mettre le graphique.

Nous allons maintenant modifier ce code pour ajouter les morceaux individuels de l’interface dans les panneaux :

interface <- fluidPage(
  titlePanel("Notre première application Shiny"),
  sidebarPanel(
    sliderInput(
      inputId = "nb_classes",
      label = "Nombre de classes:",
      min = 1,
      max = 50,
      value = 30
    ),
    sliderInput(
      inputId = "nb_echantillons",
      label = "Nombre d'échantillons:",
      min = 1,
      max = 100,
      value = 25
    )
  ),
  mainPanel(
    plotOutput(outputId = "graphique_histogramme")
  )
)

Remarquez que pour chacun des contrôles, on spécifie on ID, qui nous permettra de faire référence à ce contrôle dans le code du serveur.

Donc, maintenant que notre interface est définie, on va se créer un petit serveur qui utilisera les valeurs choisies dans l’interface pour personnaliser l’histogramme :

serveur <- function(input, output) {

  output$graphique_histogramme <- renderPlot({

    # Bloquer le générateur de nombre aléatoires pour toujours obtenir la même séquence
    set.seed(4847)
    donnees <- data.frame(
      x = rnorm(n = input$nb_echantillons)
    )

    ggplot(donnees, aes(x = x)) +
      geom_histogram(bins = input$nb_classes)

  })

}

Notre fonction serveur doit toujours recevoir deux arguments, des objets nommés input et output. Ces arguments permettent respectivement de recueillir de l’information ou d’en présenter.

Ne reste maintenant qu’à tout connecter ça ensemble dans une application et de la lancer :

appli <- shinyApp(ui = interface, server = serveur)
runApp(appli)

Une variété de contrôles disponibles

Maintenant que nous avons créé notre première application, voyons un peu quels sont les contrôles que l’on présenter à l’utilisateur et les mettre en application pour personnaliser un graphique.

Nous allons créer utiliser des sliders doubles pour déterminer quelles sections de l’axe des X et Y présenter, un slider simple pour la taille des points, des cases à cocher pour quels groupes afficher dans le graphique, un bouton radio pour le type de courbe de lissage et une boîte de texte pour choisir le titre du graphique.

Créons-nous d’abord une nouvelle interface :

interface2 <- fluidPage(

  titlePanel("Deuxième application : les contrôles"),

  sidebarPanel(

    sliderInput("limites_x", "Limites de l'axe des X",min = 0, max = 7, value = c(0, 7)),
    sliderInput("limites_y", "Limites de l'axe des Y",min = 0, max = 25, value = c(0, 25)),
    sliderInput("taille_points", "Taille des points", min=0, max = 10, value = 1),

    radioButtons(
      "type_lissage",
      "Type de lissage",
      choices = list("Régression" = "lm","Loess" = "loess"),
      selected = "lm"
    ),

    textInput("titre", "Titre du graphique", value = "Entrez votre titre ici"),

    checkboxGroupInput(
      "groupes",
      "Quel groupes voulez-vous afficher?",
      choices = list(
        "Herbivores" = "herbi",
        "Carnivores" = "carni",
        "Omnivores" = "omni",
        "Insectivores" = "insecti"
        ),
      selected = c("carni","herbi","omni","insecti")
    )
  ),

  mainPanel(
    plotOutput(outputId = "graphique")
  )

)

Et maintenant notre code de serveur pour expliquer à Shiny comment personnaliser le graphique en fonction des valeurs choisies dans les contrôles.

serveur2 <- function(input, output) {

  output$graphique <- renderPlot({

    msleep %>%
      filter(vore %in% input$groupes) %>%
      ggplot(aes(x = sleep_rem, y = sleep_total)) +
        geom_point(size = input$taille_points, aes(color = vore)) +
        xlim(input$limites_x) +
        ylim(input$limites_y) +
        geom_smooth(method = input$type_lissage) +
        labs(title = input$titre)
  })
}
appli2 <- shinyApp(ui = interface2, server = serveur2)
runApp(appli2)

Remarquez la nuance entre les contrôles de type radio (type de lissage) et les cases à cocher (groupes à afficher). La convention est que les boutons radio servent habituellement à choisir une seule option à la fois et les cases à cocher permettent d’en choisir plusieurs. Vous remarquerez cependant que certains sites et/ou logiciels n’y prêtent que peu d’attention.

Les éléments disponibles en sortie

Maintenant que nous avons vu comment recueillir les choix de l’utilisateur, voyons un peu comment lui afficher de l’information.

Nous avons déjà vu dans les exemples précédents comment afficher un graphique avec l’objet plotOutput. Il existe aussi des objets pour afficher des tableaux (tableOutput) et du texte (textOutput).

Mettons en action tous ces morceaux dans une 3e petite application dans laquelle nous permettrons à l’utilisateur d’explorer à nouveau les données du tableau msleep.

Vous verrez aussi du même coup que j’utiliserai des éléments nommés h1, h2, etc. qui permettent de créer dans éléments de titre dans l’application.

interface3 <- fluidPage(
  h1("Troisième application : les sorties"),
  mainPanel(
    h2("Explorer les sous-groupes"),

    h3("Choix des groupes"),
    checkboxGroupInput(
      "groupes",
      "Quel groupes voulez-vous afficher?",
      choices = list(
        "Herbivores" = "herbi",
        "Carnivores" = "carni",
        "Omnivores" = "omni",
        "Insectivores" = "insecti"
        ),
      selected = c("carni","herbi","omni","insecti")
    ),

    h3("Résultats"),
    textOutput(outputId = "chiffre_correlation"),
    plotOutput(outputId = "graphique_correlation"),
    tableOutput(outputId = "donnees_correlation")
  )

)

Dans le code du serveur, nous allons simplement filtrer le tableau selon les choix de l’utilisateur, puis calculer une corrélation et afficher les données dans un graphique. Remarquez que nous avons maintenant plusieurs fonctions render-qqchose. Une pour chacun des objets de type output de notre application.

serveur3 <- function(input, output) {

  output$graphique_correlation <- renderPlot({
      sous_groupes <- msleep %>% filter(vore %in% input$groupes)
      ggplot(sous_groupes, aes(x = sleep_rem, y = sleep_total)) +
        geom_point()
  })

  output$chiffre_correlation <- renderText({
      sous_groupes <- msleep %>% filter(vore %in% input$groupes) %>%
        select(sleep_rem, sleep_total) %>%
        drop_na
      paste0(
        "Corrélation de Pearson : ",
        cor(sous_groupes$sleep_rem, sous_groupes$sleep_total)
      )
  })

  output$donnees_correlation <- renderTable({
      sous_groupes <- msleep %>%
        filter(vore %in% input$groupes) %>%
        select(name, sleep_rem, sleep_total, vore) %>%
        drop_na
      sous_groupes
  })

}
appli3 <- shinyApp(ui = interface3, server = serveur3)
runApp(appli3)

Rendez votre application plus efficace

Vous avez sans doute remarqué que dans l’application précédente, nous avons dupliqué à plusieurs endroits l’opération de filtrer le tableau msleep en tableau sous_groupes.

Pour pouvoir rendre ce code plus efficace, il faut d’abord comprendre un peu les rouages internes d’une application Shiny. Vous avez sans doute remarqué que lorsque vous changer la valeur des contrôles, ce n’est pas la page entière de votre application qui est rechargée, mais uniquement les sorties. C’est que, au moment où vous lancez l’application, Shiny explore le code des fonctions render et va y attacher un petit observateur, qui va relancer ce code uniquement si les contrôles dont il a besoin sont changés.

Ce qui rendrait notre application beaucoup plus efficace ici est si on pouvait associer un tableau de données à un contrôle plutôt qu’uniquement un objet output. La façon de le faire est d’utiliser ce que Shiny nomme expresssion réactive.

L’expression réactive est une fonction intelligente, exécute le code qu’elle contient uniquement si les contrôles auxquels elle fait référence sont modifiés. Sinon, elle garde en mémoire son résultat (elle le met en cache) et nous le renvoie sans refaire le calcul.

serveur3b <- function(input, output) {

  sous_groupes <- reactive({
    msleep %>% filter(vore %in% input$groupes) %>%
      select(sleep_total, sleep_rem, vore, name) %>%
      drop_na

  })

  output$graphique_correlation <- renderPlot({
      ggplot(sous_groupes(), aes(x = sleep_rem, y = sleep_total)) +
        geom_point()
  })

  output$chiffre_correlation <- renderText({
      paste0(
        "Corrélation de Pearson : ",
        cor(sous_groupes()$sleep_rem, sous_groupes()$sleep_total)
      )
  })

  output$donnees_correlation <- renderTable({
      sous_groupes()
  })

  output$ensemble_donnees <- renderDataTable({
    msleep
  })

}
appli3b <- shinyApp(ui = interface3, server = serveur3b)
runApp(appli3b)

Remarquez que sous_groupes doit maintenant être appelé comme une fonction, à l’aide des parenthèses, plutôt que comme un objet normal de R. C’est un peu plus malcommode, mais ça permet de centraliser la préparation des données à un seul endroit et de ne pas gaspiller de temps en calculs redondants.

Cela peut paraître futile pour un tableau de 80 lignes, mais si votre tableau de données en contient 100 000, ça peut faire toute la différence sur l’expérience utilisateur.

Les règles de conservation et de partage d’information

Comme votre application Shiny n’est plus un simple que script, mais bien une application web qui peut être utilisée par plusieurs utilisateurs simultanément, il est important de comprendre comment l’information est compartimentée.

Il existe essentiellement deux niveaux : les objets globaux, qui sont partagés par tous les utilisateurs simultanément, et les objets de session, qui sont privés à chaque personne qui utilise l’application. Les objets appartiendront à un contexte ou un autre, selon l’endroit où ils sont définis dans votre application.

Si on fait au plus simple, tous les objets définis à l’extérieur des fonctions serveur et interface sont des objets globaux, et tous ceux définis dans la fonction de serveur sont des objets de session, spécifiques à chaque utilisateur. Jusqu’à maintenant, c’est ce dernier type que nous avons utilisé. On ne voudrait pas par exemple que les choix que Pierre fait pour son graphique changent sorties affichées à Jacques.

Par contre, il pourrait arriver que les utilisateurs veuillent partager de l’information.

Pour illustrer ce concept, nous allons nous construire une petite application de quiz, pour deviner la corrélation entre deux variables.

Dans ce dernier, nous aurons besoin non seulement d’une fonction d’interface et d’une fonction de serveur, mais aussi d’un tableau de données global, qui stockera les meilleurs scores des utilisateurs.

Remarquez que notre data.frame de scores a été emballé dans une fonction reactiveValues. Cette dernière permet à Shiny d’être informé des changements au data.frame, qui remettre à jour tous les outputs qui utilisent ce tableau.

À cette étape, aussi à l’extérieur des fonctions de serveur et d’interface, je prépare aussi le tableau de données pour la corrélation, puisque ce dernier sera aussi commun à tous les visiteurs. Par contre, on ne l’emballe pas dans une fonction reactiveValues, puisque les valeurs dans le tableau ne changeront jamais.

objets_reactifs <- reactiveValues(
  meilleurs_scores = data.frame(
    Nom = character(),
    Score = numeric()
  )
)

donnees_pour_correlation <- data.frame(
  x = c(0.7, 0.4, 0.4, 0.8, 0.1, 0.2, 0.2, 0.6, 0.2, 0.6),
  y = c(0.7, 0.7, 0.1, 0.9, -0.2, 0.4, 0.3, 0.7, 0.5, 0.6)
)

Pour notre interface, rien de sorcier. Ce sont des objets input et ouput comme nous en avons vu plus tôt. J’utilise aussi la fonction p (pour paragraph) pour afficher du texte à l’utilisateur et je contrôle la taille de la boîte grahpique avec les arguments width et height.

J’ai ajouté à la fin un Action Button, sur lequel l’utilisateur devra cliquer pour valider son essai.

interface_quiz <- fluidPage(

  h1("Un petit quiz"),

  mainPanel(

    p("Voici un graphique présentant la corrélation entre deux variables :"),

    plotOutput("graphique_correlation", width = "200px", height = "200px"),

    p("Selon vous, quelle est la valeur de la corrélation illustrée?"),

    textInput("nom", "Votre nom", value = ""),
    sliderInput("essai", "Votre essai", value = 0, min=-1,max=1,step=0.01),
    actionButton("envoyer", label = "Envoyer"),

    h2("5 Meilleurs scores : "),
    tableOutput("tableau_scores")

  )
)

Notre fonction de serveur contient 3 blocs de code.

Les deux premiers qui définissent quoi afficher dans notre tableau de résultats et notre graphique de corrélation. C’est la même poutine que dans les exemples précédents.

Le troisième bout de code, lui, définit la logique à appliquer lorsque l’utilisateur appuie sur le bouton Envoyer. Il “observe” le bouton Envoyer et s’exécute lorsque quelqu’un clique.

À ce moment, on calcule son score en faisant la différence entre la corrélation réelle et l’essai de notre utilisateur.

On met ensuite cette information sous forme de tableau de données.

On affiche le score à l’utilisateur à l’aide d’une fonction de notification.

Et enfin, on met à jour le tableau des meilleurs scores. Remarquez que pour ce dernier, on utilise la double flèche («-), pour sortir de la fonction serveur_quiz, et aller appliquer ce changement globalement, pour tous les utilisateurs.

Puisque le tableau des meilleurs scores est un objet réactif, l’output qui y est relié sera aussi mis à jour automatiquement, et ce, dans la fenêtre de tous les utilisateurs connectés à ce moment.

serveur_quiz <- function(input, output) {

    output$tableau_scores <- renderTable({
      objets_reactifs$meilleurs_scores %>%
      arrange(Score) %>%
      slice(1:5)
  })

  output$graphique_correlation <- renderPlot(
    ggplot(donnees_pour_correlation,aes(x=x,y=y)) +
      geom_point()
  )

  observeEvent(input$envoyer, {

    score = abs(
      input$essai -
        cor(donnees_pour_correlation$x,donnees_pour_correlation$y)
    )

    nouvel_essai <- data.frame(
      Nom = input$nom,
      Score = score
    )


    showNotification(paste0("Vous vous êtes trompé par ",score))

    objets_reactifs$meilleurs_scores <<-
      objets_reactifs$meilleurs_scores %>%
      bind_rows(nouvel_essai)

  })


}
appli_quiz <- shinyApp(ui = interface_quiz, server = serveur_quiz)
runApp(appli_quiz)

Évidement, à l’heure actuelle, vous ne pouvez pas partager vos scores avec d’autres utilisateurs puisque votre application n’existe que sur votre ordinateur personnel. Nous verrons comment faire pour publier votre application dans la prochaine section

Remarquez aussi que chaque fois que l’application sera arrêtée, toute l’historique des scores sera perdue.

Les façons de publier une application Shiny

Il est important de comprendre que pour faire fonctionner une application Shiny, vous aurez besoin d’un serveur. Lorsque vous lancez la commande runApp, R démarre pour vous un petit serveur Shiny sur votre ordinateur pour que vous puissiez tester votre application. Mais comme vous vous en doutez, ce n’est pas la bonne façon si vous voulez rendre votre application publique, accessible à tout le monde sur internet.

La façon la plus simple de rendre votre application publique est d’utiliser le service http://shinyapps.io. Ce service permet d’héberger gratuitement jusqu’à 5 applications par utilisateur, jusqu’à concurrence de 25 heures d’utilisation par mois. Ils offrent aussi des forfaits payants, allant de 9$ par mois à 3300 $ par année, selon le nombre d’applications et le nombre d’heures qu’elles seront utilisées.

Pour utiliser le service shinyapps.io, le plus facile est de démarrer un nouveau projet dans RStudio de type Shiny Web Application et de placer le code de votre application dans un fichier nommé app.R. Une fois votre application prête, vous devrez légèrement la modifier avant de la publier, en enlevant la ligne de runApp de votre code. Le serveur de shinyapps.io s’occupera lui-même de la lancer quand un utilisateur l’appellera. Il s’occupera aussi de l’arrêter après 15 minutes d’inactivité. En fait, lorsque vous fonctionnez de cette façon, vous n’avez pas besoin non plus de lancer manuellement la commande runApp, puisque RStudio vous fournit un petit bouton nommé “Run App” que vous pouvez cliquer pour la démarrer sur votre ordi.

Pour lancer la mise en ligne, utilisez le menu File… Publish…

RStudio vous demandera alors d’aller chercher votre “token” prouvant votre identité sur le site de shinyapps.io. Vous copiez-coller cette valeur et vous être prêt à rendre votre application publique. L’opération peut prendre quelques minutes parce que shinyapps.io doit, entre autres, installer sur le serveur la liste de tous les packages que vous avez utilisés dans votre application.

Une fois l’opération complétée, votre application s’ouvrira automatiquement dans votre navigateur web. Mon application de quiz par exemple se trouve à l’adresse : https://charlesmartin.shinyapps.io/QuizCorrelation/. Si jamais vous perdez l’adresse de votre application, vous avez accès à la liste de toutes vos applications dans votre compte sur shinyapps.io

Si jamais vous voulez mettre votre application à jour, vous n’avez qu’à refaire la publication avec File… Publish… L’ancienne version sera effacée et remplacée par la nouvelle.

Si jamais vous avez un background plus technique, vous pouvez aussi installer la version libre de droits de Shiny Server (https://rstudio.com/products/shiny/shiny-server/) sur une machine Linux que vous aurez préalablement configurée.

Personnalisation de l’interface

Enfin, dernier item à l’ordre du jour: comment personnaliser l’organisation de votre interface Shiny.

Nous en avons vu une première, à l’aide d’un panneau latéral (sidebarPanel) et d’un panneau principal (mainPanel), comme ceci :

interface_avec_panneau <- fluidPage(

  titlePanel("Titre"),

  sidebarLayout(
    sidebarPanel(
      h4("Panneau latéral")
    ),
    mainPanel(
      h4("Panneau principal")
    )
  )
)
serveur_vide <- function(input, output) {
}
appli <- shinyApp(ui = interface_avec_panneau, server = serveur_vide)
runApp(appli)

On peut aussi utiliser une organisation en grille, où chaque ligne est définie par une fonction fluidRow et chaque colonne par une fonction column. Pour personnaliser la largeur des cellules, gardez en tête que le code est prévu pour contenir au total 12 unités. Par exemple pour faire un layout 2/3 1/3, il faut choisir 8 et 4 comme largeurs de colonnes :

interface_grille <- fluidPage(

  titlePanel("Titre"),

  fluidRow(
    column (8,h4("Colonne qui prend le 2/3")),
    column (4,h4("Colonne qui prend le 1/3"))
  ),
  fluidRow(
    column (6,h4("Moitié")),
    column (6,h4("Moitié"))
  )
)
appli <- shinyApp(ui = interface_grille, server = serveur_vide)
runApp(appli)

On peut aussi séparer l’interface en plusieurs onglets, avec la fonction tabsetPanel et utiliser tabPanel pour définir le contenu de chaque onglet :

interface_onglets <- fluidPage(

  titlePanel("Titre"),

  tabsetPanel(
    tabPanel(
      "Titre de l'onglet",
      h4("Et son contenu")
    ),
    tabPanel(
      "Titre de l'onglet 2",
      h4("Et le contenu du deuxième onglet")
    )
  )

)
appli <- shinyApp(ui = interface_onglets, server = serveur_vide)
runApp(appli)

Remarquez que toutes ces structures peuvent être combinées, pour par exemple mettre une grille dans un onglet, etc. Notez aussi qu’il existe plusieurs autres façons d’organiser une interface, qui peuvent être trouvées ici : https://shiny.rstudio.com/articles/layout-guide.html

Enfin, sachez qu’au final, le travail de Shiny est de générer une interface en HTML que votre navigateur Web pourrait interpréter. En ce sens, vous avez accès, si vous connaissez un peu le HTML, à insérer à n’importe quel endroit de votre interface un morceau de HTML qui sera envoyé directement, un peu comme ceci :

interface_html <- fluidPage(

  titlePanel("Titre"),

  tabsetPanel(
    tabPanel(
      "Titre de l'onglet",
      HTML("Voici du contenu qui passe directement <strong>En gras</strong>, etc.")
    ),
    tabPanel(
      "Titre de l'onglet 2",
      h4("Et le contenu du deuxième onglet")
    )
  )

)
appli <- shinyApp(ui = interface_html, server = serveur_vide)
runApp(appli)

Vous comprenez qu’à ce moment, si vous ou quelqu’un dans votre entourage connaissez le HTML, le CSS ou le Javascript, les possibilités sont infinies pour personnaliser votre interface…

Ressources

En terminant, voici quelques ressources pour guider vos premiers pas avec Shiny :