Important
Ajoutez toujours la balise %req%
au début d'un observeEvent
.
Cette balise sera remplacée par un code qui invalidera les anciens observers quand le widget sera mis à jour.
Création d'un plugin simple
Spécifications du plugin
Nous allons créer une interface graphique qui permettra de visualiser la distribution d’une variable, sous forme d’un histogramme.
Nous devons faire un premier choix : s’agit-il d’un plugin de données individuelles (patient par patient) ou agrégées (sur un groupe de patients) ?
Il est plus fréquent de vouloir visualiser la distribution d’une variable sur un groupe de patients plutôt que sur un patient seul, nous allons donc créer un plugin de données agrégées.
Ensuite, à quoi devra ressembler notre interface graphique ?
Nous allons séparer l’écran en deux, à gauche nous visualiserons notre histogramme, et à droite nous pourrons régler les paramètres de la figure, avec un menu déroulant pour choisir la variable et un champ pour choisir le nombre de barres sur mon histogramme.
Côté serveur maintenant.
Un histogramme n’est pas adapté pour visualiser tout type de données : nous pourrons visualiser la distribution de données numériques, et de données catégorielles à condition que le nombre de catégories ne soit pas trop important.
Pour simplifier, nous n’allons autoriser que l’affichage des données numériques. Nous allons donc restreindre l’affichage à la variable d$measurement. Consultez la documentation sur la console pour plus d’informations sur les variables de données OMOP.
Lorsque nous allons changer la variable du nombre de barres de l’histogramme, les modifications devront être prises en compte après validation, pour ne pas réaliser de calculs inutiles. Nous devrons également donner des bornes de valeurs possibles.
Résumons les spécifications de notre plugin :
- Côté UI :
- Visualisation de l’histogramme à gauche de l’écran
- Paramètres à droite de l’écran
- Variable à afficher
- Nombre de barres composant l’histogramme, avec des bornes inférieure et supérieure
- Validation des modifications
- Côté serveur :
- N’autoriser que les données de la variable d$measurement
- Modifier le nombre de barres de l’histogramme en fonction de la valeur renseignée
- Lancer le code de la figure une fois le bouton de validation cliqué
Création du plugin
Rendez-vous sur la page des plugins depuis le menu en haut de l’écran.
Pour créer un plugin, cliquez sur l’icône “+” à gauche de l’écran.
Choisissez un nom, par exemple “Histogramme”.
Choisissez le type de données concernées : le plugin peut être utilisé sur des données individuelles (patient par patient), agrégées (un groupe de patients), ou sur les deux. Pour notre exemple, nous allons choisir “Données agrégées”.
Il est également possible de copier un plugin existant : nous le verrons dans la prochaine section, lorsque nous créerons un plugin à partir du template d’InterHop.
Une fois le plugin créé, sélectionnez-le. Vous arriverez sur la page de sommaire du plugin.
Vous voyez en haut à droite qu’un plugin est divisé en quatre onglets :
- Sommaire : ici seront affichées les informations générale et la description de votre plugin. Nous détaillons cela dans le dernier paragraphe : “Partager le plugin”.
- Code : c’est ici que nous éditerons les scripts permettant de créer le frontend et le backend de notre plugin (cf trois paragraphes suivants).
- Test : cet onglet permet de tester le code du plugin avec des données
- Partage : c’est ici que l’on pourra ajouter ce plugin à notre dépôt Git pour le partager avec le reste de la communauté
Structure d’un plugin
Rendez-vous sur l’onglet Code.
Un plugin est par défaut composé de ces trois fichiers :
- ui.R : contient le code Shiny de l’interface utilisateur, que nous allons détailler dans le prochain paragraphe
- server.R : contient le backend de l’application, que nous allons détailler dans le paragraphe “Serveur / backend”
- translations.csv : contient les traductions pour le frontend et le backend
UI - interface utilisateur / frontend
Comme nous l’avons vu sur le schéma plus haut, nous voulons séparer l’écran du plugin en deux, avec à gauche la figure et à droite les paramètres de la figure.
Commencez par cliquer sur le fichier ui.R
à gauche de l’écran.
Tout notre code de l’interface utilisateur devra se trouver dans une fonction tagList
, qui permet de mettre bout à bout des balises HTML avec la librairie R Shiny.
Pour que deux div
soient côte à côte, il faut qu’ils soient eux-mêmes dans un div
avec l’attribut style = "display:flex;"
.
tagList(
div(
div(
# Chaque id est dans une fonction ns, et comporte une balise %widget_id%
id = ns("split_layout_left_%widget_id%"),
style = "margin:10px 5px; width:50%; border:dashed 1px;"
),
div(
id = ns("split_layout_right_%widget_id%"),
style = "margin:10px 5px; width:50%; border:dashed 1px;"
),
style = "display:flex; height: 100%;", # Permet d'afficher côte à côte les deux div ci-dessus
)
)
Notez que dès que l’on attribue un ID à un élément HTML, il doit comporter une balise %widget_id%
, qui sera remplacée par l’ID du widget, permettant d’avoir des ID uniques. En effet, cela poserait problème si le même plugin était lancé dans deux widgets différents. En cas d’ID multiples, la page HTML ne s’affiche pas.
De plus, chaque ID est encapsulé dans une fonction ns
(voir le chapitre sur les modules Shiny du livre Mastering Shiny pour plus d’informations).
Nous avons ici ajouté des bordures à nos div
avec border:dashed 1px;
afin de visualiser nos div, qui sont pour le moment vides. Nous retirerons ces attributs plus tard.
Cliquez sur l’icône “Exécuter le plugin” à gauche de l’écran.
Vous serez automatiquement basculés sur l’onglet “Test” et vous devriez obtenir ce résultat.
Nous voyons bien les deux blocs div
côte à côte, avec une bordure en pointillés.
Ajoutons maintenant notre histogramme.
Nous utilisons pour cela la fonction plotOutput
, que nous modifierons côté serveur pour afficher notre plot.
div(
id = ns("split_layout_left_%widget_id%"),
plotOutput(ns("plot_%widget_id%")), # Toujours mettre les ID dans des ns() avec un attribut %widget_id%
style = "margin:10px 5px; width:50%; border:dashed 1px;"
)
Créons maintenant la configuration de notre figure, dans le div
de droite.
Nous avons dit plus haut que nous voulions trois éléments :
- un menu déroulant pour choisir la variable à afficher
- un input numérique pour choisir le nombre de barres de l’histogramme à afficher
- un bouton pour afficher la figure avec ces paramètres
Nous allons utiliser la librairie shiny.fluent
, qui est celle utilisée pour toute l’interface utilisateur de LinkR, qui utilise Fluent UI.
Voici les fonctions à utiliser pour nos trois éléments :
Dans les plugins, il faut préfixer toutes les fonctions du nom de la librairie. Par exemple : shiny.fluent::Dropdown.shinyInput()
.
Créons le code pour afficher les éléments de configuration de la figure.
div(
# id avec ns et %widget_id%
id = ns("split_layout_right_%widget_id%"),
# div contenant le titre, en gras (strong), avec un espace de 10 px entre le titre et le dropdown
div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
# Menu déroulant avec les concepts
div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),
# Input numérique pour choisir le nombre de barres de l'histogramme
# Avec une valeur à 50, un minimum à 10 et un maximum à 100
div(strong(i18np$t("num_bins")), style = "margin-bottom:10px;"),
div(shiny.fluent::SpinButton.shinyInput(ns("num_bins_%widget_id%"), value = 50, min = 10, max = 100), style = "width:300px;"), br(),
# Bouton pour afficher la figure
shiny.fluent::PrimaryButton.shinyInput(ns("show_plot_%widget_id%"), i18np$t("show_plot")),
style = "margin: 10px 5px; width:50%;"
)
Cliquez de nouveau sur “Exécuter le plugin”, vous devriez obtenir ceci.
Vous pouvez remarquer que les titres des inputs ont été mis dans une fonction i18np$t
. Ceci permet de traduire les éléments à partir du fichier de traductions (translations.csv
), ce que nous allons voir dans le prochaine paragraphe.
Traductions
Les traductions sont à insérer dans le fichier CSV translations.csv
.
Il comporte les colonnes suivantes :
- base : c’est le mot clef que vous allez insérer dans votre code et qui sera traduit selon la langue sélectionnée
- en : c’est la traduction du mot en anglais
- fr : traduction en français. Pour l’instant, seuls l’anglais et le français ont été développés. Il sera possible d’ajouter d’autres langues à terme.
Cliquez sur le fichier translations.csv
, puis complétez-le avec les traductions suivantes.
base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure
Relancez le code. Vous devriez obtenir ceci.
Les mots clefs ont été remplacés par leur traduction française.
Nous allons maintenant rendre tout ceci dynamique en codant le backend !
Serveur / backend
Sans le backend, l’interface graphique est statique, rien ne se passe quand je clique sur les boutons.
Comme nous l’avons vu dans la page de documentation pour créer des widgets, lorsque nous créons un widget, nous sélectionnons le plugin à utiliser ainsi que les concepts.
Les concepts sélectionnés se retrouveront dans la variable selected_concepts
, qui comporte les colonnes suivantes :
- concept_id : l’ID du concept, soit standard (à retrouver sur Athena), soit non standard (dans ce cas, supérieur à 2000000000 / 2B)
- concept_name : le nom du concept
- domain_id : le nom du
Domaine
OMOP, qui correspond souvent à la table OMOP (domaine ‘Measurement’ pour la variabled$measurement
) - vocabulary_id : le nom de la terminologie correspondant au concept
- mapped_to_concept_id & merge_mapped_concepts, non utilisés dans la version actuelle de LinkR
Pour pouvoir tester un plugin et faire que le backend fonctionne, il faut charger un projet contenant des donneés. Lancez par exemple le projet utilisé pour la mise en place.
Ensuite, pour simuler la création d’un widget, nous allons sélectionner des concepts pour tester notre plugin.
Vous avez à gauche de l’écran un bouton “Sélectionner des concepts”.
Ceci ouvrira le même menu que lorsque vous choisissez des concepts en créant un widget.
Sélectionnez pour l’exemple le concept Heart rate
de la terminologie LOINC, puis cliquez sur “Valider”.
Faisons le test : ouvrez le fichier server.R
, et copiez ce code :
print(selected_concepts)
Relancez le code du plugin, vous devriez obtenir ceci.
Vous voyez l’output du backend apparaître en bas de l’écran. Ceci n’est le cas qu’avec le test des plugins, ce qui permet de faciliter le débogage. Cet output est masqué lorsque les plugins sont utilisés dans des projets.
On voit donc bien le concept Heart rate
apparaître avec son concept_id
.
Nous allons maintenant écrire le code pour mettre à jour le menu déroulant des concepts.
# Ajout d'une ligne avec les valeurs 0 / "none"
concepts <-
tibble::tibble(concept_id = 0L, concept_name = i18np$t("none")) %>%
dplyr::bind_rows(selected_concepts %>% dplyr::select(concept_id, concept_name))
# On converti les concepts sous forme de liste
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")
# On instaure un délai, afin que le dropdown se mette à jour après avoir été créé
shinyjs::delay(500, shiny.fluent::updateDropdown.shinyInput(session, "concept_%widget_id%", options = concepts, value = 0L))
Mettez à jour translations.csv
pour ajouter la traduction de none
.
base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure
none,None,Aucun
Plusieurs choses à noter.
On ajoute une ligne avec un concept vide, ’none’, ce qui permettra d’éviter les erreurs si le menu déroulant est vide.
On utilise la fonction convert_tibble_to_list
, qui permet de convertir un tibble en liste, nécessaire pour être intégré dans un input de shiny.fluent
. Les arguments seront key_col
pour la colonne qui contient le code du concept (‘concept_id’), et text_col
pour la colonne qui contient le texte (‘concept_name’).
On ajoute un délai d’exécution de l’update, avec shinyjs::delay()
, qui est de 500 ms. Ceci permet de s’assurer que le dropdown a été créé dans l’UI avant de le mettre à jour.
Exécutez ce code, vous devriez maintenant avoir un menu déroulant avec les concepts que nous avons sélectionnés (en l’occurrence Heart rate
).
Il ne nous reste plus qu’à afficher notre figure.
Nous allons utiliser la fonction observeEvent, qui déclenchera le code après avoir détecté un événement.
observeEvent(input$show_plot_%widget_id%, {
# Le code dans cette fonction sera exécuté
# chaque fois que je cliquerai sur le bouton avec l'id 'show_plot_%widget_id%'
# (donc le bouton "Afficher la figure")
})
Dans le cas de l’édition de plugins, à chaque fois que vous cliquez sur “Exécuter le plugin”, les observers créés précédemment seront invalidés, ce qui évite d’avoir des conflits.
Voici les étapes de notre code :
-
- Récupérer le concept sélectionné dans le menu déroulant
-
- S’assurer que le concept appartient à un domaine qui peut s’afficher sous forme d’histogramme. Par simplicité, nous sélectionnerons uniquement le domaine ‘Measurement’.
-
- S’assurer que le tibble des données, filtré avec le concept sélectionné, n’est pas vide
-
- Créer le code de notre histogramme avec
ggplot
- Créer le code de notre histogramme avec
-
- Mettre à jour notre output
observeEvent(input$show_plot_%widget_id%, {
# Toujours mettre cette balise au début d'un observer
%req%
# Protéger le code en cas d'erreur avec un tryCatch
tryCatch({
# 1) Récupérer le concept sélectionné dans le menu déroulant
selected_concept <-
selected_concepts %>%
dplyr::filter(concept_id == input$concept_%widget_id%)
no_data_available <- TRUE
# 2) Y-a-t-il bien un concept de sélectionné et le domain_id est-il égal à 'Measurement' ?
if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){
# 3) S'assurer que le tibble des données filtré sur ce concept n'est pas vide
data <-
d$measurement %>%
dplyr::filter(measurement_concept_id == selected_concept$concept_id)
if(data %>% dplyr::count() %>% dplyr::pull() > 0){
# 4) Créer le code de l'histogramme
plot <-
data %>%
ggplot2::ggplot(ggplot2::aes(x = value_as_number)) +
# On prend en compte le nombre de barres depuis notre variable input$num_bins_%widget_id%
ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
ggplot2::theme_minimal() +
# On modifie les titres des axes X et Y
ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))
no_data_available <- FALSE
}
}
# Graphique vide si pas de données disponibles
if (no_data_available){
plot <-
ggplot2::ggplot() +
ggplot2::theme_void() +
ggplot2::labs(title = i18np$t("no_data_available"))
}
# 5) Mise à jour de l'output
output$plot_%widget_id% <- renderPlot(plot)
# Le message d'erreur s'affichera dans la console R
}, error = function(e) cat(paste0("\n", now(), " - ", toString(e))))
})
Mettez à jour les traductions.
base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure
none,None,Aucun
occurrences,Occurrences,Occurences
no_data_available,No data available,Pas de données disponibles
Vous devriez avoir ceci.
Nous visualisons la distribution de la fréquence cardiaque chez tous les patients, depuis la variable d$measurement
.
Voici les trois fichiers complets :
tagList(
div(
div(
id = ns("split_layout_left_%widget_id%"),
plotOutput(ns("plot_%widget_id%")),
style = "margin:10px 5px; width:50%; border:dashed 1px;"
),
div(
# id avec ns et %widget_id%
id = ns("split_layout_right_%widget_id%"),
# div contenant le titre, en gras (strong), avec un espace de 10 px entre le titre et le dropdown
div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
# Menu déroulant avec les concepts
div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),
# Input numérique pour choisir le nombre de barres de l'histogramme
# Avec une valeur à 50, un minimum à 10 et un maximum à 100
div(strong(i18np$t("num_bins")), style = "margin-bottom:10px;"),
div(shiny.fluent::SpinButton.shinyInput(ns("num_bins_%widget_id%"), value = 50, min = 10, max = 100), style = "width:300px;"), br(),
# Bouton pour afficher la figure
shiny.fluent::PrimaryButton.shinyInput(ns("show_plot_%widget_id%"), i18np$t("show_plot")),
style = "margin: 10px 5px; width:50%;"
),
style = "display:flex; height: 100%;",
)
)
# Ajout d'une ligne avec les valeurs 0 / "none"
concepts <-
tibble::tibble(concept_id = 0L, concept_name = i18np$t("none")) %>%
dplyr::bind_rows(selected_concepts %>% dplyr::select(concept_id, concept_name))
# On convertit les concepts sous forme de liste
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")
# On instaure un délai, afin que le dropdown se mette à jour après avoir été créé
shinyjs::delay(500, shiny.fluent::updateDropdown.shinyInput(session, "concept_%widget_id%", options = concepts, value = 0L))
observeEvent(input$show_plot_%widget_id%, {
# Toujours mettre cette balise au début d'un observer
%req%
# Protéger le code en cas d'erreur avec un tryCatch
tryCatch({
# 1) Récupérer le concept sélectionné dans le menu déroulant
selected_concept <-
selected_concepts %>%
dplyr::filter(concept_id == input$concept_%widget_id%)
no_data_available <- TRUE
# 2) Le domain_id est-il égal à 'Measurement' ?
if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){
# 3) S'assurer que le tibble des données filtré sur ce concept n'est pas vide
data <-
d$measurement %>%
dplyr::filter(measurement_concept_id == selected_concept$concept_id)
if(data %>% dplyr::count() %>% dplyr::pull() > 0){
# 4) Créer le code de l'histogramme
plot <-
data %>%
ggplot2::ggplot(ggplot2::aes(x = value_as_number)) +
# On prend en compte le nombre de barres depuis notre variable input$num_bins_%widget_id%
ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
ggplot2::theme_minimal() +
# On modifie les titres des axes X et Y
ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))
no_data_available <- FALSE
}
}
# Graphique vide si pas de données disponibles
if (no_data_available){
plot <-
ggplot2::ggplot() +
ggplot2::theme_void() +
ggplot2::labs(title = i18np$t("no_data_available"))
}
# 5) Mise à jour de l'output
output$plot_%widget_id% <- renderPlot(plot)
# Le message d'erreur s'affichera dans la console R
}, error = function(e) cat(paste0("\n", now(), " - ", toString(e))))
})
base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure
none,None,Aucun
occurrences,Occurrences,Occurences
no_data_available,No data available,Pas de données disponibles
L'avantage avec les plugins est que tout ce qui faisable en R ou Python est intégrable dans LinkR sous forme de plugin.
Ce n’est pas simple et cela demande de savoir manipuler la librairie Shiny
. Pour en apprendre plus sur Shiny, je vous conseille le très bon livre Mastering Shiny.
Les plugins deviennent rapidement complexes, c’est pourquoi nous avons créé un template de développement pour avoir une base solide et commune pour coder des plugins plus complexes. C’est ce que nous allons voir dans le prochain chapitre.
Partager le plugin
Avant de partager le plugin, il est nécessaire de le documenter, afin que les utilisateurs sachent à quoi il sert et comment l’utiliser.
Pour cela, rendez vous sur la page de Sommaire du plugin. Vous voyez que les champs “Description courte” à gauche et “Description” à droite sont vides.
Cliquez sur le bouton “Editer les informations” à gauche de l’écran.
Vous pourrez alors modifier les informations relatives au plugin, notamment les auteurs ayant aidé dans sa conception et une description courte, qui s’affichera dans la page des plugins.
Nous pourrions par exemple donner cette description courte à notre plugin : “Un plugin permettant de visualiser les données structurées sous forme d’histogramme”.
Vous pouvez également éditer la Description complète en cliquant sur l’icône en haut à droite de l’écran.
Cela ouvrira un éditeur où vous pourrez écrire la description au format Markdown.
Une fois les modifications de la description validées, cliquez sur l’icône “Sauvegarder les modifications” à droite de l’écran.
Pour valider les modifications des informations du plugin, cliquez sur l’icône “Sauvegarder” à gauche de l’écran.
Maintenant que les informations de votre plugin sont bien renseignées, vous allez pouvoir le partager via l’onglet “Partage”, en haut à droite de l’écran, en suivant ce tutoriel.