Creating a Simple Plugin

Creating a simple plugin to display data as a histogram

Introduction

Let's start by creating a simple plugin.


We will:

  • Define the plugin specifications: what do we want our plugin to offer users?
  • Create the plugin and understand its file structure
  • Create the user interface
  • Develop the server-side logic
  • Create translations to enable its use in multiple languages
  • See how to share the plugin with the user community

Plugin Specifications

We will create a graphical interface that will allow us to visualize the distribution of a variable in the form of a histogram.

We need to make an initial choice: is this a plugin for individual data (patient by patient) or aggregated data (on a group of patients)?

It is more common to want to visualize the distribution of a variable on a group of patients rather than on a single patient, so we will create an aggregated data plugin.

Next, what should our graphical interface look like?

We will split the screen in two: on the left we will visualize our histogram, and on the right we will be able to adjust the figure parameters, with a dropdown menu to choose the variable and a field to choose the number of bars in our histogram.

Server side now.

A histogram is not suitable for visualizing all types of data: we can visualize the distribution of numerical data, and categorical data provided that the number of categories is not too large.

To simplify, we will only allow the display of numerical data. We will therefore restrict the display to the OMOP measurement table.

When we change the value of the dropdown menu for the number of bars in the histogram, the modifications must be taken into account after validation, to avoid unnecessary calculations. We will also need to provide bounds for possible values.

Let’s summarize the specifications of our plugin:

  • User interface (UI) side:
    • Histogram visualization on the left side of the screen
    • Parameters on the right side of the screen
      • Variable to display
      • Number of bars composing the histogram, with lower and upper bounds
      • Validation of modifications
  • Server side:
    • Only allow data from the measurement table
    • Modify the number of bars in the histogram according to the value entered in the dropdown menu
    • Launch the figure code once the validation button is clicked

Creating the Plugin

Go to the plugins page from the menu at the top of the screen.

To create a plugin, click on the “+” icon on the left side of the screen.

Choose a name, for example “Histogram”.

Choose the type of data concerned: the plugin can be used on individual data (patient by patient), aggregated data (a group of patients), or both. For our example, we will choose “Aggregated data”.

It is also possible to copy an existing plugin: we will see this in the next section, when we create a plugin from the development template.

Once the plugin is created, select it. You will arrive at the plugin summary page.

You can see in the top right that a plugin is divided into four tabs:

  • Summary: here will be displayed the general information and description of your plugin. We detail this in the last paragraph: “Sharing the plugin”.
  • Code: this is where we will edit the scripts to create the frontend (user interface) and backend (server-side logic) of our plugin (see the next three paragraphs).
  • Test: this tab allows you to test the plugin code with data
  • Share: this is where you can add this plugin to your Git repository to share it with the rest of the community

Plugin Structure

Go to the Code tab.

A plugin is by default composed of these three files:

  • ui.R: contains the Shiny code for the user interface, which we will detail in the next paragraph
  • server.R: contains the backend of the application, which we will detail in the “Server / backend” paragraph
  • translations.csv: contains the translations for the frontend and backend

UI - User Interface / Frontend

As we saw in the diagram above, we want to split the plugin screen in two, with the figure on the left and the figure parameters on the right.

Start by clicking on the ui.R file on the left side of the screen.

All our user interface code must be within a tagList function, which allows you to put HTML tags together using the R Shiny library.

For two div elements to be side by side, they must themselves be in a div with the attribute style = "display: flex;".

tagList(
    div(
        div(
            # Each id is in an ns function, and includes a %widget_id% tag
            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%;", # Allows displaying the two divs above side by side
    )
)

Note that whenever we assign an ID to an HTML element, it must include a %widget_id% tag, which will be replaced by the widget ID, allowing for unique IDs. Indeed, it would be problematic if the same plugin was launched in two different widgets. In case of multiple IDs, the HTML page does not display.

Moreover, each ID is encapsulated in an ns function (see the chapter on Shiny modules in the Mastering Shiny book for more information).

We have added borders to our blocks (div) with border:dashed 1px; to visualize our blocks, which are currently empty. We will remove these attributes later.

Click on the “Run plugin” icon on the left side of the screen (or use the shortcut CMD/CTRL + SHIFT + ENTER).

You will be automatically switched to the “Test” tab and you should get this result.

We can clearly see the two div blocks side by side, with a dotted border.

Now let’s add our histogram.

We use the plotOutput function for this, which we will modify with renderPlot on the server side to display our plot.

div(
    id = ns("split_layout_left_%widget_id%"),
    plotOutput(ns("plot_%widget_id%")), # Always put IDs in ns() with a %widget_id% attribute
    style = "margin:10px 5px; width:50%; border:dashed 1px;"
)

Now let’s create the configuration for our figure, in the right block.

We said above that we wanted three elements:

  • a dropdown menu to choose the variable to display
  • a numeric field to choose the number of bars in the histogram to display
  • a button to display the figure with the chosen parameters

We will use the shiny.fluent library, which is the one used for the entire LinkR user interface, which uses Fluent UI.

Here are the functions to use for our three elements:

In plugins, you must prefix all functions with the library name. For example: shiny.fluent::Dropdown.shinyInput().

Let’s create the code to display the configuration elements for the figure.

div(
    # id with ns and %widget_id%
    id = ns("split_layout_right_%widget_id%"),

    # div containing the title, in bold (strong), with 10px space between title and dropdown
    div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
    # Dropdown menu with concepts
    div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),

    # Numeric field to choose the number of bars in the histogram
    # With a value of 50, minimum of 10 and maximum of 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(),

    # Button to display the figure
    shiny.fluent::PrimaryButton.shinyInput(ns("show_plot_%widget_id%"), i18np$t("show_plot")),
    style = "margin: 10px 5px; width:50%;"
)

Click “Run plugin” again, you should get this.

You can notice that the input titles have been put in an i18np$t function. This allows translating elements from the translation file (translations.csv), which we will see in the next paragraph.

Here is the complete code for the user interface:

tagList(
    div(
        div(
            # Each id is in an ns function, and includes a %widget_id% tag
            id = ns("split_layout_left_%widget_id%"),
            plotOutput(ns("plot_%widget_id%")),
            style = "margin:10px 5px; width:50%; border:dashed 1px;"
        ),
        div(
            # id with ns and %widget_id%
            id = ns("split_layout_right_%widget_id%"),
        
            # div containing the title, in bold (strong), with 10px space between title and dropdown
            div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
            # Dropdown menu with concepts
            div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),
        
            # Numeric field to choose the number of bars in the histogram
            # With a value of 50, minimum of 10 and maximum of 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(),
        
            # Button to display the 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%;", # Allows displaying the two divs above side by side
    )
)

Translations

Translations are to be inserted in the CSV file translations.csv.

It includes the following columns:

  • base: this is the keyword that you will insert in your code and that will be translated according to the selected language
  • en: this is the translation of the word in English
  • fr: French translation. For now, only English and French have been developed. It will be possible to add other languages in the future.

Click on the translations.csv file, then complete it with the following translations.

base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure

Rerun the code. You should get this.

The keywords have been replaced by their French translation.

We will now make all this dynamic by coding the backend!

Server-side logic / backend

Without the backend, the graphical interface is static, nothing happens when I click on the buttons.

As we saw in the documentation page for creating widgets, when we create a widget, we select the plugin to use as well as the concepts.

The selected concepts will be found in the selected_concepts variable, which includes the following columns:

  • concept_id: the concept ID, either standard (to be found on Athena), or non-standard (in this case, greater than 2000000000 / 2B)
  • concept_name: the concept name
  • domain_id: the name of the OMOP Domain, which often corresponds to the OMOP table (‘Measurement’ domain for the measurement table)
  • vocabulary_id: the name of the terminology corresponding to the concept

To be able to test a plugin and make the backend work, you need to load a project containing data. Launch for example the project used for setup (LinkR Demo).

Then, to simulate widget creation, we will select concepts to test our plugin.

You have a “Select concepts” button on the left side of the screen.

This will open the same menu as when you choose concepts when creating a widget.

Select for example the Heart rate concept from the LOINC terminology, then click “Validate”.

Let’s test: open the server.R file, and copy this code:

print(selected_concepts)

Rerun the plugin code, you should get this.

You see the backend output appear at the bottom of the screen. This is only the case with plugin testing, which makes debugging easier. This output is hidden when plugins are used in projects.

So we can clearly see the Heart rate concept appear with its concept_id.

We will now write the code to update the concepts dropdown menu.

# Adding a row with values 0 / "none"
concepts <-
    tibble::tibble(concept_id = 0L, concept_name = i18np$t("none")) %>%
    dplyr::bind_rows(selected_concepts %>% dplyr::select(concept_id, concept_name))

# We convert the concepts to list format
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")

# We establish a delay, so that the dropdown updates after being created
shinyjs::delay(500, shiny.fluent::updateDropdown.shinyInput(session, "concept_%widget_id%", options = concepts, value = 0L))

Update translations.csv to add the translation for 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

Several things to note.

We add a row with an empty concept, ’none’, which will help avoid errors if the dropdown menu is empty.

We use the convert_tibble_to_list function, which converts a tibble to a list, necessary to be integrated into a shiny.fluent input. The arguments will be key_col for the column containing the concept code (‘concept_id’), and text_col for the column containing the text (‘concept_name’).

We add an execution delay for the update, with shinyjs::delay(), which is 500 ms. This ensures that the dropdown has been created in the UI before updating it.

Execute this code, you should now have a dropdown menu with the concepts we selected (in this case Heart rate).

We just need to display our figure.

We will use the observe_event function, which is a modified version of the observeEvent function from Shiny, with the difference that this function prevents the application from crashing in case of an error in the observer code, and adds error messages to the application log.

This function will trigger the code after detecting an event.

observe_event(input$show_plot_%widget_id%, {

    # The code in this function will be executed
    # each time I click on the button with id 'show_plot_%widget_id%'
    # (so the "Show plot" button)
})

In the case of plugin editing, each time you click “Run plugin”, previously created observers will be invalidated, which avoids conflicts.

Here are the steps of our code:

    1. Retrieve the selected concept from the dropdown menu
    1. Ensure that the concept belongs to a domain that can be displayed as a histogram. For simplicity, we will select only the ‘Measurement’ domain.
    1. Ensure that the data tibble, filtered with the selected concept, is not empty
    1. Create the code for our histogram with ggplot
    1. Update our output
observe_event(input$show_plot_%widget_id%, {

    # 1) Retrieve the selected concept from the dropdown menu
    selected_concept <-
        selected_concepts %>%
        dplyr::filter(concept_id == input$concept_%widget_id%)

    no_data_available <- TRUE

    # 2) Is there a concept selected and is the domain_id equal to 'Measurement'?
    if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){

        # 3) Ensure that the data tibble filtered on this concept is not empty
        data <-
            d$measurement %>%
            dplyr::filter(measurement_concept_id == selected_concept$concept_id)

        if(data %>% dplyr::count() %>% dplyr::pull() > 0){

            # 4) Create the histogram code
            plot <-
                data %>%
                ggplot2::ggplot(ggplot2::aes(x = value_as_number)) +
                # We take into account the number of bars from our variable input$num_bins_%widget_id%
                ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
                ggplot2::theme_minimal() +
                # We modify the X and Y axis titles
                ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))

            no_data_available <- FALSE
        }
    }

    # Empty graph if no data available
    if (no_data_available){
            plot <-
                ggplot2::ggplot() +
                ggplot2::theme_void() +
                ggplot2::labs(title = i18np$t("no_data_available"))
    }
    
    # 5) Output update
    output$plot_%widget_id% <- renderPlot(plot)
})

Update the translations.

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

You should have this.

We visualize the distribution of heart rate among all patients, from the d$measurement variable.

Here are the three complete files:

tagList(
    div(
        div(
            # Each id is in an ns function, and includes a %widget_id% tag
            id = ns("split_layout_left_%widget_id%"),
            plotOutput(ns("plot_%widget_id%")),
            style = "margin:10px 5px; width:50%; border:dashed 1px;"
        ),
        div(
            # id with ns and %widget_id%
            id = ns("split_layout_right_%widget_id%"),
        
            # div containing the title, in bold (strong), with 10px space between title and dropdown
            div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
            # Dropdown menu with concepts
            div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),
        
            # Numeric field to choose the number of bars in the histogram
            # With a value of 50, minimum of 10 and maximum of 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(),
        
            # Button to display the 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%;", # Allows displaying the two divs above side by side
    )
)
# Adding a row with values 0 / "none"
concepts <-
    tibble::tibble(concept_id = 0L, concept_name = i18np$t("none")) %>%
    dplyr::bind_rows(selected_concepts %>% dplyr::select(concept_id, concept_name))

# We convert the concepts to list format
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")

# We establish a delay, so that the dropdown updates after being created
shinyjs::delay(500, shiny.fluent::updateDropdown.shinyInput(session, "concept_%widget_id%", options = concepts, value = 0L))

# Code that will be executed when the user presses the input show_plot_%widget_id%
observe_event(input$show_plot_%widget_id%, {

    # 1) Retrieve the selected concept from the dropdown menu
    selected_concept <-
        selected_concepts %>%
        dplyr::filter(concept_id == input$concept_%widget_id%)

    no_data_available <- TRUE

    # 2) Is there a concept selected and is the domain_id equal to 'Measurement'?
    if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){

        # 3) Ensure that the data tibble filtered on this concept is not empty
        data <-
            d$measurement %>%
            dplyr::filter(measurement_concept_id == selected_concept$concept_id)

        if(data %>% dplyr::count() %>% dplyr::pull() > 0){

            # 4) Create the histogram code
            plot <-
                data %>%
                ggplot2::ggplot(ggplot2::aes(x = value_as_number)) +
                # We take into account the number of bars from our variable input$num_bins_%widget_id%
                ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
                ggplot2::theme_minimal() +
                # We modify the X and Y axis titles
                ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))

            no_data_available <- FALSE
        }
    }

    # Empty graph if no data available
    if (no_data_available){
            plot <-
                ggplot2::ggplot() +
                ggplot2::theme_void() +
                ggplot2::labs(title = i18np$t("no_data_available"))
    }
    
    # 5) Output update
    output$plot_%widget_id% <- renderPlot(plot)
})
base,en,fr
concept,Concept to show,Concept à afficher
num_bins,Number of bins,Nombre de barres
show_plot,Show plot,Afficher la figure
none,None,Aucun
occurrences,Occurrences,Occurences
no_data_available,No data available,Pas de données disponibles

This is not simple and requires knowing how to manipulate the Shiny library. To learn more about Shiny, we recommend the excellent book Mastering Shiny.

Sharing the plugin

Before sharing the plugin, it is necessary to document it, so that users know what it is for and how to use it.

For this, go to the plugin Summary page. You see that the “Short description” fields on the left and “Description” on the right are empty.

Click on the “Edit information” button on the left side of the screen.

You can then modify the information related to the plugin, including the authors who helped in its design and a short description, which will be displayed on the plugins page.

We could for example give this short description to our plugin: “A plugin allowing visualization of structured data as a histogram”.

You can also edit the Complete description by clicking on the icon at the top right of the screen.

This will open an editor where you can write the description in Markdown format.

Once the description modifications are validated, click on the “Save modifications” icon on the right side of the screen.

To validate the plugin information modifications, click on the “Save” icon on the left side of the screen.

Now that your plugin information is well documented, you will be able to share it via the “Share” tab, at the top right of the screen, by following this tutorial.

Conclusion

You have just created your first plugin! You can now use it in a project and especially improve it.


The advantage with plugins is that everything that is doable in R or Python is integrable into LinkR as a plugin!


Plugins quickly become complex, which is why we created a development template to have a solid and common base for coding more complex plugins. This is what we will see in the next chapter.