1 - Installation

Comment installer LinkR, sur R ou depuis une image Docker

LinkR peut être installé et lancé depuis RStudio ou être lancé en tant que container Docker.

Installation sur RStudio

La librairie remotes sera nécessaire pour l’installation de LinkR. Vous pouvez l’installer avec cette commande :

install.packages("remotes")

Version stable

Installez la dernière version stable avec cette commande :

remotes::install_gitlab("interhop/linkr/linkr", host = "framagit.org")

Version en développement

Pour installer la dernière version en développement, ajouter @dev à la fin du lien du dépôt git.

remotes::install_gitlab("interhop/linkr/linkr@dev", host = "framagit.org")

Important - version de shiny.fluent

La version 0.3.0 de shiny.fluent est nécessaire


Vous devez également installer la version 0.3.0 de shiny.fluent.

Par défaut, c’est la version 0.4.0 qui est installée mais elle présente des bugs non résolus.

remotes::install_github('Appsilon/shiny.fluent', ref = 'dd1c956')

Démarrer LinkR

Pour lancer LinkR depuis RStudio ou depuis une console R, exécutez la fonction linkr.

linkr::linkr()

Vous pouvez choisir la langue en changeant l’argument language. Vous avez le choix parmi “fr” et “en”.

linkr::linkr(language = "fr")

Par défaut, les fichiers de LinkR seront sauvegardés dans votre dossier Home (que vous pouvez retrouver en exécutant path.expand("~")). Vous pouvez changer l’argument app_folder et choisir le dossier de votre choix.

linkr::linkr(language = "fr", app_folder = "/home/username")

Si vous voulez afficher le log dans la console, ajouter l’argument debug avec la valeur TRUE.

linkr::linkr(language = "fr", app_folder = "/home/username", debug = TRUE)

Ceci affichera tous les messages dans la console de R. Si vous préférez stocker ces messages dans un fichier de log accessible depuis LinkR, renseignez log_file à TRUE.

linkr::linkr(language = "fr", app_folder = "/home/username", debug = TRUE, log_file = TRUE)

A partir d’une image Docker

Si Docker est déjà installé

Copiez l’image Docker depuis Docker Hub.

docker pull interhop/linkr:latest

Lancez un container depuis cette image.

docker run -p 3838:3838 interhop/linkr:latest

Vous pouvez maintenant accéder à LinkR via l’adresse http://localhost:3838.

Vous pouvez également lancer LinkR en changeant les arguments de la fonction linkr (cf paragraphe précédent).

docker run \
    -p 3838:3838 \
    interhop/linkr:latest \
    R -e "linkr::linkr(language = 'fr', app_folder = '/root', debug = TRUE)"

Pour permettre au conteneur d’accéder à un dossier spécifique de votre système hôte (par exemple, /mon_dossier_perso/linkr), vous pouvez monter ce dossier dans le conteneur. Cela se fait avec l’option -v lors du lancement du conteneur.

docker run \
    -p 3838:3838 \
    -v /mon_dossier_perso/linkr:/root \
    interhop/linkr:latest \
    R -e "linkr::linkr(language = 'fr', app_folder = '/root', debug = TRUE)"

On a ici bien configuré l’argument app_folder de la fonction linkr pour sauvegarder les fichiers de l’application dans le dossier /root, qui sera en réalité le dossier de votre système que vous aurez indiqué avec l’option -v.

Installer Docker

Sur Windows ou macOS

  1. Téléchargez Docker Desktop
  2. Installez et lancez Docker Desktop en suivant les instructions
  3. Vérifiez que Docker fonctionne en ouvrant un terminal (PowerShell ou CMD sur Windows) et en exécutant :
docker --version

Sur Linux

  1. Installez Docker en fonction de votre distribution Linux.
sudo apt-get update
sudo apt-get install -y docker.io
sudo dnf install docker
sudo pacman -S docker
  1. Démarrez le service Docker.
sudo systemctl start docker
  1. Ajoutez votre utilisateur au groupe Docker pour éviter d’utiliser sudo (optionnel).
sudo usermod -aG docker $USER

Déconnectez-vous et reconnectez-vous pour appliquer les changements.

  1. Vérifiez que Docker fonctionne.
docker --version

2 - Mise en place

Un tutoriel permettant d’avoir rapidement un premier projet fonctionnel

Introduction

Dans ce tutoriel, nous allons voir comment :

  • importer des données
  • importer un projet
  • lancer ce projet

A la fin de ce tutoriel, vous aurez un tableau de bord vous permettant de visualiser les données de 100 patients.

Faut-il avoir des connaissances en programmation pour réaliser ce tutoriel ?


Pas besoin de connaissances avancées en programmation pour réaliser ce tutoriel !

La seule ligne de code à lancer est celle que vous voyez ci-dessous.

Il vous faudra en revanche installer RStudio.

Pour ce faire, vous pouvez consulter les tutoriels du site analyse-R :

RStudio est installé ? Allons-y, lançons LinkR !


Après avoir installé LinkR, lancez l’appli avec ce code.

linkr::linkr(language = "fr", app_folder = "/home/user_name")

Un onglet devrait s’ouvrir dans votre navigateur, avec la page d’accueil de LinkR.

Retournez sur la page d’installation pour plus d’informations sur les arguments que peut prendre la fonction.

Voyons maintenant comment importer des données.

Importer des données

Nous allons importer des données depuis le catalogue de contenu.

Depuis la page d’accueil ou depuis le menu en haut de la page, rendez-vous sur la page “Catalogue de contenus”.

Trouvez Saint-Malo sur la carte et sélectionnez InterHop.

Vous verrez à droite l’écran la page d’accueil du contenu partagé d’InterHop : on voit que sont proposés des plugins et des sets de données notamment.

Pour accéder au détail de ce contenu partagé, cliquez sur le bouton “Afficher le contenu” en bas de la page.

Nous voulons télécharger les données du set “MIMIC-IV demo set”.

Nous allons donc cliquez sur l’onglet “Sets de données”, en haut à droite de l’écran, puis cliquer sur le widget qui correspond à notre set de données.

Il ne reste plus qu’à cliquer sur “Installer”, et ça y est, le set de données (du moins son code) est installé !

Ce catalogue permet donc de partager des données ?


Non, aucune donnée n’est téléchargée lors de l’installation d’un set de données.

Les données de santé, qui sont des données sensibles, doivent être maniées dans un cadre réglementaire bien précis.

Ici, nous avons téléchargé le code qui permet d’accéder aux données, et non les données elles-mêmes.

Il se trouve que ce dans notre cas, ce sont des données anonymisées, et donc de fait non concernées par le RGPD. C’est pourquoi ce code peut, sans authentification quelconque, télécharger les données des 100 patients de la base test MIMIC-IV.

Vous pouvez lire cet article pour plus d’informations sur la base de données MIMIC.

Voyons à présent comment importer un projet.

Importer un projet

Nous allons procéder de la même façon pour importer un projet.

Rendez-vous sur la page “Catalogue de contenus”, sélectionnez cette fois-ci le laboratoire DOMASIA à Rennes.

Cliquez sur “Afficher le contenu”, vous devriez arriver sur l’onglet “Projets”, et vous devriez voir le projet “Dashboard indicateurs qualité”.

Lorsque je clique sur le projet, je vois sa description apparaître : ce projet permet de créer un tableau de bord présentant les indicateurs de qualité de la prise en charge de patients.

Cliquez sur “Installer”.

Ca y est, le projet est installé. Nous allons pouvoir le lancer.


Lancer le projet

Afin de lancer notre projet, nous devons associer le projet et le set de données.

Pour cela, rendez-vous sur la page “Projets”, depuis la première icône en partant de la gauche en haut de l’écran ou depuis la page d’accueil (à laquelle on accède en cliquant sur l’icône de LinkR en haut à gauche de l’écran).

Cliquez ensuite sur l’icône “Paramétrer le projet”, ceci permet de configurer le projet sans charger les données et les widgets.

Cliquez sur l’onglet “Set de données”, puis sélectionnez le set de donnée que nous avons précédemment installé : “MIMIC-IV set de démo”.

Nous avons associé le set de données au projet : ces données seront chargées au démarrage du projet.


Vous pouvez maintenant cliquez sur une des pages de données (les icônes avec les personnages).

Plusieurs choses se produisent.

  • Le code set de données, étant lancé pour la première fois, télécharge les fichiers CSV de la base de données, ceci peut prendre quelques minutes. Le chargement les fois suivantes sera plus rapide, étant donné que les fichiers seront stockés localement, ils ne devront pas être téléchargés de nouveau.

  • Un subset comprenant tous les patients va être créé.

  • Les widgets vont se charger et afficher les données

Enfin, cliquez sur l’icône des “Données agrégées” (celle avec plusieurs personnages), vous verrez alors apparaître le tableau de bord !

Les données de ce set (MIMIC-IV) étant anonymisées, les dates ont été modifiées (de 2110 à 2202), c’est pourquoi la figure des admissions en fonction du temps, en haut à droite, n’a pas un rendu correct.

Le tableau en bas à droite devrait afficher les diagnostics CIM-10. Il affiche ici des nombres, parce que nous n’avons pas importé la terminologie CIM-10 : la correspondance avec les noms correspondant aux codes CIM-10 ne peut se faire. Rendez-vous ici pour savoir comment importer des terminologies !

Conclusion

Nous avons vu dans ce tutoriel comment importer des données et importer un projet depuis le catalogue des contenus.

Comment créer un projet avec mes propres données ?


Pour importer vos données, suivez ce tutoriel.

Pour créer un projet à partir de zéro, suivez ce tutoriel.

3 - Composants

Une description des différents éléments composant LinkR

Introduction

LinkR s’articule autour de projets, dans lesquels des sets de données sont chargés, contenant des données au format OMOP et utilisant des terminologies standards.

Ces données peuvent être mises en qualité grâce à l’utilisation et au partage de scripts de data cleaning.

Dans ces projets, l’utilisateur peut visualiser et analyser les données à l’aide de widgets, qui sont des plugins paramétrés.

Il est également possible d’accéder à un environnement de développement R et Python via la console.


Sets de données

LinkR fonctionne avec OMOP, un modèle de données commun international pour les données de santé.

Vous pouvez importer des données depuis différentes sources : une base de données, des fichiers Parquet ou des fichiers CSV.

Un même set de données peut être utilisé dans plusieurs projets.

Pour l’instant il est nécessaire de coder en R le script d’import des données. Une interface graphique sera codée dans une prochaine version.

Voir comment importer des données.

Terminologies

Le modèle de données OMOP se base sur des terminologies standards, tells que :

  • LOINC pour la biologie
  • SNOMED pour les diagnostics
  • RxNorm pour les médicaments
  • etc…

Toutes ces terminologies sont disponibles sur Athena.

Ces terminologies doivent être importées dans LinkR, pour avoir les noms des concepts correspondants aux ID des concepts (colonnes concept_id des tables OMOP).

Voir comment importer des terminologies.

Scripts de data cleaning

Les données importées au format OMOP nécessitent souvent d’être mises en qualité à l’aide de scripts de data cleaning.

Un exemple classique est représenté par les données de poids et taille, qui, du fait de la façon dont sont créés les logiciels de soin, comporte souvent des données aberrantes, par exemple par inversion entre les champs poids et taille.

Des scripts pour exclure ce type de données sont souvent réalisés. LinkR permet de faciliter le partage de tels scripts, qui, du fait de l’utilisation du modèle de données commun OMOP, seront susceptibles de fonctionner sur les différents sets de données importés dans LinkR.

Quelques autres exemples de scripts :

  • calcul de scores, tels que l’IGS-2 ou le score SOFA
  • calcul de diurèse, en faisant la somme des différents paramètres (sonde urinaire, néphrostomie etc)
  • etc…

Voir comment créer un script de data cleaning.

Projets

Un projet est un environnement R et Python où seront analysées des données.

Un projet peut correspondre à une étude (par exemple une étude sur la prédiciton de la mortalité), mais également à des analyse de données hors étude, tels que la création de tableaux de bord (un tableau de bord permettant de visualiser l’activité d’un service hospitalier par exemple).

Lors de la création d’un projet, l’utilisateur choisit les données à utiliser, depuis les sets de données chargés dans l’application.

Le projet s’articulera autour de deux pages principales :

  • Page de données individuelles : ici, l’utilisateur pourra recréer l’équivalent d’un dossier clinique, en créant des onglets où il configurera des widgets, par exemple :

    • un onglet “Hémodynamique” où nous créerons des widgets permettant de visualiser la fréquence cardiaque, la pression artérielle et les traitements anti-hypertenseurs reçus par le patient)

    • un onglet “Notes” où nous afficherons toutes les documents textuels concernant le patient (compte-rendus hospitaliers, notes cliniques quotidiennes etc)

    • un onglet “Infectiologie” où nous afficherons toutes les données concernant l’infectiologie (prélèvements bactériologiques, antibioques reçus etc)

    • etc…

  • Page de données agrégées : ici, l’utilisateur créera de la même façon des onglets où il configurera des widgets. Il s’agira d’analyses sur un groupe de patients, par exemple :

    • un onglet “Données démographiques” où l’utilisateur affichera les données démographiques du groupe de patients (âge, sexe, durée de séjour, mortalité etc)

    • un onglet “Données aberrantes” où sera affichée la distribution des différents paramètres et excluera les données aberrantes

    • un onglet “Analyse de survie” où un widget sera configuré pour réaliser l’analyse de survie de la population sélectionnée

    • etc…

A l’aide de l’interface low-code (qui associe une interface de code et une interface graphique), le travail collaboratif entre data scientists, statisticiens et cliniciens devient plus facile.

Voir comment créer un projet.

Plugins

Les plugins sont des briques de code R et Python qui permettent d’ajouter des fonctionnalités à LinkR.

Comme nous l’avons vu dans le paragraphe précédent, les projets s’articulent en onglets.

Ces onglets comportent des widgets, qui sont des plugins appliqués à des données.

Par exemple, si je choisir le plugin “Timeline” pour être appliqué au paramètre “Fréquence cardiaque”, le widget résultant sera un graphique sous forme de timeline qui affichera la fréquence cardiaque du patient sélectionné.

Il existe des plugins de données individuelles, qui sont les éléments permettant de recréer un dossier médical, par exemple :

  • Lecteur de document : ce plugin permet d’afficher les documents textuels (compte-rendus hospitaliers, notes cliniques) et de les filtrer (avec recherche de mot clef ou avec filtre sur le titre par exemple)
  • Timeline : comme évoqué ci-dessus, pour afficher les données temporelles sous forme de timeline
  • Datatable : permet d’afficher les données sous forme de Datatable, comme par exemple l’affichage de la biologie d’un patient en fonction de l’horaire de prélèvement
  • etc…

Nous avons aussi des plugins de données agrégées, qui serviront à visualiser et analyser des données agrégées, par exemple :

  • ggplot2 : un plugin qui permet d’afficher les variables avec les différentes figures proposées par la librairie ggplot2
  • Analyse de survie : permet de réaliser des analyses de survie
  • Machine learning : pour entraîner et évaluer des modèles de machine learning, avec des librairies R ou Python
  • etc…

Voir comment créer un plugin.

Widgets

Les widgets correspondent à des plugins appliqués à des données.

Après avoir créé un onglet, je peux y ajouter plusieurs widgets.

Ces widgets peuvent être changés de taille et déplacés sur la page.

Subsets

Au niveau d’un projet, un set de données peut être partagé en plusieurs subsets.

Un subset est un sous-ensemble du set de données global, après avoir appliqué des filtres pour sélectionner des patients.

Voici des exemples de subsets que l’on pourrait imaginer sur la base de données MIMIC, qui comporte les séjours de plus de 50 000 patients en réanimation, adulte et pédiatrique :

  • Patients de plus de 18 ans admis en réanimation médicale pour COVID-19
  • Patients ayant un code CIM-10 de pneumopathie infectieuse et ayant été traité par Amoxicilline
  • Patients exclus : il peut être utile de créer un subset avec uniquement les patients exclus des analyses
  • etc…

Pour l’instant il est nécessaire de coder en R les scripts pour créer les subsets. Une interface graphique sera codée dans une prochaine version.

Voir comment créer un subset.

4 - Importer des données

Comment importer des données depuis différentes sources : bases de données, Parquet, CSV…

Créer un set de données

Pour importer des données, rendez-vous sur la page Sets de données depuis le menu en haut de l’écran ou depuis le widget de la page d’accueil.

Cliquez ensuite sur l’icône Plus (+) à gauche de l’écran pour créer un nouveau set de données.

Choisissez un nom. Pour l’exemple, nous importerons le set de données MIMIC-IV set de démo.

Pour plus d’informations sur la base de données MIMIC, rendez-vous ici.

Une fois le set créé, cliquez sur le widget correspondant à ce set et rendez-vous sur l’onglet Code à droite de l’écran.

Vous verrez que du code R a été généré automatiquement.

Fonction import_dataset

Pour importer des données dans LinkR, nous utilisons la fonction import_dataset.

Voici les arguments que peut prendre cette fonction.

Il y a certains arguments qu’il n’est pas nécessaire de modifier, nous renseignerons des valeurs par défaut :

  • r, d : ce sont des variables qui servent à communiquer des informations au sein de l’application, elles doivent être mises en arguments pour être disponibles à l’intérieur de la fonction
  • dataset_id : c’est l’ID du set de données actuel, vous pouvez remplacer cet argument pour %dataset_id%, qui sera remplacé par l’ID du set de données

Vous devrez modifier ces arguments :

  • omop_version : c’est la version d’OMOP des données que vous allez importer. Si vous indiquez %omop_version%, c’est la version renseignée dans l’onglet Sommaire qui sera utilisée
  • data_source : vous indiquerez ici d’où proviennent les données, db si les données proviennent d’une connexion à une base de données, disk si elles sont stockées localement
  • data_folder : si vous avez choisi la valeur disk pour l’argument data_source, vous indiquerez ici le dossier contenant les données
  • con : si vous avez choisi la valeur db pour l’arguemnt data_source, vous indiquerez ici la variable de connexion à la base de données
  • load_tables : par défaut, toutes les tables OMOP seront chargées depuis la source indiquée. Si vous ne souhaitez charger qu’une partie de ces tables, renseignez ici les tables à importer. Par exemple load_tables = c('person', 'visit_occurrence', 'visit_detail')

Connexion à une base de données

Connexion et lecture des données

Vous pouvez importer des données à une partie d’une connexion à une base de données.

Configurez d’abord l’objet de connexion con avec la librairie DBI, puis utilisez la fonction import_dataset.

Pour indiquer que nous chargeons une base de données, l’argument data_source doit prendre la valeur “db”.

L’argument con prendra notre objet con en valeur.

# Objet de connexion. Nous voyons ceci en détail en-dessous.
con <- DBI::dbConnect(...)

# Fonction pour charger les données au chargement du projet
import_dataset(
    r, d, dataset_id = %dataset_id%, omop_version = "5.4",
    data_source = "db", con = con
)

Ce code établira une connexion à la base de données au chargement du projet.

Voyons maintenant comment configurer la connexion à la base de données.

PostgreSQL

con <- DBI::dbConnect(
    RPostgres::Postgres(),
    host = "localhost",
    port = 5432,
    dbname = "mimic-iv-demo",
    user = "postgres",
    password = "postgres"
)

DuckDB

Vous pouvez vous connecter à une base de données duckdb via le fichier .db.

con <- DBI::dbConnect(duckdb::duckdb(), dbdir = "/my_db_file.db", read_only = TRUE)

Exemple complet

# Connexion à la base PostgreSQL locale
con <- DBI::dbConnect(
    RPostgres::Postgres(),
    host = "localhost",
    port = 5432,
    dbname = "mimic-iv-demo",
    user = "postgres",
    password = "postgres"
)

# Chargement des données au lancement du projet
import_dataset(
    r, d, dataset_id = %dataset_id%, omop_version = %omop_version%,
    data_source = "db", con = con
)

Importer des fichiers

Vous pouvez également importer des fichiers sans passer par une connexion à une base de données.

Pour cela :

  • renseignez la valeur disk dans l’argument data_source
  • renseignez la localisation des fichiers dans l’argument data_folder

Par exemple, mettons que les fichiers de ma base de données soient dans le dossier /data/mimic-omop/ :

/data/mimic-iv-demo/
--- person.parquet
--- visit_occurrence.parquet
--- visit_detail.parquet
--- measurement.parquet

Je les charge comme ceci.

import_dataset(
    r, d, dataset_id = %dataset_id%, omop_version = "5.4",
    data_source = "disk", data_folder = "/data/mimic-iv-demo/"
)

Charger certaines tables

Vous pouvez choisir de n’importer que certaines tables depuis la base de données, avec l’argument load_tables.

Vous n’avez qu’à renseigner dans un vecteur de caractères les tables à importer, comme ceci :

# Chargement des tables person, visit_occurrence, visit_detail et measurement seulement
tables <- c("person", "visit_occurrence", "visit_detail", "measurement")

# Ajout de l'argument load_tables dans import_dataset
import_dataset(
    r, d, dataset_id = %dataset_id%, omop_version = "5.4",
    data_source = "db", con = con,
    load_tables = tables
)

Depuis le catalogue de contenus

Vous pouvez également installer un set de données depuis la librairie des contenus.

Ceci vous permettra de télécharger le code permettant de charger des données, mais uniquement le code.

Les données ne seront pas téléchargées : l’accès aux données de santé nécessite généralement une authentification.

Retrouvez le tutoriel ici.

5 - Importer des terminologies

Comment importer des terminologies, depuis Athena jusqu’à LinkR

Terminologies dans OMOP

Deux types de terminologies sont utilisées dans le modèle de données commun OMOP :

  • Les terminologies standards, qui sont des terminologies internationales, de référence. Parmi celles-ci, on peut citer :

    • LOINC pour les données de biologie et les paramètres vitaux
    • SNOMED pour les diagnostics et les actes
    • RxNorm pour les prescriptions

  • Les terminologies non standards, qui sont souvent des terminologies internationales, mais pas seulement. Ces terminologies sont largement utilisées, raison pour laquelle elles sont utilisées, bien que non standards. Parmi celles-ci, on peut citer :

    • CIM-10 pour les diagnostics
    • CCAM, une terminologie française d’actes médicaux

Les terminologies standards et non standards peuvent être utilisées dans le modèle de données OMOP. Les concepts standards se retrouveront dans les colonnes _concept_id, tandis que les concepts non standards seront dans les colonnes _source_concept_id. Il faut au maximum s’efforcer d’utiliser les concepts standards lors du processus d’ETL.

ATHENA

ATHENA est une plateforme de requêtage de terminologies éditée par OHDSI.

Il est possible de recherche des concepts parmi l’ensemble des terminologies OMOP, en utilisant des filtres.

En cliquant sur l’onglet Download en haut de la page, vous pouvez télécharger les terminologies de votre choix.

Commencez par décocher toutes les terminologies en cliquant sur la case en haut à gauche de l’écran, puis sélectionnez les terminologies que vous souhaitez télécharger.

Pour l’exemple, nous allons télécharger la terminologie LOINC.

Cochez la case de la terminologie LOINC, puis cliquez sur Download vocabularies en haut à droite.

A noter que certaines terminologies ne sont pas publiques et nécessitent d’avoir une licence pour les télécharger.

Choisissez un nom pour le bundle, puis cliquez sur Download. Le site vous indiquera que le bundle va se créer, cela peut prendre quelques minutes, le temps que la requête SQL sur le serveur créé les fichiers CSV puis le ZIP.

Vous aurez ensuite à cliquer sur “Show history” puis sur “Download” pour télécharger votre bundle.

Vous allez télécharger un fichier ZIP contenant un fichier CSV par table de terminologie (“VOCABULARY.csv”, “CONCEPT.csv” etc).

Importer les terminologies dans LinkR

Il ne nous reste plus qu’à importer la terminologie dans LinkR.

Pour cela, rendez vous sur la page “Terminologies”, depuis l’accueil ou depuis le lien en haut de la page.

Cliquez ensuite sur le bouton “Importer des concepts ou des terminologies” dans la barre latérale.

Sélectionnez soit le fichier ZIP, soit les fichiers CSV.

Cliquez sur “Importer”. Ca y est, nous avons importé LOINC dans LinkR.

Requêter les terminologies dans LinkR

Rendez vous sur la base de données de l’application, depuis l’onglet en haut à droite de l’écran.

Allez sur l’onglet “Requêter la BDD” en haut à droite de l’écran, puis sélectionnez

En haut à droite de l’écran, sélectionnez “BDD publique”.

Vous pouvez requêter les tables de concept en SQL :

6 - Créer un projet

Maintenant que tout est en place, nous allons pouvoir créer un projet

Comme nous l’avons vu précédemment, un projet est un environnement R et Python où seront analysées des données.

Un projet peut correspondre à une étude (par exemple une étude sur la prédiction de la mortalité), mais également à des analyses de données hors étude, tels que la création de tableaux de bord (un tableau de bord permettant de visualiser l’activité d’un service hospitalier par exemple).

Créer un projet

Pour commencer, rendez vous sur la page des projets, depuis le menu en haut de l’écran ou depuis la page d’accueil.

Cliquez ensuite sur “Créer un projet”.

Choisissez un nom pour votre projet puis cliquez sur “Ajouter”.

Cliquez sur le projet pour l’ouvrir.

Vous arrivez sur la page d’accueil de votre projet.

Cette page d’accueil est divisée en plusieurs onglets :

  • Sommaire : sont affichées les informations principales relatives au projet : le ou les auteur(s), la description du projet, une vue rapide sur les données chargées
  • Données : ici sont donnés les détails des données chargées dans ce projet : combien de patients, combien de séjours, combien de lignes par table OMOP etc
  • Data cleaning : ici seront configurés les scripts de data cleaning qui s’appliqueront aux données au chargement du projet
  • Partage : cet onglet permet de mettre à jour un dépôt Git pour partager votre projet avec le reste de la communauté

Notons que le nom du projet apparaît en haut de l’écran. Si je suis sur une autre page du projet (Données individuelles par exemple) et que je clique sur le nom, j’atterirai de nouveau sur la page d’accueil du projet.

A droite du nom du projet sont apparus plusieurs boutons :

  • Données individuelles : pour vous rendre sur la page où vous pourrez configurer les données pour créer un dossier patient
  • Données agrégées : il s’agit de la page où vous pourrez visualiser et analyser des données de cohorte
  • Concepts : vous pourrez rechercher des concepts parmi ceux présents dans le set de données importé
  • Subsets : vous pourrez créer des sous-ensembles de patients en les filtrant selon des critères

Configurer le projet

Notre projet étant créé, nous allons le configurer.

Tout d’abord, il nous faut renseigner quel set de données sera chargé au lancement de notre projet.

Pour cela, retournons dans l’onglet “Données” de notre projet.

Actuellement, aucun set de données n’est associé à notre projet.

Si ce n’est pas déjà fait, installez le set de démo MIMIC-IV depuis notre dépôt Git, en suivant ce tutoriel. Vous trouvez le dépôt Git d’InterHop en cliquant sur son icône située sur la carte à Saint-Malo, en Bretagne.

Choisissez le set de données “MIMIC-IV set de démo” dans le menu déroulant, puis cliquez sur le bouton “Charger les données” à droite du menu déroulant.

Vous verrez maintenant que les informations sur les données chargées ont été mises à jour : vous avez chargé 100 patients, correspondant à 852 séjours.

Cliquez sur le décompte des patients pour afficher les informations les concernant.

Explorer les concepts

Les données étant chargées, nous allons pouvoir voir de quoi elles sont composées.

Chaque information dans une base de données OMOP est codée à l’aide d’un concept, appartenant à une terminologie. Chaque concept a un identifiant unique que vous pouvez retrouver via le requêteur ATHENA.

Les concepts sont stockées dans les colonnes _concept_id des différentes tables OMOP. Pour pouvoir avoir la correspondance de chaque ID de concept, vous devez importer les terminologies nécessaires dans LinkR.

Une fois que cela est fait, rendez-vous sur la page des concepts du projet via l’icône à droite du nom du projet, en haut de l’écran.

Vous arriverez sur cette page. Sélectionnez une terminologie dans le menu déroulant pour charger ses concepts.

Pour obtenir des données au format OMOP, il est nécessaire de réaliser un processus d’ETL (Extract, Transform and Load).

Durant ce processus, les données sont transformées pour être adéquates au modèle de données OMOP, et les différents concepts locaux sont alignés sur les concepts standards ATHENA. Par exemple, le code de la fréquence cardiaque d’un hôpital sera aligné au concept standard “Heart rate” de la terminologie LOINC.

Ce processus d’alignement des concepts est long et compliqué, étant donné qu’il existe des milliers de code à aligner, souvent manuellement.

C’est pourquoi la majorité des sets de données OMOP ont une partie des concepts qui sont alignés seulement. C’est pour cela que vous voyez dans le menu déroulant ci-dessus certaines terminologies standards (LOINC, SNOMED), et d’autres locales (préfixées par mimiciv).

Si vous n’aviez pas encore importé les terminologies, vous devrez recharger le décompte des concepts en cliquant sur l’icône “Recharger le décompte” en haut à gauche de l’écran.

De la même façon, si vous changez le set de données associé au projet, vous devrez recahrger le décompte des concepts.

En sélectionnant une terminologie, vous verrez apparaître dans le tableau les différents concepts de cette terminologie utilisés dans le set de données chargé pour votre projet.

Vous le nombre de patients ayant au moins une fois ce concept dans la colonne “Patients”, et le nombre de lignes toutes tables confondues associées à ce concept, dans la colonnes “Lignes”.

Lorsque vous cliquez sur un concept dans le tableau, les informations relatives à ce concept apparaîtront à droite de l’écran.

Vous pouvez notamment récupérer l’ID du concept, qui vous servira lorsque vous requêterez les tables OMOP. Vous pouvez également voir la distribution des valeurs du concept dans le set de données chargé.

Vous pouvez filtrer les concepts sur leur nom, avec le menu en haut de la colonne “Nom du concept”. Vous pouvez également choisir quelles colonnes du tableau afficher. Il s’agit des colonnes de la table OMOP CONCEPT.

Créer des onglets et des widgets

Maintenant que nous avons chargé un set de données et exploré les concepts le composant, nous allons pouvoir visualiser et analyser ces données, à l’aide de widgets.

Pour cela, rendez-vous sur la page des Données individuelles, soit à partir de l’onglet sommaire du projet, soit à partir de l’icône en haut de l’écran, à droite du titre du projet (celle avec un seul individu).

Vous arriverez sur la page des Données individuelles, où vous allez recréer un dossier patient selon les besoins de votre projet.

Le menu à gauche de l’écran vous permet :

  • d’ajouter des onglets : les onglets permettent d’organiser les différents widgets
  • d’ajouter des widgets : nous allons le voir, les widgets sont la brique élémentaire composant les projets. Ils permettent de visualiser et d’analyser les données à l’aide de plugins
  • d’éditer la page : une fois les widgets créés, vous pourrez changer leur disposition sur la page. Vous pourrez également modifier ou supprimer les onglets.
  • de sélectionner des patients : chaque subset contient plusieurs patients, chaque patient a un ou plusieurs séjours (séjour hospitalier ou consultation)

C’est à vous de choisir comment organiser votre projet.

Pour la page des données individuelles, il est habituel de créer un onglet par thématique, avec par exemple un onglet “Hémodynamique” rassemblant les données relatives à l’état hémodynamique d’un patient, ou un onglet “Infectiologie” pour afficher les éléments en rapport avec les problématiques infectieuses : traitements antibiotiques, prélèvements microbiologiques etc.

Créons un premier onglet “Hémodynamique”. Pour cela, cliquez sur le bouton “+ Onglet” à gauche de l’écran, puis choisissez un nom.

Vous aurez un nouvel onglet vide. Les onglets sont affichés à droite de l’écran.

Nous allons maintenant pouvoir ajouter différents widgets à cet onglet. Cliquez sur le bouton “+ Widget” à gauche de l’écran.

Il vous faudra :

  • choisir un nom
  • choisir un plugin
  • choisir des concepts

Un plugin est un script écrit en R et/ou Python permettant d’ajouter des fonctionnalités à l’application.

Il existe des plugins spécifiques aux données individuelles, d’autres aux données agrégées, et d’autres mixtes.

Chaque plugin a une fonctionnalité principale.

Certains plugins servent à visualiser un type de données, par exemple le plugin permettant de visualiser les données de prescriptions sous forme de timeline, ou encore le plugin permettant d’afficher les données structurées sous forme de tableau.

D’autres servent à analyser les données, par exemple le plugin servant à créer un modèle de régression logistique, ou encore celui permettant d’entraîner des modèles de machine learning.

Chaque étape d’un projet de data science peut être transformée en plugin, afin de gagner en temps et en qualité dans l’analyse des données. LinkR a pour objectif de proposer de plus en plus de plugins, grâce au travail de sa communauté.

Pour l’exemple, nous voulons afficher les paramètres hémodynamiques des patients sous forme de timeline.

Nous allons donc cliquer sur “Sélectionner un plugin”, puis sélectionner le plugin “Timeline {dygraphs}”, qui permet d’afficher les données sous forme de timeline avec la librairie Javascript dygraphs.

Si le plugin ne s'affiche pas dans la liste, téléchargez-le depuis le Catalogue des contenus.


Sélectionnons maintenant quels concepts afficher, en cliquant sur “Sélectionner des concepts”.

Pour l’exemple, nous avons séletionné les concepts de fréquence cardiaque et de pressions artérielles systolique, diastolique et moyenne avec la terminologie LOINC.

Choisissons un nom, par exemple “Timeline hémodynamique” et cliquons sur “Ajouter”. Notre widget va apparaître sur la page.

Un widget apparaîtra souvent sous la même forme, avec trois ou quatre icônes en haut du widget, deux boutons à gauche et le nom du fichier de sauvegarde.

Commençons par le menu en haut du widget.

Les icônes sont, de gauche à droite :

  • Figure : permet d’afficher la figure ou plus globalement le résultat que le plugin est censé afficher
  • Paramètres de la figure : permet de configurer la figure à l’aide d’une interface graphique
  • Code de la figure : permet d’éditer le code R ou Python qui permet d’afficher la figure
  • Paramètres généraux : ce sont les paramètres généraux du widget, permettant par exemple d’afficher ou de masquer certains éléments

Chaque widget fonctionne de la même façon : une interface graphique permet de configurer la figure. Lorsque les paramètres sont modifiés, le code R ou Python correspond peut être généré. Une fois ce code généré, il peut être modifié directement avec l’éditeur de code, ce qui permet d’aller au-delà ce que propose l’interface graphique seule.

Les widgets fonctionnent avec des fichiers de sauvegarde, permettant de sauvegarder à la fois les paramètres de la figure et le code de la figure. Ceci permet de créer plusieurs configurations pour un même widget.

Pour choisir un fichier de sauvegarde, cliquez sur le nom du fichier (ici “Pas de fichier de sauvegarde sélectionné”), puis sélectionnez le fichier dans le menu déroulant.

Pour créer un fichier de sauvegarde, cliquez sur l’icône “+” sur cette même page, choisissez un nom et créez le fichier. Pour ce premier exemple, nous choisissons le nom “Set hémodynamique 1”.

Une fois le fichier créé, les paramètres sauvegardés dans les pages “Paramètre de la figure” et “Code de la figure” seront sauvegardés dans ce fichier.

Avant de configurer notre figure, regardons les “Paramètres généraux” du widget.

Dans la rubrique “Affichage”, nous pouvons choisir d’afficher ou de masquer le fichier de sauvegarde sélectionné.

Nous pouvons également choisir d’afficher les paramètres et l’éditeur côte à côte avec la figure. Ceci permettra de diviser l’écran du widget en deux parties, avec la figure à gauche et les paramètres ou le code de la figure à droite, ce qui est utile pour voir rapidement le résultat de nos paramètres.

Dans la partie “Exécution du code”, nous pourrons choisir d’exécuter le code au chargement d’un fichier de sauvegarde : au chargement d’un projet par exemple, le dernier fichier de sauvegarde sélectionné sera chargé, ce qui permet d’initialiser tous les widgets au chargement du projet. Je peux aussi choisir de ne pas charger un widget, s’il est susceptible de prendre du temps à s’exécuter et s’il n’est pas forcément nécessaire dès le chargement du projet.

L’option “Exécuter le code lors de la mise à jour des données” permet par exemple de mettre à jour la figure lorsque le patient change, si ce widget utilise les données patient par patient.

Nous allons donc choisir de masquer le fichier de sauvegarde, d’afficher les paramètres ou l’éditeur côte à côte avec la figure, et d’exécuter le code à la fois lors du chargement du fichier de sauvegarde et lors de la mise à jour des données.

Nous voyons disparaître le nom du fichier de sauvegarde, et également l’icône de la figure : en effet, la figure s’affichera dans les onglets “Paramètres de la figure” et “Code de la figure”.

N’oubliez pas de sauvegarder vos paramètres généraux avec l’icône à gauche du widget. Les paramètres généraux du widget dépendent du widget, et non d’un fichier de sauvegarde.

Avant d’afficher nos données, réglons un dernier détail : faisons en sorte d’agrandir le widget.

Pour cela, cliquez sur “Editer la page” à gauche de l’écran. Vous verrez alors apparaître de nouvelles icônes en haut à droite du widget :

  • une icône pour passer le widget en plein écran, ce qui est utile dans la phase de configuration du widget
  • une icône pour modifier le widget, si l’on veut modifier le nom, ou ajouter ou supprimer des concepts
  • une icône pour supprimer le widget

Il y a également des icônes aux quatre coins, qui permettent de définir la taille du widget.

Faisons en sorte que le widget prenne toute la largeur de l’écran et un tiers de sa hauteur.

Puis passons le en mode plein écran. Cliquez sur “Validez les modifications” à gauche de l’écran pour sortir du mode “Edition”.

Allons dans la rubrique “Paramètres de la figure” afin de configurer notre figure.

Pour ce plugin, nous avons trois options :

  • Données à afficher : veut-on afficher les données du patient sélectionné, ou uniquement du séjour sélectionné ?
  • Concepts : quels concepts afficher ? Nous voyons ici apparaître les concepts que nous avons sélectionné lors de la création du widget. On peut choisir de n’en afficher que certains.
  • Synchroniser les timelines : ceci peut être utile pour synchroniser différents widgets entre eux.

Sélectionnez “Données du patient” dans “Données à afficher”, puis “Heart rate” dans le menu déroulant des concepts.

Cliquez ensuite sur l’icône “Sauvegarder” à gauche du widget, puis sur l’icône “Afficher la figure” (icône Play).

Il vous sera demandé de sélectionner un patient : en effet, nous n’avions pas encore choisi de patient.

Commencez par sélectionner “Tous les patients” dans le menu déroulant “Subset”, puis n’importe quel patient.

Etant donné que nous avions sélectionné le fait de mettre à jour le code au changement de patient, vous devriez voir la fréquence cardiaque du patient sélectionné sous forme de timeline.

Cliquez de nouveau sur “Editer la page”, puis sortez du mode plein écran. Votre widget devrait reprendre les dimensions que vous aviez attribuées : un tiers de la hauteur de la page et toute la largeur, ce qui est adapté pour cette timeline.

Vous pouvez zoomer sur la figure, et changer l’intervalle de temps sélectionné.

A vous de jouer !

Tentez maintenant de :

  • Créer un nouveau fichier de sauvegarde pour le widget actuel, "Set hémodynamique 2" par exemple
  • Configurer le widget pour afficher la fréquence cardiaque et les pressions artérielles systolique, diastolique et moyenne
  • Créer un nouveau widget avec le plugin "Tableau de données", où vous afficherez les mêmes concepts
  • Synchroniser les timelines des deux widgets


Vous devriez obtenir quelque chose comme ceci (exemple tiré du projet “Démo LinkR”, que vous pouvez télécharger sur le catalogue de contenus d’InterHop) :

Nous avons vu comment créer des onglets et des widgets pour créer un dossier patient, sur la page “Données individuelles”.

Le principe est le même pour la page des “Données agrégées”, sauf que les onglets correspondent généralement à des étapes d’un projet de recherche, avec par exemple un widget pour créer l’outcome de l’étude, un widget pour exclure les données aberrantes ou encore un widget pour entraîner des modèles de machine learning.

Partager le projet

Une fois votre projet configuré, vous pouvez le partager en l’intégrant à vos dépôt Git, directement depuis l’application.

Rendez-vous sur l’onglet “Partage” depuis la page principale du projet (en cliquant sur le nom du projet, en bleu, en haut de la page).

Le tutoriel pour partager du contenu est disponible ici.

7 - Console

Utiliser la console R ou Python dans LinkR

Introduction

Nous avons vu comment créer un projet en utilisant des plugins pour créer des widgets.

Ces widgets comprennent toujours un onglet pour afficher ou manipuler les données via une interface graphique, et un onglet pour modifier le code à l’origine du résultat affiché.

Modifier le code via l’éditeur des widgets peut permettre d’aller un peu plus loin que ce qui est proposé par l’interface graphique, mais dès lors que l’on veut afficher les données sous une forme différente (un tableau de données plutôt qu’une figure ggplot2 par exemple), nous sommes limités.

Pour cela, il existe deux solutions :

  • Utiliser le plugin Console, qui est un plugin générique permettant d’afficher les données sous le format demandé, par exemple un tableau de données, une figure Plotly ou même une interface web générée avec Shiny
  • Utiliser la page Console, qui est d’accès plus rapide et permet de tester des portions de code facilement avec les données chargées

Plugin Console

Si vous n’avez pas ce plugin, téléchargez le plugin depuis le catalogue de contenus d’InterHop.

Créez un widget avec le plugin Console où vous le souhaitez, dans un onglet sur la page des données individuelles ou des données agrégées.

Inutile de sélectionner des concepts, ce plugin ne les utilise pas.

Une fois le widget créé, il fonctionne de la même façon que les autres widgets, tel que détaillé dans Créer un projet > Créer des onglets et des widgets.

Dans la partie “Paramètres de la figure”, sélectionnez le langage et l’output.

Par exemple, pour le langage R, les outputs suivants sont disponibles :

  • Console : le résultat affiché sera celui qui serait affiché dans la console R
  • UI (HTML) : le résultat sera affiché dans un uiOutput de Shiny
  • Figure : correspond à un figureOutput de Shiny
  • Table : correspond à un tableOutput de Shiny
  • DataTable : correspond à un DT::DTOutput de DT et Shiny
  • RMarkdown : correspond à l’HTML sortant de la conversion du fichier RMarkdown présenté dans la console

Vous pouvez différents fichiers de sauvegardes, qui correspondront à différents scripts.

Ainsi, le plugin Console peut être utilisé par tout ce que les plugins ne peuvent pas (encore !) afficher.

Souvent, répondre à une problématique avec le plugin Console est la première étape de la création d’un plugin.

Voici un exemple de l’utilisation du plugin “Console” pour la création d’un histogramme affichant l’âge des patients du set de données associé au projet.

Voici le code utilisé si vous souhaitez le tester :

d$visit_occurrence %>%
    dplyr::left_join(
        d$person %>% dplyr::select(person_id, birth_datetime),
        by = "person_id"
    ) %>%
    dplyr::collect() %>%
    dplyr::mutate(
        age = round(
            as.numeric(
                difftime(visit_start_datetime, birth_datetime, units = "days")
            ) / 365.25, 1
        )
    ) %>%
    ggplot2::ggplot(ggplot2::aes(x = age)) +
    ggplot2::geom_histogram(binwidth = 5, fill = "#0084D8", color = "white") +
    ggplot2::labs(
        x = "Age (années)",
        y = "Fréquence"
      ) +
    ggplot2::theme_minimal() +
    ggplot2::theme(
        plot.title = ggplot2::element_text(size = 16, face = "bold"),
        axis.title = ggplot2::element_text(size = 14)
    )

Page Console

La page Console est accessible depuis n’importe quelle page de l’application, en cliquant sur l’icône correspondante en haut de l’écran.

De la même façon que pour le plugin Console, vous pouvez choisir le langage de programmation et l’output.

En plus de pouvoir utiliser R et Python, le Shell est accessible ici, permettant par exemple d’afficher les fichiers et dossiers avec ls, ce qui est utile dans le cas d’un container Docker.

Cette console permet également d’aider dans la programmation de LinkR, en rendant les différentes variables de fonctionnement de l’application disponibles. Ces variables sont préfixées par r$.

Par exemple, r$users permet d’afficher la variable contenant les utilisateurs.

L’accès à cette console peut être restreint depuis la page de gestion des utilisateurs.

L’accès aux variables r$... n’est pas disponible depuis le plugin Console.

Que ce soit dans le plugin Console ou sur la page Console, des raccourcis sont disponibles :

  • Ctrl|CMD + Shift + C : commente ou décommente le code sélectionné
  • Ctrl/CMD + Enter : exécute le code sélectionné (exécute tout le code si rien n’est sélectionné)
  • Ctrl/CMD + Shift + Enter : exécute tout le code

Variables des données

L’intérêt principal de cette console, que ce soit via le plugin Console ou via la page Console, réside dans le fait de pouvoir manipuler les tables OMOP depuis un set de données.

Pour charger des données, vous pouvez :

  • soit les charger depuis la page “Set de données”, en sélectionnant un set, puis en allant dans l’onglet “Code” et en cliquant sur “Exécuter le code”
  • soit en chargeant un projet associé à un set de données

Une fois les données chargées, elles deviennent accessibles via les variables préfixées par d$ (d pour data).

Toutes les tables OMOP sont disponibles via ces variables.

Comme vous pouvez le voir ici, toutes les tables sont chargées en lazy (d’où les points d’interrogations à la place du nombre de lignes du dataframe), ce qui signifie qu’elles ne sont pas chargées en mémoire.

Ceci permet d’économiser des ressources, de filtrer les donneés avant de les charger en mémoire, avec dplyr::collect().

Dans l’exemple suivant, nous filtrons les données de la table Measurement pour le patient 13589912, avant de collecter les données en mémoire.

Les tables disponibles dans d$ sont les tables complètes, comprenant toutes les données du set de données chargé.

Des sous-ensembles de ces données existent, en fonction des éléments sélectionnés :

  • d$data_subset : comprend tous les tables des patients du subset sélectionné
  • d$data_person : comprend les données du patient sélectionné
  • d$data_visit_detail : comprend les données du séjour sélectionné

Chacune de ces variables comprendra les tables OMOP, sous la forme d$data_person$measurement par exemple, sauf pour les tables pour lesquelles cela n’aurait pas de sens (il n’existe pas de table d$data_person$person, étant donné que la table d$person recense les patients).

Par exemple, si dans le projet actuellement ouvert, j’ai sélectionné le même patient que précédemment (13589912), j’obtiendrai avec d$data_person$measurement les mêmes données que précédemment, lorsque j’ai filtré la variable globale d$measurement sur ce patient.

Pour obtenir les éléments sélectionnés, je peux utiliser les variables préfixées par m$ :

  • m$selected_subset : subset actuellement sélectionné
  • m$selected_person : patient sélectionné
  • m$selected_visit_detail : séjour sélectionné

Les concepts des terminologies OMOP sont disponibles dans la variable d$concept.

Vous pouvez utiliser la fonction join_concepts pour faciliter les jointures entre les variables.

d$measurement %>%
    dplyr::filter(person_id == 13589912) %>%
    join_concepts(d$concept, c(
        "measurement", "measurement_type",
        "operator", "unit"
    )) %>%
    dplyr::select(
        person_id,
        measurement_concept_id, measurement_concept_name,
        value_as_number, unit_concept_name
    ) %>%
    dplyr::collect()

Vous pouvez retrouver les requêtes OMOP usuelles dans ce tutoriel.

Requêtes SQL

Par souci d’interopérabilité, il est nécessaire de pouvoir requêter les tables OMOP en SQL.

Lorsque vous importez des données dans LinkR, il s’agit toujours d’une connexion à une base de donnnées.

En effet, soit vous utilisez la valeur “db” pour l’argument data_source, dans ce cas vous renseignez l’objet con qui est directement l’objet de connexion à la base de données OMOP, soit vous utilisez la valeur “disk” pour l’argument data_source, et à ce moment-là, que vos données soient au format Parquet ou CSV, elles sont chargées en créant une base de données DuckDB.

Ainsi, dès que des données sont chargées dans LinkR, un objet de connexion d$con est créé, vous permettant de requêter vos données en SQL.

Ce code vous permet donc d’afficher toutes les données de la table patient :

DBI::dbGetQuery(d$con, "SELECT * FROM person") %>% tibble::as_tibble()

Vous pouvez également utiliser les requêtes SQL du tutoriel requêtes OMOP usuelles.

Cette requête, extraite de ce tutoriel, vous permet d’obtenir l’âge des patients :

sql <- "
    SELECT 
        v.visit_occurrence_id,
        v.person_id,
        ROUND(
            EXTRACT(EPOCH FROM (
                CAST(v.visit_start_datetime AS TIMESTAMP) - 
                CAST(p.birth_datetime AS TIMESTAMP)
            )) / (365.25 * 86400), 
            1
        ) AS age
    FROM 
        visit_occurrence v
    LEFT JOIN 
        (SELECT person_id, birth_datetime FROM person) p
    ON 
        v.person_id = p.person_id;
"

DBI::dbGetQuery(d$con, sql) %>% tibble::as_tibble()

Jointure avec la table CONCEPT

Il n'est actuellement pas possible de réaliser des jointures en SQL, les concepts se trouvant dans une base de données différente de celle des données.
Ceci sera résolu dès que possible, en ajoutant une table CONCEPT à la base de données chargée, contenant uniquement les concepts utilisés par le set de données chargé (#135).

8 - Créer un subset

Analysez des sous-ensembles de population à l’aide des subsets

Introduction

Au cours d’un projet, il est souvent nécessaire de travailler sur un sous-ensemble de la population de patients d’un set de données.

Il peut par exemple être intéressant de créer un subset “Patients inclus” ne contenant que les patients finalement inclus dans les analyses finales d’une étude.

De la même façon, on peut imaginer créer un subset de patients présentant un certain diagnostic, inclus sur une période ou encore exposé à tel ou tel traitement.

Tout ceci est possible grâce à la création de subsets.

Créer un subset

Pour créer un subset, rendez-vous sur la page des Subsets. Pour cela, il est nécessaire d’avoir chargé un projet. Cliquez ensuite sur l’icône “Subsets” à partir du menu en haut de la page, à droite du nom du projet chargé.

Vous arriverez sur la page des subsets du projet.

Un subset est un sous-ensemble d’un set de données, mais il dépend d’un projet. Si deux projets utilisent le même set de données, ils ne partageront pas les mêmes sets de données.

Un subset “Tous les patients” est créé par défaut à la création d’un projet.

Pour créer un subset, cliquez sur l’icône “+” à gauche de l’écran.

Choisissez un nom, puis cliquez sur “Ajouter”. Nous allons pour l’exemple un subset contenant les patients âgés de plus de 50 ans.

Cliquez sur le subset que vous venez de créer : vous arriverez sur la page du subset sélectionné.

A droite de l’écran, vous avez deux onglets :

  • Sommaire : sont présentées les informations du subset, qu’il est possible modifier (notamment la description du subset)
  • Code : cet onglet permet de modifier le code et de rajouter ou retirer des patients d’un subset, ce que nous allons voir dans les prochains paragraphes

Ajouter des patients à un subset

Pour ajouter des patients à un subset, nous utilisons la fonction add_patients_to_subset.

Cette fonction prend les arguments suivants :

  • patients : un vecteur numérique contenant les ID des patients à ajouter
  • subset_id : l’ID du subset auquel les patients seront ajoutés (remplacé par %subset_id% dans le code du subset, qui est remplacé par l’ID du subset sélectionné)
  • output, r, m, i18n et ns : arguments nécessaires pour manipuler les données et afficher les messages d’erreurs

A la création d’un subset, le code permettant d’ajouter tous les patients au subset est créé.

Ce code sera exécuté si l’utilisateur appuie sur le bouton pour exécuter le code, ou si le subset est sélectionné depuis le projet (et s’il ne contient pas déjà des patients).

Nous allons modifier ce code pour ajouter les patients de plus de 50 ans.

Reprenons le code pour créer une colonne contenant l’âge des patients, depuis le tutoriel sur les requêtes OMOP usuelles.

d$visit_occurrence %>%
    dplyr::left_join(
        d$person %>% dplyr::select(person_id, birth_datetime),
        by = "person_id"
    ) %>%
    dplyr::collect() %>%
    dplyr::mutate(
        age = round(as.numeric(difftime(visit_start_datetime, birth_datetime, units = "days")) / 365.25, 1)
    )

L’éditeur de code du subset sélectionné permet de tester le code. Nous allons extraire les ID des patients ayant un âge supérieur à 50 ans. Nous mettons pour l’instant la fonction add_patients_to_subset en commentaire.

d$visit_occurrence %>%
    dplyr::left_join(
        d$person %>% dplyr::select(person_id, birth_datetime),
        by = "person_id"
    ) %>%
    dplyr::collect() %>%
    dplyr::mutate(
        age = round(as.numeric(difftime(visit_start_datetime, birth_datetime, units = "days")) / 365.25, 1)
    ) %>%
    dplyr::filter(age > 50) %>%
    dplyr::distinct(person_id) %>%
    dplyr::pull()

Notre code fonctionne, nous pouvons donc stocker ces ID dans une variable puis l’intégrer à la fonction add_patients_to_subset.

Un message nous indique que les patients ont bien été ajoutés au subset.

Interface graphique

Il n'existe pas encore d'interface graphique, qui serait bien utile pour filtrer les patients sur certaines caractéristiques (âge, sexe, durée de séjour, dates d'hospitalisation, présence de concepts tels que des diagnostics ou des traitements).

Cette interface graphique sera développée dans la prochaine version.


Retirer des patients d’un subset

Pour retirer des patients d’un subset, il faut utiliser la fonction remove_patients_from_subset, qui fonctionne comme add_patients_to_subset, avec les mêmes arguments, notamment patients et subset_id.

Nous pourrions par exemple, après avoir ajouté tous les patients dans le subset, retirer ceux ayant un âge inférieur ou égal à 50 ans.

Intégration dans les plugins

Il serait intéressant de créer un plugin de Données individuelles permettant d’exclure des patients avec un ou plusieurs critères d’exclusions, créés par l’utilisateur.

On pourrait imaginer que ce plugin retire les patients du subset “Patients inclus”, et les ajouteraient au subset “Patients exclus”.

Pour ce faire, il suffirait d’utiliser les fonctions add_patients_to_subset et remove_patients_from_subset.

Comment récupérer l’ID des subsets ? Grâce à la variable m$subsets.

Il n’y a plus qu’à créer le plugin !

9 - Créer un script de data cleaning

Comment créer et appliquer des scripts de data cleaning, pour obtenir des données de qualité

Cette fonctionnalité sera disponible dans la version 0.4.

10 - Créer un plugin

Comment créer des plugins, pour ajouter des foncitonnalités à LinkR

Les plugins sont ce qui permet de visualiser et d’analyser les données à l’aide d’une interface low-code.

Ce sont des scripts écrits en R et Python, qui utilisent la librairie Shiny pour créer l’interface graphique.

Ils permettent de réaliser n’importe quelle visualisation ou n’importe quelle analyse sur les données, du moment que cela est possible en R ou en Python.

LinkR évolue en permanence grâce aux plugins créés par la communauté de ses utilisateurs.

Nous allons tout d’abord voir comment créer un plugin simple, puis nous verrons comment créer des plugins plus complexes grâce au template de développement proposé par InterHop.

10.1 - Création d'un plugin simple

Création d’un plugin simple permettant d’afficher les données sous forme d’histogramme

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 variable d$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")
})

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.


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 :

    1. Récupérer le concept sélectionné dans le menu déroulant
    1. 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’.
    1. S’assurer que le tibble des données, filtré avec le concept sélectionné, n’est pas vide
    1. Créer le code de notre histogramme avec ggplot
    1. 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
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.

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.

10.2 - Création d'un plugin avancé

Création d’un plugin plus complexe en utilisant le template de développement d’InterHop

Template de développement

La création de plugins peuvent rapidement s’avérer complexe, en étant composé de nombreux scripts.

C’est pourquoi InterHop a créé un plugin servant de template de développement, que vous pouvez télécharger sur le catalogue de contenus d’InterHop, dans la rubrique “Plugins”.

Une fois le template téléchargé, créez un nouveau plugin à partir du template.

Ceci va créer un nouveau plugin en copiant l’ensemble des fichiers du template.

Sélectionnez ce nouveau plugin puis rendez-vous sur l’onglet “Code”. Vous devriez avoir ces scripts.

Nous retrouvons donc les trois fichiers de base composant n’importe quel plugin (qu’il n’est d’ailleurs pas possible de renommer ou supprimer) : ui.R, server.R et translations.csv.

Nous avons en plus de ceux-ci des fichiers préfixés par ui_* et server_*. Ces scripts permettent de fragmenter le code exécuté dans ui.R et server.R. Ils sont chacun importés avec la balise %import_script()%.

Lorsque le plugin sera exécuté, ne seront exécutés que les trois fichiers de base. Ces fichiers doivent donc appeler les autres fichiers composant le plugin.

Voici une partie du fichier ui.R.

%import_script('ui_load_general_settings.R')%

tagList(
    # Code retiré pour l'exemple...
    shinyjs::hidden(
        div(
            id = ns("general_settings_div_%widget_id%"),
            div(
                id = ns("general_settings_sidenav_%widget_id%"),
                save_general_settings_button,
                class = "widget_icon",
                style = "border-right: solid grey 0.5px; width: 25px;"
            ),
            %import_script('ui_general_settings.R')%,
            style = "display: flex; height: calc(100% - 40px);"
        )
    ),
    shinyjs::hidden(
        div(
            id = ns("settings_files_div_%widget_id%"),
            %import_script('ui_settings_files.R')%,
            style = "display: flex; height: calc(100% - 40px);"
        )
    )
)

Nous voyons ici que nous utilisons plusieurs fois la balise %import_script()% pour intégrer les autres scripts de l’UI. La syntaxe est simple, il suffit de mettre le nom du script avec son extension à l’intérieur d’apostrophes, tel que %import_script('ui_load_general_settings.R')% pour importer le script ui_load_general_settings.R.

Ces balises seront remplacées à l’exécution du plugin par un copier-coller du code contenu dans le script.

Pour les fichiers ui_*.R, les scripts doivent contenir du code R uniquement. Cela peut être du code R standard comme nous le verrons dans ui_load_general_settings.R, ou du code R - Shiny. Nous verrons que nous pouvons également importer des scripts Python dans server.R.

Si vous exécutez le code du plugin, vous devriez obtenir le résultat suivant.

Cette structure doit vous rappeler celle détaillée dans Créer un projet > Créer des onglets et des widgets.

Nous retrouvons les trois ou quatre icônes en haut de l’écran (selon les paramètres généraux du widget) :

  • Figure : permet d’afficher la figure ou plus globalement le résultat que le plugin est censé afficher
  • Paramètres de la figure : permet de configurer la figure à l’aide d’une interface graphique
  • Code de la figure : permet d’éditer le code R ou Python qui permet d’afficher la figure
  • Paramètres généraux : ce sont les paramètres généraux du widget, permettant par exemple d’afficher ou de masquer certains éléments

Par défaut, la page affichée est celle des paramètres de la figure côte à côte avec la figure.

ID des input

Pensez à nommer les ID de vos inputs de cette façon : ns(le_nom_de_mon_input_%widget_id%).

L'ID est contenu dans la fonction ns() et qu'il contient la balise %widget_id% en suffixe, qui sera remplacée par l'ID du widget, ce qui permet d'assurer le caractère unique de chaque éléménet de l'UI, sans quoi la page ne s'afficherait pas.

UI - interfance utilisateur / frontend

Voici le contenu du fichier ui.R en entier (susceptible de changer en fonction des versions du template de développement).

# UI - main file

## Hide save buttons if the user does not have access

if ("projects_console_access" %in% user_accesses){
    code_button <- shiny.fluent::IconButton.shinyInput(
        ns("code_button_%widget_id%"), iconProps = list(iconName = "Code"), title = i18np$t("show_code_editor"),
        onClick = htmlwidgets::JS(paste0("item => {",
            "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
            "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'code');",
        "}"))
    )
} else code_button <- ""

if ("projects_widgets_settings" %in% user_accesses){
    save_figure_settings_buttons <- shiny.fluent::IconButton.shinyInput(ns("save_params_and_code_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_figure_settings_and_code"), style = "margin: 0")
    save_general_settings_button <- shiny.fluent::IconButton.shinyInput(ns("save_general_settings_button_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_general_settings"), style = "margin: 0")
} else {
    save_figure_settings_buttons <- ""
    save_general_settings_button <- ""   
}

%import_script('ui_load_general_settings.R')%

tagList(
    div(
        shinyjs::hidden(
            div(
                id = ns("figure_button_div_%widget_id%"),
                shiny.fluent::IconButton.shinyInput(
                    ns("figure_button_%widget_id%"), iconProps = list(iconName = "BarChart4"), title = i18np$t("show_figure"),
                    onClick = htmlwidgets::JS(paste0("item => {",
                        "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
                        "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure');",
                    "}"))
                )
            )
        ),
        shiny.fluent::IconButton.shinyInput(
            ns("figure_settings_button_%widget_id%"), iconProps = list(iconName = "AllApps"), title = i18np$t("show_figure_settings"),
            onClick = htmlwidgets::JS(paste0("item => {",
                "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
                "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure_settings');",
            "}"))
        ),
        code_button,
        shiny.fluent::IconButton.shinyInput(
            ns("general_settings_button_%widget_id%"), iconProps = list(iconName = "Settings"), title = i18np$t("show_general_settings"),
            onClick = htmlwidgets::JS(paste0("item => {",
                "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
                "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'general_settings');",
            "}"))
        ),
        uiOutput(
            ns("settings_files_ui_%widget_id%"),
            onclick = paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random())")
        ),
        class = "widget_icon",
        style = "display: flex; color: #808080; border-bottom: solid grey 0.5px; height: 28px; padding: 5px 0 0 5px; font-size: 12px;"
    ),
    div(
        id = ns("figure_settings_code_div_%widget_id%"),
        div(
            id = ns("figure_settings_code_sidenav_%widget_id%"),
            shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
            save_figure_settings_buttons,
            class = "widget_icon",
            style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
        ),
        div(
            id = ns("figure_div_%widget_id%"),
            %import_script('ui_figure.R')%,
            style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
            class = "left-panel"
        ),
        div(
            id = ns("resizer_%widget_id%"),
            style = "width: 5px; cursor: col-resize; background-color: #ccc;",
            class = "resizer"
        ),
        div(
            id = ns("figure_settings_div_%widget_id%"),
            %import_script('ui_figure_settings.R')%,
            style = paste0("height: 100%; flex-basis: ", div_width, "%; padding: 5px; overflow: auto; flex: 1; box-sizing: border-box;")
        ),
        shinyjs::hidden(
            div(
                id = ns("code_div_%widget_id%"),
                %import_script('ui_code.R')%,
                style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
                class = "right-panel"
            )
        ),
        style = "display: flex; height: calc(100% - 34px);"
    ),
    shinyjs::hidden(
        div(
            id = ns("general_settings_div_%widget_id%"),
            %import_script('ui_general_settings.R')%,
            style = "height: calc(100% - 40px);"
        )
    ),
    shinyjs::hidden(
        div(
            id = ns("settings_files_div_%widget_id%"),
            %import_script('ui_settings_files.R')%,
            style = "display: flex; height: calc(100% - 40px);"
        )
    )
)

Prenons ces parties une par une.

## Hide save buttons if the user does not have access

if ("projects_console_access" %in% user_accesses){
    code_button <- shiny.fluent::IconButton.shinyInput(
        ns("code_button_%widget_id%"), iconProps = list(iconName = "Code"), title = i18np$t("show_code_editor"),
        onClick = htmlwidgets::JS(paste0("item => {",
            "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
            "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'code');",
        "}"))
    )
} else code_button <- ""

if ("projects_widgets_settings" %in% user_accesses){
    save_figure_settings_buttons <- shiny.fluent::IconButton.shinyInput(ns("save_params_and_code_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_figure_settings_and_code"), style = "margin: 0")
    save_general_settings_button <- shiny.fluent::IconButton.shinyInput(ns("save_general_settings_button_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_general_settings"), style = "margin: 0")
} else {
    save_figure_settings_buttons <- ""
    save_general_settings_button <- ""   
}

Les droits de l’utilisateur sont disponibles dans la variable user_accesses. Ceci nous permet de :

  • masquer le bouton d’édition du code si l’utilisateur n’a pas accès à la console dans les projets
  • masquer les boutons d’édition des widgets si l’utilisateur n’a pas ce droit

Ensuite nous importons le fichier ui_load_general_settings.R avec

%import_script('ui_load_general_settings.R')%

Nous y reviendrons plus tard.

Détaillons les différents éléments du tagList :

UI du header

div(
    shinyjs::hidden(
        div(
            id = ns("figure_button_div_%widget_id%"),
            shiny.fluent::IconButton.shinyInput(
                ns("figure_button_%widget_id%"), iconProps = list(iconName = "BarChart4"), title = i18np$t("show_figure"),
                onClick = htmlwidgets::JS(paste0("item => {",
                    "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
                    "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure');",
                "}"))
            )
        )
    ),
    shiny.fluent::IconButton.shinyInput(
        ns("figure_settings_button_%widget_id%"), iconProps = list(iconName = "AllApps"), title = i18np$t("show_figure_settings"),
        onClick = htmlwidgets::JS(paste0("item => {",
            "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
            "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure_settings');",
        "}"))
    ),
    code_button,
    shiny.fluent::IconButton.shinyInput(
        ns("general_settings_button_%widget_id%"), iconProps = list(iconName = "Settings"), title = i18np$t("show_general_settings"),
        onClick = htmlwidgets::JS(paste0("item => {",
            "Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
            "Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'general_settings');",
        "}"))
    ),
    uiOutput(
        ns("settings_files_ui_%widget_id%"),
        onclick = paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random())")
    ),
    class = "widget_icon",
    style = "display: flex; color: #808080; border-bottom: solid grey 0.5px; height: 28px; padding: 5px 0 0 5px; font-size: 12px;"
)

Il s’agit du code permettant d’afficher les icônes du header du plugin. Notez que l’on utilise la variable code_button définie plue haut, afin de masquer ce bouton si l’utilisateur n’a pas accès. Nous utilisons la classe widget_icon pour le CSS des icônes.

Nous utilisons des triggers avec onclick (pour les fonctions Shiny) ou OnClick (pour les fonctions shiny.fluent) qui vont permettre de modifier la valeur de input$current_tab_%widget_id% afin de changer l’onglet du widget affiché.

input$show_settings_files_tab_%widget_id% permettra d’afficher la page des fichiers de sauvegarde lorsque l’on cliquera sur le nom du fichier de sauvegarde dans le header (en cliquant sur “Pas de fichier de sauvegarde sélectionné” sur la capture d’écran ci-dessus).

UI de la figure, des paramètres et du code de la figure

div(
    id = ns("figure_settings_code_div_%widget_id%"),
    div(
        id = ns("figure_settings_code_sidenav_%widget_id%"),
        shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
        save_figure_settings_buttons,
        class = "widget_icon",
        style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
    ),
    div(
        id = ns("figure_div_%widget_id%"),
        %import_script('ui_figure.R')%,
        style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
        class = "left-panel"
    ),
    div(
        id = ns("resizer_%widget_id%"),
        style = "width: 5px; cursor: col-resize; background-color: #ccc;",
        class = "resizer"
    ),
    div(
        id = ns("figure_settings_div_%widget_id%"),
        %import_script('ui_figure_settings.R')%,
        style = paste0("height: 100%; flex-basis: ", div_width, "%; padding: 5px; overflow: auto; flex: 1; box-sizing: border-box;")
    ),
    shinyjs::hidden(
        div(
            id = ns("code_div_%widget_id%"),
            %import_script('ui_code.R')%,
            style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
            class = "right-panel"
        )
    ),
    style = "display: flex; height: calc(100% - 34px);"
)

Le premier div permet d’afficher les boutons de la barre latérale gauche, avec le bouton pour afficher la figure et le bouton pour sauvegarder les paramètres et le code de la figure.

div(
    id = ns("figure_settings_code_sidenav_%widget_id%"),
    shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
    save_figure_settings_buttons,
    class = "widget_icon",
    style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
)

Notez que l’on utilise save_figure_settings_buttons afin de masquer le bouton de sauvegarde si l’utilisateur n’a pas l’accès.

  1. UI de la figure

Le script de l’UI de la figure est importé dans ce code.

div(
    id = ns("figure_div_%widget_id%"),
    %import_script('ui_figure.R')%,
    style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
    class = "left-panel"
)

Regardons le fichier ui_figure.R.

# UI - Figure page
#
# Insert the UI components for the figure in this section.

div(
    shinyjs::hidden(
        div(
            id = ns("error_message_div_%widget_id%"),
            uiOutput(ns("error_message_%widget_id%")),
            style = "padding-top: 10px;"
        )
    ),
    # Div containing the figure output
    style = "width: 100%; height: 100%;"
)

On a un uiOutput pour afficher un message d’erreur, que nous verrons dans le script backend correspondant.

C’est à vous d’ajouter l’UI le code pour l’output comme vous l’entendez, avec une fonction d’output Shiny.

Ce tableau rassemble les fonctions d’output proposées par Shiny, mais il en existe d’autres, proposées par des librairies de visualisation, telle que plotlyOutput.

  1. UI des paramètres de la figure

Le script de l’UI des paramètres de la figure est importé dans ce code.

div(
    id = ns("figure_settings_div_%widget_id%"),
    %import_script('ui_figure_settings.R')%,
    style = paste0("height: 100%; flex-basis: ", div_width, "%; margin: 5px 10px; overflow: auto; flex: 1; box-sizing: border-box;")
)

Notez que nous avons entre le code de la figure et le code des paramètres de la figure un resizer, qui permettra d’ajuster la taille des deux blocs lorsqu’ils seront affichés côte à côte.

div(
    id = ns("resizer_%widget_id%"),
    style = "width: 5px; cursor: col-resize; background-color: #ccc;",
    class = "resizer"
)

Voici le contenu du fichier ui_figure_settings.R.

# UI - Figure settings
#
# Insert the UI components for configuring the figure settings in this section.

div(
    
)

Cette UI est donc vide, à vous d’y ajouter les éléments permettant de configurer la figure, avec les fonctions d’input Shiny ou celles de shiny.fluent.

  1. UI du code de la figure

Et finalement, voici le code permettant d’importer l’UI du code de la figure.

shinyjs::hidden(
    div(
        id = ns("code_div_%widget_id%"),
        %import_script('ui_code.R')%,
        style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
        class = "right-panel"
    )
)

On voit que le code est encapsulé dans une fonction shiny.hidden, ce qui lui attribura un style avec display: none par défaut. Ceci permet au chargement du widget d’afficher la figure et les paramètres de la figure côte à côte en masquant le bloc avec l’éditeur de code.

Voici le contenu de ui_code.R.

# UI - Code page

shinyAce::aceEditor(
    ns("code_%widget_id%"), value = "", mode = "r",
    hotkeys = list(
        save = list(win = "CTRL-S", mac = "CTRL-S|CMD-S"),
        run_all = list(win = "CTRL-SHIFT-ENTER", mac = "CTRL-SHIFT-ENTER|CMD-SHIFT-ENTER"),
        comment = list(win = "CTRL-SHIFT-C", mac = "CTRL-SHIFT-C|CMD-SHIFT-C")
    ),
    autoScrollEditorIntoView = TRUE, height = "100%", debounce = 100, fontSize = 11, showPrintMargin = FALSE
)

Comme partout dans LinkR, nous utilisons les éditeurs de texte Ace avec la fonction shinyAce.

On utilise ici les mêmes raccourcis qu’ailleurs sur LinkR, pour sauvegarder le code, le commenter et l’exécuter. Le debounce à 100 permet que l’input correspondant à l’éditeur soit mis à jour fréquemment.

UI des paramètres généraux du widget

L’UI des paramètres généraux est généré avec ce code.

shinyjs::hidden(
    div(
        id = ns("general_settings_div_%widget_id%"),
        %import_script('ui_general_settings.R')%,
        style = "height: calc(100% - 40px);"
    )
)

Voici le contenu de ui_general_settings.R.

# UI - General settings
#
# Include the UI components for the general settings in this section.
# If you add any fields here, ensure that you also update server_general_settings.R and ui_general_settings.R accordingly.

div(
    div(
        tags$strong(i18np$t("display")), br(),
        div(
            shiny.fluent::Toggle.shinyInput(
                ns("show_settings_file_%widget_id%"), value = toggle_values$show_settings_file,
                onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
            ),
            tags$label(i18np$t("show_settings_file"), `for` = ns("show_settings_file_%widget_id%"), style = "margin-left: 5px;"),
            style = "display: flex; margin-top: 8px;" 
        ),
        div(
            shiny.fluent::Toggle.shinyInput(
                ns("figure_and_settings_side_by_side_%widget_id%"), value = toggle_values$figure_and_settings_side_by_side,
                onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
            ),
            tags$label(i18np$t("figure_and_settings_side_by_side"), `for` = ns("figure_and_settings_side_by_side_%widget_id%"), style = "margin-left: 5px;"),
            style = "display: flex; margin-top: 5px;" 
        )
    ),
#     div(
#         tags$strong(i18np$t("code_execution")), br(),
#         div(
#             shiny.fluent::Toggle.shinyInput(
#                 ns("run_code_at_settings_file_load_%widget_id%"), value = toggle_values$run_code_at_settings_file_load,
#                 onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
#             ),
#             tags$label(i18np$t("run_code_at_settings_file_load"), `for` = ns("run_code_at_settings_file_load_%widget_id%"), style = "margin-left: 5px;"),
#             style = "display: flex; margin-top: 8px;" 
#         ),
#         div(
#             shiny.fluent::Toggle.shinyInput(
#                 ns("run_code_on_data_update_%widget_id%"), value = toggle_values$run_code_on_data_update,
#                 onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
#             ),
#             tags$label(i18np$t("run_code_on_data_update"), `for` = ns("run_code_on_data_update_%widget_id%"), style = "margin-left: 5px;"),
#             style = "display: flex; margin-top: 5px;" 
#         ),
#         style = "margin-top: 10px;"
#     ),
    style = "padding: 10px;"
)

Nous affichons par défaut les toggles permettant de gérer l’affichage. Les toggles permettant de gérer l’exécution du code sont par défaut mis en commentaires.

Nous voyons que les valeurs sont définies par la variable toggle_values, qui est déclarée dans ui_load_general_settings.R, que voici :

# UI - Load general settings
#
# Insert the UI components for configuring the figure settings here.

sql <- glue::glue_sql("SELECT link_id, name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings'", .con = m$db)
general_settings <- DBI::dbGetQuery(m$db, sql)

toggle_values <- list()

if (nrow(general_settings) == 0){

    toggle_values$show_settings_file <- TRUE
    toggle_values$figure_and_settings_side_by_side <- TRUE
    toggle_values$run_code_on_data_update <- FALSE
    toggle_values$run_code_at_settings_file_load <- FALSE
    
    dropdown_options <- list()
    selected_file <- NULL
    
} else if (nrow(general_settings) > 0){

    # Toggles values
    
    # general_settings_vec <- c("show_saved_file", "figure_and_settings_side_by_side", "run_code_on_data_update", "run_code_at_settings_file_load")
    general_settings_vec <- c("show_settings_file", "figure_and_settings_side_by_side")
    
    sapply(general_settings_vec, function(name){
        
        toggle_value <- FALSE
        
        row <- general_settings %>% dplyr::filter(name == !!name)
        if (nrow(row) > 0){
            if (is.na(row %>% dplyr::pull(value_num))) toggle_value <- FALSE
            else (toggle_value <- as.logical(row %>% dplyr::pull(value_num)))
        }
        
        toggle_values[[name]] <<- toggle_value
    })
    
    # Selected settings file
    
    sql <- glue::glue_sql("SELECT id, value AS name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name'", .con = m$db)
    m$settings_filenames_%widget_id% <- DBI::dbGetQuery(m$db, sql)
    dropdown_options <- convert_tibble_to_list(m$settings_filenames_%widget_id%, key_col = "id", text_col = "name")
    selected_file <- general_settings %>% dplyr::filter(name == "selected_file_id") %>% dplyr::pull(value_num)
}

if (toggle_values$figure_and_settings_side_by_side) div_width <- "50%" else div_width <- "100%"

On requête la base de données de l’application pour aller chercher les valeurs enregistrées des paramètres généraux.

UI des fichiers de sauvegarde

L’UI des fichiers de sauvegarde est généré avec ce code.

shinyjs::hidden(
    div(
        id = ns("settings_files_div_%widget_id%"),
        %import_script('ui_settings_files.R')%,
        style = "display: flex; height: calc(100% - 40px);"
    )
)

Voici le contenu de ui_settings_files.R.

# UI - Settings files
#
# This code corresponds to the page that appears when you click on the file icon at the top of the widget.
# The UI allows you to select, add, and delete settings files.

tagList(

    # Add settings file modal
    shinyjs::hidden(
      div(
        id = ns("add_settings_file_modal_%widget_id%"),
        div(
            div(
                tags$h1(i18np$t("create_settings_file"), style = "font-size: 14px;"),
                shiny.fluent::IconButton.shinyInput(ns("close_add_settings_file_modal_%widget_id%"), iconProps = list(iconName = "ChromeClose")),
                style = "display: flex; justify-content: space-between;",
                class = "small_close_button"
            ),
            div(shiny.fluent::TextField.shinyInput(ns("settings_file_name_%widget_id%"), label = i18np$t("file_name")), style = "width: 200px;"),
            div(
                shiny.fluent::PrimaryButton.shinyInput(ns("add_settings_file_%widget_id%"), i18np$t("add")),
                style = "position: absolute; right: 10px; bottom: 8px;"
            ),
            style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
        ),
        style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0;  width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
      )
    ),
    
    # Delete settings file modal
    shinyjs::hidden(
      div(
        id = ns("delete_settings_file_modal_%widget_id%"),
        div(
            tags$h1(i18np$t("delete_settings_file_title"), style = "font-size: 14px;"),
            tags$p(i18np$t("delete_settings_file_text")),
            div(
                shiny.fluent::DefaultButton.shinyInput(ns("close_file_deletion_modal_%widget_id%"), i18np$t("dont_delete")),
                div(shiny.fluent::PrimaryButton.shinyInput(ns("confirm_file_deletion_%widget_id%"), i18np$t("delete")), class = "delete_button"),
                style = "position: absolute; right: 10px; bottom: 8px; display: flex; gap: 5px;"
            ),
            style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
        ),
        style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0;  width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
      )
    ),
    
    # Sidenav
    div(
        id = ns("settings_files_sidenav_%widget_id%"),
        shiny.fluent::IconButton.shinyInput(ns("create_settings_file_%widget_id%"), iconProps = list(iconName = "Add"), title = i18np$t("create_settings_file"), style = "margin: 0"),
        class = "widget_icon",
        style = "border-right: solid grey 0.5px; padding-left: 5px;"
    ),
    
    # Dropdown and delete button
    div(
        div(shiny.fluent::Dropdown.shinyInput(ns("settings_file_%widget_id%"), label = i18np$t("file"), options = dropdown_options, value = selected_file), style = "width: 200px"),
        shinyjs::hidden(
            div(
                id = ns("delete_settings_file_div_%widget_id%"),
                shiny.fluent::IconButton.shinyInput(ns("delete_settings_file_%widget_id%"), iconProps = list(iconName = "Delete")),
                style = "margin-top: 26px;", class = "widget_icon"
            )
        ),
        style = "display: flex; gap: 5px; margin: 5px 10px;"
    )
)

Prenons d’abord les deux derniers blocs, affichés par défaut.

# Sidenav
div(
    id = ns("settings_files_sidenav_%widget_id%"),
    shiny.fluent::IconButton.shinyInput(ns("create_settings_file_%widget_id%"), iconProps = list(iconName = "Add"), title = i18np$t("create_settings_file"), style = "margin: 0"),
    class = "widget_icon",
    style = "border-right: solid grey 0.5px; padding-left: 5px;"
),

# Dropdown and delete button
div(
    div(shiny.fluent::Dropdown.shinyInput(ns("settings_file_%widget_id%"), label = i18np$t("file"), options = dropdown_options, value = selected_file), style = "width: 200px"),
    shinyjs::hidden(
        div(
            id = ns("delete_settings_file_div_%widget_id%"),
            shiny.fluent::IconButton.shinyInput(ns("delete_settings_file_%widget_id%"), iconProps = list(iconName = "Delete")),
            style = "margin-top: 26px;", class = "widget_icon"
        )
    ),
    style = "display: flex; gap: 5px; margin: 5px 10px;"
)

Ce code nous permet d’afficher le bouton “+” latéralement pour afficher un fichier et le menu déroulant permettant de choisir le fichier.

Nous avons ensuite le modal permettant de créer un fichier, avec le bouton permettant de fermer le modal, l’input de texte et le bouton permettant de valider l’ajout du fichier.

# Add settings file modal
shinyjs::hidden(
    div(
    id = ns("add_settings_file_modal_%widget_id%"),
    div(
        div(
            tags$h1(i18np$t("create_settings_file"), style = "font-size: 14px;"),
            shiny.fluent::IconButton.shinyInput(ns("close_add_settings_file_modal_%widget_id%"), iconProps = list(iconName = "ChromeClose")),
            style = "display: flex; justify-content: space-between;",
            class = "small_close_button"
        ),
        div(shiny.fluent::TextField.shinyInput(ns("settings_file_name_%widget_id%"), label = i18np$t("file_name")), style = "width: 200px;"),
        div(
            shiny.fluent::PrimaryButton.shinyInput(ns("add_settings_file_%widget_id%"), i18np$t("add")),
            style = "position: absolute; right: 10px; bottom: 8px;"
        ),
        style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
    ),
    style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0;  width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
    )
)

Et finalement, le code du modal permettant de confirmer la suppression du fichier.

# Delete settings file modal
shinyjs::hidden(
    div(
    id = ns("delete_settings_file_modal_%widget_id%"),
    div(
        tags$h1(i18np$t("delete_settings_file_title"), style = "font-size: 14px;"),
        tags$p(i18np$t("delete_settings_file_text")),
        div(
            shiny.fluent::DefaultButton.shinyInput(ns("close_file_deletion_modal_%widget_id%"), i18np$t("dont_delete")),
            div(shiny.fluent::PrimaryButton.shinyInput(ns("confirm_file_deletion_%widget_id%"), i18np$t("delete")), class = "delete_button"),
            style = "position: absolute; right: 10px; bottom: 8px; display: flex; gap: 5px;"
        ),
        style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
    ),
    style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0;  width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
    )
)

Passons maintenant à la partie backend de notre plugin.

Serveur / backend

Invalidation des observers

Pensez à systématiquement ajouter une balise %req% au début des observers, ce qui permet d'invalider les anciens observers.
Pensez également à ajouter un message avec if (debug) ... cat(). Ceci permet de localiser les bugs à partir du fichier de log.


Voici le contenu du fichier server.R en entier.

# Server - Main file

# Tabs

## All tabs
tabs <- c("figure", "figure_settings", "code", "general_settings")

observeEvent(input$current_tab_trigger_%widget_id%, {
    %req%
    if (debug) cat(paste0("\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$current_tab_trigger"))
    
    tab <- input$current_tab_%widget_id%
    
    tryCatch({
        if (tab == "general_settings") shinyjs::hide("figure_settings_code_div_%widget_id%")
        else shinyjs::show("figure_settings_code_div_%widget_id%")
        
        sapply(paste0(setdiff(c("figure_settings", "code", "general_settings"), tab), "_div_%widget_id%"), shinyjs::hide)
        shinyjs::hide("settings_files_div_%widget_id%")
        
        if (tab != "code" | (tab == "code" & "projects_console_access" %in% user_accesses)) shinyjs::show(paste0(tab, "_div_%widget_id%"))
        
        if (tab %in% c("figure_settings", "code")){
            if (length(input$figure_and_settings_side_by_side_%widget_id%) > 0) if (input$figure_and_settings_side_by_side_%widget_id%) shinyjs::show("figure_div_%widget_id%")
            else shinyjs::hide("figure_div_%widget_id%")
            
            shinyjs::show("figure_settings_code_sidenav_%widget_id%")
            
            if (tab == "figure_settings") anti_tab <- "code"
            else anti_tab <- "figure_settings"
            
            shinyjs::runjs(paste0("
                var figureSettingsDiv = document.getElementById('", id, "-figure_settings_div_%widget_id%');
                var codeDiv = document.getElementById('", id, "-code_div_%widget_id%');
                
                if ('", tab, "' === 'figure_settings') {
                    figureSettingsDiv.style.flexBasis = codeDiv.style.flexBasis;
                } else {
                    codeDiv.style.flexBasis = figureSettingsDiv.style.flexBasis;
                }
            "))
        }
        else {
            shinyjs::hide("figure_settings_code_sidenav_%widget_id%")
            if (tab != "figure") shinyjs::hide("figure_div_%widget_id%")
        }
        
        # Prevent a bug with scroll into ace editor
        shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);")
        
    }, error = function(e) cat(paste0("\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

# Figure settings
%import_script('server_figure_settings.R')%

# Code
%import_script('server_code.R')%

# Settings files
%import_script('server_settings_files.R')%

# General settings
%import_script('server_general_settings.R')%

Backend des onglets

Le code permettant d’afficher ou de masquer chaque onglet est le suivant :

## All tabs
tabs <- c("figure", "figure_settings", "code", "general_settings")

observeEvent(input$current_tab_trigger_%widget_id%, {
    %req%
    if (debug) cat(paste0("\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$current_tab_trigger"))
    
    tab <- input$current_tab_%widget_id%
    
    tryCatch({
        if (tab == "general_settings") shinyjs::hide("figure_settings_code_div_%widget_id%")
        else shinyjs::show("figure_settings_code_div_%widget_id%")
        
        sapply(paste0(setdiff(c("figure_settings", "code", "general_settings"), tab), "_div_%widget_id%"), shinyjs::hide)
        shinyjs::hide("settings_files_div_%widget_id%")
        
        if (tab != "code" | (tab == "code" & "projects_console_access" %in% user_accesses)) shinyjs::show(paste0(tab, "_div_%widget_id%"))
        
        if (tab %in% c("figure_settings", "code")){
            if (length(input$figure_and_settings_side_by_side_%widget_id%) > 0) if (input$figure_and_settings_side_by_side_%widget_id%) shinyjs::show("figure_div_%widget_id%")
            else shinyjs::hide("figure_div_%widget_id%")
            
            shinyjs::show("figure_settings_code_sidenav_%widget_id%")
            
            if (tab == "figure_settings") anti_tab <- "code"
            else anti_tab <- "figure_settings"
            
            shinyjs::runjs(paste0("
                var figureSettingsDiv = document.getElementById('", id, "-figure_settings_div_%widget_id%');
                var codeDiv = document.getElementById('", id, "-code_div_%widget_id%');
                
                if ('", tab, "' === 'figure_settings') {
                    figureSettingsDiv.style.flexBasis = codeDiv.style.flexBasis;
                } else {
                    codeDiv.style.flexBasis = figureSettingsDiv.style.flexBasis;
                }
            "))
        }
        else {
            shinyjs::hide("figure_settings_code_sidenav_%widget_id%")
            if (tab != "figure") shinyjs::hide("figure_div_%widget_id%")
        }
        
        # Prevent a bug with scroll into ace editor
        shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);")
        
    }, error = function(e) cat(paste0("\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

Ce code correspond à l’UI de l’header, où il existe un bouton par onglet : pour afficher l’onglet de la figure, celui des paramètres de la figure, du code et des paramètres généraux du plugin.

Les noms des onglets sont contenus dans la variable tabs. Un observer sur input$current_tab_trigger_%widget_id% permet de déclencher le code lorsque l’on clique sur une icône du header. L’affichage des onglets du widget sera réalisé avec shinyjs::show() ou shinyjs::hide() pour afficher ou masquer les éléments de l’UI.

Il est prévu également d’afficher l’onglet figure si l’option pour afficher la figure et les paramètres / le code de la figure côte à côte est cochée, et que l’onglet paramètres de la figure ou code de la figure est sélectionné.

Le code Javascript exécuté dans shinyjs::runjs permet, lorsque la barre, qui sépare le div de la figure et celui soit du code soit des paramètres de la figure, est changée de position, d’appliquer la nouvelle largeur à l’autre élément (au div figure_settings si on a changé la largeur du div code et inversement).

Backend des paramètres de la figure

Le code de server_figure_settings.R est le suivant.

# Server - Figure settings

# Load figure settings

observeEvent(input$load_figure_settings_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$load_figure_settings"))
    
    # Update figure settings UI
    
    link_id <- input$settings_file_%widget_id%
    sql <- glue::glue_sql("SELECT name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db)
    figure_settings <- DBI::dbGetQuery(m$db, sql)
    
    if (nrow(figure_settings) > 0){
        sapply(figure_settings$name, function(name){
        
            value <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value)
            value_num <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value_num)
            
            # Update figure settings UI here with loaded figure settings
        })
    }
    
    # Run code if toggle is activated
    # if (length(input$run_code_at_settings_file_load_%widget_id%) > 0){
    #     if (input$run_code_at_settings_file_load_%widget_id%){
    #        shinyjs::delay(500, shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-display_figure_%widget_id%', Math.random());")))
    #    }
    # }
})

# Save current settings

observeEvent(input$save_params_and_code_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_params_and_code"))
    
    tryCatch({
    
        # If no settings file is selected, go to settings files management page
        if (length(input$settings_file_%widget_id%) == 0) shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random());"))
        
        if (length(input$settings_file_%widget_id%) > 0){
            
            link_id <- input$settings_file_%widget_id%
        
            # Delete old settings
            sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db))
            
            # Add new settings in db
            
            # new_data <- tibble::tribble(
            #     ~name, ~value, ~value_num,
            #     ...
            # )
            
            # new_data <-
            #     new_data %>%
            #     dplyr::transmute(
            #         id = get_last_row(m$db, "widgets_options") + 1:nrow(new_data), widget_id = %widget_id%, person_id = NA_integer_, link_id = link_id,
            #         category = "figure_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
            #     )
            
            # DBI::dbAppendTable(m$db, "widgets_options", new_data)
            
            # Notify user
            show_message_bar(output, "modif_saved", "success", i18n = i18n, ns = ns)
        }
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

Commençons par la première partie du code qui permet de charger les paramètres de la figure sauvegardés.

observeEvent(input$load_figure_settings_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$load_figure_settings"))
    
    # Update figure settings UI
    
    link_id <- input$settings_file_%widget_id%
    sql <- glue::glue_sql("SELECT name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db)
    figure_settings <- DBI::dbGetQuery(m$db, sql)
    
    if (nrow(figure_settings) > 0){
        sapply(figure_settings$name, function(name){
        
            value <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value)
            value_num <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value_num)
            
            # Update figure settings UI here with loaded figure settings
        })
    }
    
    # Run code if toggle is activated
    # if (length(input$run_code_at_settings_file_load_%widget_id%) > 0){
    #     if (input$run_code_at_settings_file_load_%widget_id%){
    #        shinyjs::delay(500, shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-display_figure_%widget_id%', Math.random());")))
    #    }
    # }
})

Le code se déclenche lorsqu’un nouvelle valeur est sélectionnée dans le dropdown load_figure_settings_%widget_id%. Nous pouvons récupérer l’ID du fichier de paramètres avec la variable input$load_figure_settings_%widget_id%.

Pour sauvegarder n’importe quelle information sur un widget (que ce soit dans les paramètres de la figure ou dans les paramètres généraux du plugin), nous enregistrons les données dans la table widgets_options, accessible avec la connexion m$db.

Regardons la structure de cette table :

Nom de la colonne Type Description
id Entier Identifiant unique pour chaque enregistrement
widget_id Entier Identifiant associé au widget
person_id Entier Identifiant du patient
link_id Entier Identifiant pour faire le lien entre deux enregistrements
category Texte Catégorie associée à l’enregistrement
name Texte Nom ou libellé de l’enregistrement
value Texte Valeur enregistrée sous forme de texte
value_num Numérique Valeur enregistrée sous forme numérique
creator_id Entier Identifiant du créateur de l’enregistrement
datetime Texte/Date Date et heure associées à l’enregistrement
deleted Entier Indicateur si l’enregistrement est supprimé : n’est plus utilisé

Vous pouvez utiliser cette table comme vous l’entendez, dans le sens où c’est vous qui choisissez les valeurs de category, de name, de value et de value_num.

La colonne link_id permet de rattacher un enregistrement à d’autres enregistrements.

Nous utilisons donc le code suivant pour charger les fichiers de sauvegarde :

SELECT name, value, value_num 
    FROM widgets_options 
    WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}

%widget_id% est remplacé par l’ID du widget, link_id contient l’ID du fichier de sauvegardé sélectionné. Nous allons donc récupérer toutes les entrées de la table de la catégorie figure_settings rattachés au fichier de sauvegade sélectionné.

Ensuite, chaque valeur des paramètres sera chargée avec la boucle sapply, les valeurs textuelles et numériques seront récupérées avec value et value_num, et nous allons pouvoir modifier les input avec ces nouvelles valeurs.

Le code commenté permet d’exécuter le code après le chargement d’un fichier de sauvegardé, si l’option est sélectionnée dans les paramètres généraux du plugin.

Voyons maintenant le code pour sauvegarder les paramètres.

observeEvent(input$save_params_and_code_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_params_and_code"))
    
    tryCatch({
    
        # If no settings file is selected, go to settings files management page
        if (length(input$settings_file_%widget_id%) == 0) shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random());"))
        
        if (length(input$settings_file_%widget_id%) > 0){
            
            link_id <- input$settings_file_%widget_id%
        
            # Delete old settings
            sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db))
            
            # Add new settings in db
            
            # new_data <- tibble::tribble(
            #     ~name, ~value, ~value_num,
            #     ...
            # )
            
            # new_data <-
            #     new_data %>%
            #     dplyr::transmute(
            #         id = get_last_row(m$db, "widgets_options") + 1:nrow(new_data), widget_id = %widget_id%, person_id = NA_integer_, link_id = link_id,
            #         category = "figure_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
            #     )
            
            # DBI::dbAppendTable(m$db, "widgets_options", new_data)
            
            # Notify user
            show_message_bar(output, "modif_saved", "success", i18n = i18n, ns = ns)
        }
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

Si aucun fichier de sauvegarde n’est sélectionné, l’utilisateur sera renvoyé à la page des fichiers de sauvegarde afin d’en créer un.

Les anciennes entrées correspond au fichier de sauvegarde sont supprimées et remplacées avec les nouveaux paramètres.

Backend du code et de la figure

Voici le code de server_code.R.

Il n’y a pas de fichier server_figure.R : l’onglet des paramètres de la figure permet de générer le code, qui est ensuite exécuté. Ces deux aspects (génération du code à partir des paramètres + exécution du code) sont regroupés au sein du même fichier.

# Server - Code

# Init code var
m$code_%widget_id% <- ""

# Prevent a bug with scroll into ace editor
shinyjs::delay(300, shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);"))

# Comment code
observeEvent(input$code_%widget_id%_comment, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_comment"))

    tryCatch({
        lines <- strsplit(input$code_%widget_id%, "\\n")[[1]]
        req(length(lines) > 0)
        
        start_row <- input$code_%widget_id%_comment$range$start$row + 1
        end_row <- input$code_%widget_id%_comment$range$end$row + 1
        
        for (i in start_row:end_row) if (startsWith(lines[i], "# ")) lines[i] <- substr(lines[i], 3, nchar(lines[i])) else lines[i] <- paste0("# ", lines[i])
        
        shinyAce::updateAceEditor(session, "code_%widget_id%", value = paste0(lines, collapse = "\\n"))
        
        shinyjs::runjs(sprintf("
            var editor = ace.edit('%s-rode');
            editor.moveCursorTo(%d, %d);
            editor.focus();
        ", id, input$code_%widget_id%_comment$range$end$row, input$code_%widget_id%_comment$range$end$column))
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

# Run all code with shortcut
observeEvent(input$code_%widget_id%_run_all, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_run_all"))

    m$code_%widget_id% <- input$code_%widget_id%
    shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-run_code_%widget_id%', Math.random());"))
})

# Run code when button is clicked
observeEvent(input$display_figure_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$display_figure"))
    
    tryCatch({
    
        # If current selected tab is figure settings when run code button is clicked, generate code from these settings
        if (length(input$current_tab_%widget_id%) == 0) current_tab <- "figure_settings"
        else current_tab <- input$current_tab_%widget_id%
        
        if (current_tab == "figure_settings"){
            
            # Code to generate code from figure settings
            
            # ...
            code <- ""
            
            # Update ace editor with generated code
            shinyAce::updateAceEditor(session, "code_%widget_id%", value = code)
            
            m$code_%widget_id% <- code
        }
        
        # Check if user has access
        else if ("projects_console_access" %in% user_accesses) m$code_%widget_id% <- input$code_%widget_id%
        shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-run_code_%widget_id%', Math.random());"))
        
    }, error = function(e){
        show_message_bar(output, "error_displaying_figure", "severeWarning", i18n = i18np, ns = ns)
        cat(paste0("\\n", now(), " - widget %widget_id% - input$display_figure - error = ", toString(e)))
    })
})

# Run code
observeEvent(input$run_code_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$run_code"))
    
    tryCatch({
        
        # Put here the code to execute when the "Run code" button is clicked
        
        # ...
        
        # Go to figure tab
        if (!input$figure_and_settings_side_by_side_%widget_id%) shinyjs::click("figure_button_%widget_id%")
        
    }, error = function(e){
        show_message_bar(output, "error_displaying_figure", "severeWarning", i18n = i18np, ns = ns)
        cat(paste0("\\n", now(), " - widget %widget_id% - input$display_figure - error = ", toString(e)))
    })
})

# Save code with shortcut
observeEvent(input$code_%widget_id%_save, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_save"))
    
    shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-save_params_and_code_%widget_id%', Math.random());"))
})

Le code dans l’observeur observeEvent(input$code_%widget_id%_comment, {}) permet de commenter le code dans l’éditeur avec le raccourci CTRL/CMD + SHIFT + C.

Le code peut être exécuté de deux façons :

  • avec le raccourci CTRL/CMD + SHIFT + C via l’observeur observeEvent(input$code_%widget_id%_run_all, {})
  • en cliquant sur le bouton “Afficher la figure” à gauche du widget, via l’observeur observeEvent(input$display_figure_%widget_id%, {})

Dans les deux cas, le code est copié dans la variable m$code_%widget_id% puis l’observeur observeEvent(input$run_code_%widget_id%, {}) est exécuté, permettant d’exécuter le code à l’origine de la figure et de basculer sur l’onglet de la figure.

Dans le cas où l’on se situe sur l’onglet des paramètres de la figure et que l’on clique sur le bouton “Afficher la figure”, le code est généré à partir des paramètres de la figure avant d’être exécuté. L’éditeur de code est mis à jour avec le code ainsi généré.

Enfin, nous avons un observeur pour sauvegarder le code depuis le raccourci (observeur observeEvent(input$code_%widget_id%_save, {})).

Backend des fichiers de sauvegarde

Le code de server_settings_files.R est le suivant.

# Server - Settings files management

settings_files_ui_style <- paste0(
    "display: inline-block; color: white; max-width: 250px; border-radius: 8px; padding: 1px 5px; align-items: center;",
    "height: 18px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; margin: 2px 5px;"
)

output$settings_files_ui_%widget_id% <- renderUI({
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer output$settings_files"))
    
    div(i18np$t("no_settings_file_selected"), style = paste0(settings_files_ui_style, "background-color: #606060ab;"))
})

## Show / hide settings file
observeEvent(input$show_settings_file_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$show_settings_file"))
    
    if (input$show_settings_file_%widget_id%) shinyjs::show("settings_files_ui_%widget_id%")
    else shinyjs::hide("settings_files_ui_%widget_id%")
})

## Show / hide settings files div
observeEvent(input$show_settings_files_tab_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$show_settings_files_tab"))
    
    sapply(c(paste0(tabs, "_div_%widget_id%"), "figure_settings_code_div_%widget_id%"), shinyjs::hide)
    shinyjs::show("settings_files_div_%widget_id%")
})

## Show add settings file modal
observeEvent(input$create_settings_file_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$create_settings_file"))
    
    shinyjs::show("add_settings_file_modal_%widget_id%")
})

## Close add settings file modal
observeEvent(input$close_add_settings_file_modal_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$close_add_settings_file_modal"))
    
    shinyjs::hide("add_settings_file_modal_%widget_id%")
})

## Confirm creation of settings file
observeEvent(input$add_settings_file_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$add_settings_file"))
    
    tryCatch({
        file_name <- input$settings_file_name_%widget_id%
        
        # Check if name if empty
        empty_name <- TRUE
        if (length(file_name) > 0) if (!is.na(file_name) & file_name != "") empty_name <- FALSE
        if (empty_name) shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = i18np$t("provide_valid_name"))
        else {
        
            shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = NULL)
            
            # Check if name is already used
            sql <- glue::glue_sql("SELECT name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name' AND LOWER(value) = {tolower(file_name)}", .con = m$db)
            name_already_used <- nrow(DBI::dbGetQuery(m$db, sql) > 0)
            
            if (name_already_used) shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = i18np$t("name_already_used"))
            else {
                
                new_id <- get_last_row(m$db, "widgets_options") + 1
                
                # Add settings file in database
                new_data <- tibble::tibble(
                    id = new_id, widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
                    category = "settings_files", name = "file_name", value = file_name, value_num = NA_real_, creator_id = m$user_id, datetime = now(), deleted = FALSE
                )
                DBI::dbAppendTable(m$db, "widgets_options", new_data)
                
                # Reset fields
                shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", value = "")
                
                # Update dropdown
                shiny.fluent::updateDropdown.shinyInput(session, "settings_file_%widget_id%", value = new_id)
                shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-reload_dropdown_%widget_id%', Math.random());"))
                
                # Close modal
                shinyjs::hide("add_settings_file_modal_%widget_id%")
                
                # Notify user
                show_message_bar(output, "new_settings_file_added", "success", i18n = i18np, ns = ns)
            }
        }
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

## Update dropdown
observeEvent(input$reload_dropdown_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$settings_file"))
    
    tryCatch({
        sql <- glue::glue_sql("SELECT id, value AS name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name'", .con = m$db)
        m$settings_filenames_%widget_id% <- DBI::dbGetQuery(m$db, sql)
        
        dropdown_options <- convert_tibble_to_list(m$settings_filenames_%widget_id%, key_col = "id", text_col = "name")
        shiny.fluent::updateDropdown.shinyInput(session, "settings_file_%widget_id%", options = dropdown_options)
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

## A settings file is selected
observeEvent(input$settings_file_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$settings_file"))
    
    tryCatch({
    
        # Show delete button
        shinyjs::show("delete_settings_file_div_%widget_id%")
    
        # Get file name
        file_id <- input$settings_file_%widget_id%
        filename <- m$settings_filenames_%widget_id% %>% dplyr::filter(id == file_id) %>% dplyr::pull(name)
        
        output$settings_files_ui_%widget_id% <- renderUI(div(filename, style = paste0(settings_files_ui_style, "background-color: #1d94ce;")))
        
        # Save that this file is selected
        sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings' AND name = 'selected_file_id'", .con = m$db))
        new_data <- tibble::tibble(
            id = get_last_row(m$db, "widgets_options") + 1, widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
            category = "general_settings", name = "selected_file_id", value = NA_character_, value_num = file_id, creator_id = m$user_id, datetime = now(), deleted = FALSE
        )
        print(new_data)
        DBI::dbAppendTable(m$db, "widgets_options", new_data)
        
        # Load saved settings
        shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-load_figure_settings_%widget_id%', Math.random());"))
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

## Open delete a settings file modal
observeEvent(input$delete_settings_file_%widget_id%, {
    %req%
    req(length(input$settings_file_%widget_id%) > 0)
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$delete_settings_file"))
    
    shinyjs::show("delete_settings_file_modal_%widget_id%")
})

## Close delete a settings file modal
observeEvent(input$close_file_deletion_modal_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$close_file_deletion_modal"))
    
    shinyjs::hide("delete_settings_file_modal_%widget_id%")
})

## Confirm settings file deletion
observeEvent(input$confirm_file_deletion_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$confirm_file_deletion"))
    
    tryCatch({
        file_id <- input$settings_file_%widget_id%
        
        # Delete row in db
        sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE id = {file_id}", .con = m$db))
        
        # Update dropdown
        shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-reload_dropdown_%widget_id%', Math.random());"))
        
        # Close modal
        shinyjs::hide("delete_settings_file_modal_%widget_id%")
        
        # Update selected settings file UI
        output$settings_files_ui_%widget_id% <- renderUI(div(i18np$t("no_settings_file_selected"), style = paste0(settings_files_ui_style, "background-color: #606060ab;")))
        
        # Hide delete button
        shinyjs::hide("delete_settings_file_div_%widget_id%")
        
        # Notify user
        show_message_bar(output, "settings_file_delete", "warning", i18n = i18np, ns = ns)
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

Vous devriez normalement avoir avec les explications précédentes les clefs pour comprendre le code ci-dessus, avec les commentaires fournis dans le code.

Le bloc avec output$settings_files_ui_%widget_id% permet d’afficher par défaut qu’il n’y a pas de fichier chargé, dans le header.

Voici le détail des observeurs :

  • observeEvent(input$show_settings_file_%widget_id%, {}) permet d’afficher ou de masquer le div avec le nom du fichier sélectionné, selon la valeur du paramètre dans les paramètres généraux du widget.
  • observeEvent(input$show_settings_files_tab_%widget_id%, {}) permet d’afficher ou de masquer l’UI de l’onglet permettant de gérer les fichiers de sauvegarde.
  • observeEvent(input$create_settings_file_%widget_id%, {}) permet d’afficher le modal de création d’un fichier de sauvegarde lorsque l’on clique sur le bouton “+”.
  • observeEvent(input$close_add_settings_file_modal_%widget_id%, {}) permet de masquer le modal lorsque l’on clique sur la croix pour quitter le modal.
  • observeEvent(input$add_settings_file_%widget_id%, {}) permet d’ajouter les informations du fichier créé dans la base de données et de réinitialiser les champs
  • observeEvent(input$settings_file_%widget_id, {}) permet de charger le code et les paramètres de la figure associés au fichier de sauvegarde sélectionné
  • observeEvent(input$delete_settings_file_%widget_id%, {}) permet d’ouvrir le modal de suppression du fichier de sauvegarde sélectionné
  • observeEvent(input$confirm_file_deletion_%widget_id%, {}) permet de supprimer le fichier de savuegarde de la base de données

Backend des paramètres généraux

Voici le code de server_general_settings.R :

# Server - General settings

# Settings / editor side-by-side with figure

observeEvent(input$figure_and_settings_side_by_side_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$figure_and_settings_side_by_side"))
    
    tryCatch({
        if (input$figure_and_settings_side_by_side_%widget_id%){
            shinyjs::runjs(paste0("
                $('#", id, "-figure_div_%widget_id%').css('flex-basis', '50%');
                $('#", id, "-figure_settings_div_%widget_id%').css('flex-basis', '50%');
                $('#", id, "-code_div_%widget_id%').css('flex-basis', '50%');
                
                if (!window.resizingInitialized_%widget_id%) {
                    var container = document.getElementById('", id, "-figure_settings_code_div_%widget_id%');
                    
                    var isResizing = false;
                    var lastDownX = 0;
                    
                    var leftPanel = container.querySelector('.left-panel');
                    var figureSettingsPanel = document.getElementById('", id, "-figure_settings_div_%widget_id%');
                    var codePanel = document.getElementById('", id, "-code_div_%widget_id%');
                    var resizer = container.querySelector('.resizer');
                    
                    function triggerResizeEvent() {
                        var event = new Event('resize');
                        window.dispatchEvent(event);
                    }
                    
                    resizer.addEventListener('mousedown', function(e) {
                        isResizing = true;
                        lastDownX = e.clientX;
                
                        document.addEventListener('mousemove', resizePanels);
                        document.addEventListener('mouseup', stopResizing);
                    });
                    
                    function resizePanels(e) {
                        if (!isResizing) return;
                        
                        var offsetLeftPanel = leftPanel.offsetWidth;
                        var offsetFigureSettingsPanel = figureSettingsPanel.offsetWidth;
                        var offsetCodePanel = codePanel.offsetWidth;
                        var deltaX = e.clientX - lastDownX;
                        
                        leftPanel.style.flexBasis = (offsetLeftPanel + deltaX) + 'px';
                        
                        figureSettingsPanel.style.flexBasis = (offsetFigureSettingsPanel - deltaX) + 'px';
                        codePanel.style.flexBasis = (offsetCodePanel - deltaX) + 'px';
                
                        lastDownX = e.clientX;
                        triggerResizeEvent();
                    }
                    
                    function stopResizing(e) {
                        isResizing = false;
                        document.removeEventListener('mousemove', resizePanels);
                        document.removeEventListener('mouseup', stopResizing);
                        triggerResizeEvent();
                    }
                }
                
                window.resizingInitialized_%widget_id% = true;
            "))
            shinyjs::hide("figure_button_div_%widget_id%")
            shinyjs::show("resizer_%widget_id%")
        }
        else {
            shinyjs::runjs(paste0("
                $('#", id, "-figure_div_%widget_id%').css('flex-basis', '100%');
                $('#", id, "-figure_settings_div_%widget_id%').css('flex-basis', '100%');
                $('#", id, "-code_div_%widget_id%').css('flex-basis', '100%');
            "))
            
            shinyjs::show("figure_button_div_%widget_id%");
            shinyjs::hide("resizer_%widget_id%");
        }
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

# Save general settings in db

observeEvent(input$save_general_settings_%widget_id%, {
    %req%
    if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_general_settings"))
    
    tryCatch({
    
        # Delete old rows
        sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings' AND name != 'selected_file_id'", .con = m$db))
        
        file_id <- input$settings_file_%widget_id%
        # new_data <- tibble::tibble(name = "selected_file_id", value = NA_character_, value_num = NA_integer_, link_id = file_id)
        new_data <- tibble::tibble(name = character(), value = character(), value_num = integer())
        
        # general_settings_vec <- c("show_saved_file", "figure_and_settings_side_by_side", "run_code_on_data_update", "run_code_at_settings_file_load")
        general_settings_vec <- c("show_settings_file", "figure_and_settings_side_by_side")
        
        sapply(general_settings_vec, function(name){
            toggle_value <- 0L
            input_name <- paste0(name, "_%widget_id%")
            if (length(input[[input_name]]) > 0) if (input[[input_name]]) toggle_value <- 1L
            new_data <<- new_data %>% dplyr::bind_rows(tibble::tibble(name = name, value = NA_character_, value_num = toggle_value))
        })
        
        new_data <-
            new_data %>%
            dplyr::transmute(
                id = get_last_row(m$db, "widgets_options") + 1:(length(general_settings_vec)), widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
                category = "general_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
            )
        
        DBI::dbAppendTable(m$db, "widgets_options", new_data)
        
    }, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})

Le premier observeur, observeEvent(input$figure_and_settings_side_by_side_%widget_id%, {}) contient le code Javascript qui permettra de gérer la taille des div côte à côte lorsque l’utilisateur cliquera sur la barre de séparation entre ces deux div.

L’observeur observeEvent(input$save_general_settings_%widget_id%, {}) permet de sauvegarder les paramètres généraux du widget automatiquement à chaque modification d’un toggle.

Conclusion

Vous êtes maintenant prêts à créer vos propres plugins !

La structure de base du template est somme toute assez complète.

Voici les étapes à suivre lors de la création d'un plugin à partir du template :

  • Créez un nouveau plugin en dupliquant le template
  • Codez dans un premier temps l'UI des paramètres du plugin et de la figure
  • Développez progressivement le backend de la création de la figure à partir des paramètres
  • Configurez le chargement des paramètres de la figure à partir des fichiers de sauvegarde
  • Seulement une fois que tout fonctionne, passez à la génération du code à partir des paramètres de la figure
  • Configurez enfin les paramètres généraux du widget
  • Une fois le plugin terminé, pensez à le documenter avec la description et à la partager !

11 - Catalogue de contenus

Accédez au contenu partagé d’autres équipes

Installer un élément

Pour commencer, rendez-vous sur la page “Catalogue de contenus”, depuis l’accueil ou depuis le menu en haut de la page.

En sélectionnant un point sur la carte, vous verrez apparaître sa description (correspondant au fichier README.md du dépôt git).

Cliquez sur le bouton “Afficher le contenu”, vous aurez accès au contenu partagé que propose cette équipe.

Vous pouvez choisir la catégorie de contenu à partir des onglets en haut à droite de l’écran, parmi :

  • Projets
  • Plugins
  • Sets de data cleaning
  • Sets de données

En cliquant sur un widget, vous accéderez à la description de ce contenu.

Vous pouvez installer ou mettre à jour l’élément simplement en cliquant sur le bouton “Installer” ou “Mettre à jour”.

Une fois l’élément installé, vous pouvez y accéder localement depuis la page correspondante (page Projets, Sets de données etc).

Pour revenir à la carte, cliquez sur “Dépôts git” en haut de l’écran.

12 - Partager

Partagez vos travaux pour contribuer à l’open science

Hébergement décentralisé

Pour partager du contenu (plugins, projets, scripts de data cleaning…), chaque centre doit disposer d’un dépôt Git dédié au contenu de LinkR qu’il souhaite partager.

LinkR offre deux méthodes pour mettre à jour ces dépôts Git avec le contenu que chaque équipe souhaite partager, que nous verrons dans le prochain paragraphe.

Ces dépôts Git sont ensuite mis en lien, grâce au catalogue de contenus, qui référence de façon centralisée les différents dépôts Git dans le fichier git_repos.csv).

Faire partie du réseau de partage LinkR

Pour faire partie du réseau de partage LinkR, il vous suffit d'envoyer une demande par mail à linkr-app@pm.me.
Votre dépôt Git sera ajouté dès que possible au catalogue de contenus.

Méthodes de partage

Tout d’abord, vous devez créer un dépôt Git vierge, nommé par exemple linkr-content dans votre profil Git.

Pour mettre à jour ce dépôt, il existe deux méthodes :

  • Vous pouvez réaliser les mises à jour vous-même, pour cela vous pouvez télécharger le contenu (plugin, projet…) au format zip depuis LinkR et pousser ce contenu sur votre dépôt
  • Vous pouvez également réaliser les mises à jour via une interface graphique, depuis LinkR

Les différents dossiers du dépôt seront créés lors de la première synchronisation de votre dépôt Git depuis LinkR.

Si vous souhaitez utiliser la méthode en faisant vous-même les mises à jour, vous devrez dans votre dépôt Git créer ces éléments :

  • README.md : le contenu du README sera affiché lorsqu’un utilisateur cliquera sur votre dépôt Git, sur la carte du catalogue de contenus
  • data_cleaning_scripts/ : il s’agit du dossier où se trouveront les scripts de data cleaning
  • datasets/ : hébergera les scripts permettant de charger des données (et non les données elles-mêmes)
  • plugins/ : dossier des plugins, que ce soit pour les données individuelles ou agrégées
  • projects/ : dossier des projets

Comme vous l’avez vu, chaque page des différents éléments (plugins, projets…) est organisée de la même façon, avec un onglet “Partage” en haut à droite de l’écran.

On y voit une partie dédieé à la synchronisation Git, et un bouton à gauche de l’écran permettant de télécharger le contenu localement.

Synchronisation avec l’interface graphique

Créer un token d’accès

Si vous souhaitez utiliser la synchronisation avec l’interface graphique, vous devrez créer un token d’accès.

Vous pouvez retrouver la documentation ici :

Ajouter le dépôt Git sur LinkR

Ensuite, vous devez ajouter votre dépôt Git aux dépôts enregistrés sur votre instance de LinkR.

Pour cela, rendez-vous sur la page “Catalogue de contenus”.

Ici, deux solutions :

  1. Cliquez sur le bouton “+” pour ajouter un dépôt Git

Cela ouvrira un modal, dans lequel vous devrez renseigner :

  • Le nom du dépôt Git : c’est vous qui choisissez, c’est le nom qui apparaîtra dans la liste des dépôts Git sauvegardés sur votre instance
  • L’adresse URL : c’est l’adresse URL du dépôt Git
  • L’adresse URL des fichiers raw : c’est l’adresse URL que vous obtenez lorsque vous accéder à un fichier brut, sans passer par le prévisualiseur de l’hébergeur Git. Par exemple, pour Gitlab et Framagit, il suffit de rajouter -/raw/main/ pour la branche main

Une fois le dépôt le dépôt ajouté, il devient disponible sur la liste des dépôts localement sauvegardés, accessibles depuis ce bouton à gauche de la page.

Vous arriverez sur cette page, où sont listés sur tous les dépôts Git sauvegardés sur votre instance.

Vous pouvez supprimer les dépôts Git à l’aide de l’icône dans le cadre en bas à gauche de l’écran.

Edition du README

Notez que vous pouvez modifier le README.md depuis l’écran des dépôts Git sauvegardés.

Vous pouvez cliquer sur le bouton “Modifier le Readme” en haut à droite, au niveau de la description du dépôt Git.

Ceci va ouvrir un éditeur permettant de modifier le Readme.

Remarquez les deux boutons en haut à droite de l’éditeur.

Celui de gauche permet de générer le contenu du Readme : cela va créer le Markdown à partir du contenu actuellement disponible dans le dépôt Git, selon les descriptions de chaque élément.

Celui de droit permet d’exécuter le Markdown pour afficher la page HTML correspondance.

Sauvegardez ensuite les modifications avec le bouton de validation en haut à droite de l’écran.

Synchronisation du contenu local

Pour synchroniser du contenu local, après avoir ajouté un dépôt Git sur votre instance de LinkR comme on vient de le voir, rendez-vous sur l’onglet “Partage” de l’élément à partager.

Vous pouvez voir un menu déroulant “Dépôt Git” : choisissez le dépôt sur lequel vous souhaitez pousser votre contenu local.

Une fois le dépôt Git sélectionné, il sera copié localement, et il vous sera indiqué si le contenu que souhaitez synchroniser est :

  • Absent du dépôt Git : il vous sera proposé de l’ajouter
  • Présent sur le dépôt Git mais moins récent : il vous sera proposé de le mettre à jour
  • Présent sur le dépôt Git avec la même version

Un modal va s’ouvrir, où vous devrez rentrer votre token d’accès et le message de commit (facultatif).

Cliquez sur “Mettre à jour” : le tour est joué !

Mises à jour par vous-même

Vous pouvez également choisir de mettre à jour vous-même votre dépôt Git.

Pour cela, vous pouvez télécharger le contenu que vous souhaitez synchroniser sur la page “Partage” avec le bouton “Exporter le contenu” en haut à gauche de l’écran.

Cela va télécharger les fichiers de l’élément dans un fichier zip.

13 - Roadmap

Les prochaines étapes dans le développement de LinkR

Version 0.4 (courant 2025) :

  • Améliorer le partage du contenu (#122, #123)
  • Créer une page de catalogue de contenu centralisée sur le site web (#125)
  • Page “Fichiers du projet” : page permettant de créer, éditer et supprimer des fichiers contenus dans un projet (#128)
  • Intégrer les scripts de data cleaning dans les projets : choisir quels scripts exécuter, dans quel ordre (#129)
  • Page “Workflow” : une page permettant de configurer le workflow d’un projet : quels fichiers, quels scripts exécuter, quels widgets, dans quel ordre (#130)
  • Intégration des LLMs au sein de LinkR : page pour configurer le code permettant de charger chaque LLM local (et uniquement local, pour éviter les fuites de données sur des serveurs non HDS) (#8, #127)
  • Créer une interface graphique pour l’import des données (#97)
  • Créer une interface graphique pour la création de subsets (sélection de critères, opérateurs logiques etc) (#126)
  • Gestion de tâches de fond / programmation asynchrone (#70)

Plugins prioritaires à développer (fin 2024 - début 2025) :

  • Visualisation des actes (données individuelles) (#21)
  • Visualisation des diagnostics (données individuelles) (#22)

Scripts de data cleaning prioritaires à développer (mi-2025) :

  • SOFA (#1)
  • IGS-2 (#3)
  • Outliers poids et taille (#5)