Modules Shiny (part 2) :
Transfert de reactive entre l’application et ses modules

Quentin Fazilleau

2019/04/26


Dans l’article précédent nous avons vu pourquoi les modules sont d’une grande aide dans la création d’applications Shiny. Nous avons également vu un exemple minimal “Hello-World”.

Cela peut parfois être difficile de partager des reactives entre l’application Shiny elle-même et ses modules. Dans cet article nous décrirons les trois cas les plus communs d’échange de données :

  • Module → Application
  • Application → Module
  • Application ↔ Module

Si vous souhaitez jouer les exemples en local

Tous les codes utilisés ici sont disponibles sur Github ardata-fr/Shiny-Modules-Tutorials. Ce dépôt est un package R dont les modules sont les fonctions exportées. Les applications Shiny sont dans le dossier inst.

Installer le package et lancer localement les applications :

# install.packages("remotes")
remotes::install_github("ardata-fr/Shiny-Modules-Tutorials")
library(shinyModulesTuto)
# Lister les applications disponibles
listEx()
# Lancer la 1ere application
runEx(listEx()[1])

La donnée du module vers l’application

Voir l’application exemple en ligne ou bien lancer la commande :

# Lancer localement l'application exemple
runEx("module-vs-app")

Dans le module

On initialise la reactiveValues que l’on va retourner :

toReturn <- reactiveValues(
                variables = NULL,
                variable_name = NULL,
                trigger = 0
            )

Puis on la met à jour reactiveValues :

observeEvent(input$AB_load, {
    toReturn$variable       <- get(input$SI_dataset)[,input$SI_var]
    toReturn$variable_name  <- input$SI_var
    toReturn$trigger        <- toReturn$trigger + 1
})

Pour finir elle est retournée à l’application :

return(toReturn)

Dans l’application

On récupère les résultats du module :

# results est une reactiveValues avec 3 slots (defini dans le module)
results <- callModule(module = load_data, id = "id1")

On peut ensuite l’utiliser comme une reactiveValues classique :

output$PR_results_print <- renderPrint({
    print(results$variable_name)
    print(results$variable)
})

La donnée de l’application vers le module

Voir l’application exemple en ligne ou bien lancer la commande :

# Lancer localement l'application exemple
runEx("app-vs-module")

Dans l’application

Solution 1 : Utiliser une reactive

On crée la reactive que l’on donnera ensuite au module :

variable <- reactive({
    iris[, input$SI_colname]
})

On passe la reactive comme un paramètre du module :

callModule(module = show_data, id = "id1",
    variable = variable,
    variable_name = reactive(input$SI_colname))

NB : variable étant une reactive, pas besoin de l’encapsuler dans la fonction reactive().

Solution 2 : Utiliser une reactiveValues

On crée la reactiveValues que l’on donnera ensuite au module :

rv <- reactiveValues(variable = NULL)

observe({
    rv$variable <- iris[, input$SI_colname]
})

On passe la reactiveValues comme un paramètre du module :

callModule(module = show_data, id = "id2",
    variable = reactive(rv$variable),
    variable_name = reactive(input$SI_colname))

NB : Comme rv$variable est une reactiveValues, on l’encapsule dans la fonction reactive().

Dans le module

Solutions 1 & 2

Pour les deux solutions (avec une reactive ou une reactiveValues dans l’application), on récupère les paramètres depuis le module avec :

output$PL_histogram_var <- renderPlot({
    hist(variable(), main = variable_name(), xlab = NULL)
})

NB : On utilise ici les paramètres du module variable et variable_name comme des reactives classiques (cf variable() & variable_name()).

Donnée de l’application modifiée dans un module

Voir l’application exemple en ligne ou bien lancer la commande :

# Lancer localement l'application exemple
runEx("app-pong-module")

L’objectif est d’utiliser un module pour modifier une donnée présente dans l’application. L’astuce ici est d’utiliser une deuxième reactiveValues.

Comme nous avons décrit dans le chapitre précédent comment utiliser les paramètres dans un module, dans cette partie on se concentre uniquement sur l’application.
Considérons le module apply_function qui prend en paramètre un vecteur numérique sur lequel l’utilisateur peut appliquer une fonction (ex : log(x)). Ensuite, le module retourne une reactiveValues contenant plusieurs slots : le vecteur numérique résultant de cette fonction, le nom de la fonction appliquée et un “trigger” qui s’incrémente dès lors qu’une nouvelle fonction est appliquée.

Dans l’application, on initialise la reactiveValues comme d’habitude :

rv <- reactiveValues(variable = NULL, fun_historic = NULL)

observe({
    rv$variable     <- iris[, input$SI_colname]
    rv$fun_historic <- NULL
})

Que l’on passe ensuite au module apply_function :

modified_data <- callModule(module = apply_function, id = "id1",
                     variable = reactive(rv$variable))

La sortie du module est une reactiveValues avec les 3 slots suivants :

  • result (le vecteur numérique résultant de la fonction)
  • fun (nom de la fonction appliquée)
  • trigger (integer qui s’incrémente dès qu’une nouvelle fonction est appliquée)

Dans l’application, pour mettre à jour la reactiveValues rv$variable en fonction du résultat obtenu (modified_data$result), on utilise un observeEvent basé sur modified_data$trigger.

observeEvent(modified_data$trigger, {
    rv$variable     <- modified_data$result
    rv$fun_historic <- c(rv$fun_historic, modified_data$transformation)
})

L’astuce ici consistait à utiliser une reactiveValues “tampon”. Le vrai schéma devrait donc être :

Application avec l’ensemble des exemples précédents

Voir l’application exemple en ligne ou bien lancer la commande :

# Lancer localement l'application exemple
runEx("whole-app")

Cette application utilise l’ensemble des modules des exemples précédents. De plus, le module apply_scale ajoute une deuxième façon de modifier la reactiveValues “principale” rv (applique la fonction scale(x)).
Le schéma ci-dessous montre que la reactiveValues rv est le coeur de l’application. Elle contient deux slots, variable qui est le vecteur numérique ainsi que fun_history qui est l’ensemble des fonctions qui ont été appliquées.

La reactiveValues rv est initialisée dans l’application comme ci-dessous :

rv <- reactiveValues(variable = NULL, fun_history = NULL)

Puis elle est mise à jour par trois modules différents :

  • Par le module load_data en utilisant la reactiveValues tampon data_mod1.
  • Par le module apply_function en utilisant la reactiveValues tampon data_mod2.
  • Par le module apply_scale en utilisant la reactiveValues tampon data_mod3.

Pour faciliter le processus de mise à jour de la reactiveValues rv, on place des observeEvent sur les slots trigger retournés pour les modules. Ainsi on s’assure que la dernière action de l’utilisateur est bien prise en compte.

Déclenchement de la mise à jour à partir des résultats du module load_data :

# Appel du module load_data
data_mod1 <- callModule(module = load_data, id = "mod1")

# Lorsque data_mod1$trigger change, mise à jour de rv$variable & rv$fun_history
observeEvent(data_mod1$trigger, {
    req(data_mod1$trigger>0)
    rv$variable    <- data_mod1$variable
    rv$fun_history <- c()
})

Déclenchement de la mise à jour à partir des résultats du module apply_function :

# Appel du module apply_function avec le parametre rv$variable
data_mod2 <- callModule(module = apply_function, id = "mod2",
                 variable = reactive(rv$variable))

# Lorsque data_mod2$trigger change, mise à jour de rv$variable & rv$fun_history
observeEvent(data_mod2$trigger, {
    req(data_mod2$trigger>0)
    rv$variable    <- data_mod2$result
    rv$fun_history <- c(rv$fun_history, data_mod2$fun)
})

Déclenchement de la mise à jour à partir des résultats du module apply_scale :

# Appel du module apply_scale avec le parametre rv$variable
data_mod3 <- callModule(module = apply_scale, id = "mod3",
                 variable = reactive(rv$variable))

# Lorsque data_mod3$trigger change, mise à jour de rv$variable & rv$fun_history
observeEvent(data_mod3$trigger, {
    req(data_mod3$trigger>0)
    rv$variable    <- data_mod3$result
    rv$fun_history <- c(rv$fun_history, "scale")
})

Conclusion

Nous avons décrit les cas ci-dessous (lien vers les applications) :

Nous vous recommandons vivement d’utiliser les modules lors du développement d’applications Shiny conséquentes. Vous pouvez aller encore plus loin avec :

  • Les modules imbriqués (très simple en réalité)
  • L’appel dynamique de modules

Avant de voir ces concepts (il me reste une partie 3 à écrire), j’espère que vous allez vous amuser à créer vos applications Shiny !