Création d’un plugin simple permettant d’afficher les données sous forme d’histogramme
Introduction
Commençons par la création d'un plugin simple.
Nous allons :
Définir les spécifications du plugin : que voulons-nous que notre plugin propose aux utilisateurs ?
Créer le plugin et comprendre sa structure en fichiers
Créer l'interface utilisateur
Développer la logique côté serveur
Créer les traductions pour permettre son utilisation en plusieurs langues
Voir comment partager le plugin avec la communauté des utilisateurs
Spécifications du plugin
Nous allons créer une interface graphique qui permettra de visualiser la distribution d’une variable sous la 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 table OMOP measurement.
Lorsque nous allons changer la valeur du menu déroulant 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é interface utilisateur (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 table measurement
Modifier le nombre de barres de l’histogramme en fonction de la valeur renseignée dans le menu déroulant
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 de développement.
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érales 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 (l’interface utilisateur) et le backend (la logique côté serveur) de notre plugin (cf les 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 blocs (div) avec border:dashed 1px; afin de visualiser nos blocs, 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 (ou utilisez le raccourci CMD/CTRL + SHIFT + ENTER).
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 avec renderPlot 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 bloc de droite.
Nous avons dit plus haut que nous voulions trois éléments :
un menu déroulant pour choisir la variable à afficher
un champ numérique pour choisir le nombre de barres de l’histogramme à afficher
un bouton pour afficher la figure avec les paramètres choisis
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 dropdowndiv(strong(i18np$t("concept")),style="margin-bottom:10px;"),# Menu déroulant avec les conceptsdiv(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")),style="width:300px;"),br(),# Champ numérique pour choisir le nombre de barres de l'histogramme# Avec une valeur à 50, un minimum à 10 et un maximum à 100div(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 figureshiny.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.
Voici le code complet pour l’interface utilisateur :
tagList(div(div(# Chaque id est dans une fonction ns, et comporte une balise %widget_id%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 dropdowndiv(strong(i18np$t("concept")),style="margin-bottom:10px;"),# Menu déroulant avec les conceptsdiv(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")),style="width:300px;"),br(),# Champ numérique pour choisir le nombre de barres de l'histogramme# Avec une valeur à 50, un minimum à 10 et un maximum à 100div(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 figureshiny.fluent::PrimaryButton.shinyInput(ns("show_plot_%widget_id%"),i18np$t("show_plot")),style="margin: 10px 5px; width:50%;"),style="display: flex; height: 100%;",# Permet d'afficher côte à côte les deux div ci-dessus))
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 !
Logique côté 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 table measurement)
vocabulary_id : le nom de la terminologie correspondant au concept
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ébuggage. 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 convertit les concepts sous forme de listeconcepts<-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 observe_event, qui est une version modifiée de la fonction observeEvent de Shiny, à la différence que cette fonction empêche l’application de crasher en cas d’erreur dans le code de l’observer, et ajoute les messages d’erreurs dans le log de l’application.
Cette fonction déclenchera le code après avoir détecté un événement.
observe_event(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
Mettre à jour notre output
observe_event(input$show_plot_%widget_id%,{# 1) Récupérer le concept sélectionné dans le menu déroulantselected_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 videdata<-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'histogrammeplot<-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 Yggplot2::labs(x=selected_concept$concept_name,y=i18np$t("occurrences"))no_data_available<-FALSE}}# Graphique vide si pas de données disponiblesif(no_data_available){plot<-ggplot2::ggplot()+ggplot2::theme_void()+ggplot2::labs(title=i18np$t("no_data_available"))}# 5) Mise à jour de l'outputoutput$plot_%widget_id%<-renderPlot(plot)})
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(# Chaque id est dans une fonction ns, et comporte une balise %widget_id%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 dropdowndiv(strong(i18np$t("concept")),style="margin-bottom:10px;"),# Menu déroulant avec les conceptsdiv(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")),style="width:300px;"),br(),# Champ numérique pour choisir le nombre de barres de l'histogramme# Avec une valeur à 50, un minimum à 10 et un maximum à 100div(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 figureshiny.fluent::PrimaryButton.shinyInput(ns("show_plot_%widget_id%"),i18np$t("show_plot")),style="margin: 10px 5px; width:50%;"),style="display: flex; height: 100%;",# Permet d'afficher côte à côte les deux div ci-dessus))
# 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 listeconcepts<-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))# Code qui sera exécuté lorsque l'utilisateur appuiera sur l'input show_plot_%widget_id%observe_event(input$show_plot_%widget_id%,{# 1) Récupérer le concept sélectionné dans le menu déroulantselected_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 videdata<-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'histogrammeplot<-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 Yggplot2::labs(x=selected_concept$concept_name,y=i18np$t("occurrences"))no_data_available<-FALSE}}# Graphique vide si pas de données disponiblesif(no_data_available){plot<-ggplot2::ggplot()+ggplot2::theme_void()+ggplot2::labs(title=i18np$t("no_data_available"))}# 5) Mise à jour de l'outputoutput$plot_%widget_id%<-renderPlot(plot)})
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
Ce n’est pas simple et cela demande de savoir manipuler la librairie Shiny. Pour en apprendre plus sur Shiny, nous vous conseillons le très bon livre Mastering Shiny.
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.
Conclusion
Vous venez de créer votre premier plugin ! Vous pouvez dès lors l'utiliser dans un projet et surtout l'améliorer.
L'avantage avec les plugins est que tout ce qui faisable en R ou Python est intégrable dans LinkR sous forme de plugin !
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.