Create a simple plugin

Creating a simple plugin to display data as a histogram

Plugin specifications

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

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

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

Next, what should our graphical interface look like?

We will divide the screen into two sections: on the left, we will display the histogram, and on the right, we will configure the figure’s parameters. This will include a dropdown menu to select the variable and a field to set the number of bins in the histogram.

On the server side, now.

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

To simplify, we will only allow the display of numerical data. Thus, we will restrict the display to the variable d$measurement. Refer to the console documentation for more details on OMOP data variables.

When we change the histogram’s number of bins, the updates should only apply after validation, to avoid unnecessary calculations. We will also need to set bounds for possible values.

Let’s summarize the specifications of our plugin:

  • UI:
    • Histogram visualization on the left side of the screen
    • Parameters on the right side of the screen:
      • Variable to display
      • Number of bins in the histogram, with upper and lower bounds
      • Validation of changes
  • Server:
    • Only allow data from the d$measurement variable
    • Adjust the number of bins in the histogram based on the input value
    • Trigger the figure code execution after the validation button is clicked

Create the plugin

Navigate to the plugins page from the top menu.

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

Choose a name, such as “Histogram.”

Select the type of data: the plugin can be used for individual data (patient by patient), aggregated data (a group of patients), or both. For this 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 using the InterHop template.

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

In the top-right corner, you will see that a plugin is divided into four tabs:

  • Summary: displays general information and the plugin description. This will be detailed in the last section: “Share the Plugin”.
  • Code: where we edit the scripts to create the plugin’s frontend and backend (see the next three sections)
  • Test: this tab allows you to test the plugin code with data
  • Share: this is where you can add the plugin to your Git repository to share it with the community

Plugin structure

Go to the Code tab.

By default, a plugin consists of these three files:

  • ui.R: contains the Shiny code for the user interface, detailed in the next section
  • server.R: contains the application’s backend, detailed in the “Server / backend” section
  • translations.csv: contains translations for the frontend and backend

UI - user interface / frontend

As seen in the diagram above, we want to split the plugin screen into two sections: 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 of our user interface code should be placed inside a tagList function, which combines HTML tags using the R library Shiny.

To place two div elements side by side, they must be wrapped in a div with the attribute style = "display:flex;".

tagList(
    div(
        div(
            # Each id is wrapped in the 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%;", # Displays the two div elements side by side
    )
)

Note that whenever an ID is assigned to an HTML element, it must include a %widget_id% tag. This will be replaced with the widget’s ID, ensuring unique IDs. Without unique IDs, issues arise if the same plugin is launched in two different widgets. In the case of duplicate IDs, the HTML page will fail to render.

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

Here, we’ve added borders to our div elements using border:dashed 1px; to visualize the divs, which are currently empty. We will remove these attributes later.

Click on the “Run plugin code” icon on the left side of the screen.

You will automatically be redirected to the “Test” tab, and you should see the following result.

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

Now let’s add our histogram.

To do this, we use the plotOutput function, which we will modify on the server side to display our plot.

div(
    id = ns("split_layout_left_%widget_id%"),
    plotOutput(ns("plot_%widget_id%")), # Always wrap IDs in ns() and include 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-hand div.

As mentioned earlier, we want three elements:

  • a dropdown menu to select the variable to display.
  • a numeric input to set the number of bins in the histogram.
  • a button to display the figure with these parameters.

We will use the shiny.fluent library, which is used for the entire user interface of LinkR and is based on Fluent UI.

Here are the functions we will 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 a 10 px space between the title and the dropdown
    div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
    # Dropdown menu with the concepts
    div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),

    # Numeric input to select the number of bins in the histogram
    # With a value of 50, a minimum of 10, and a 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 on “Run plugin code” again, and you should see the following result.

You may notice that the titles of the inputs are wrapped in the i18np$t function. This allows the elements to be translated based on the translations.csv file, which we will explore in the next section.

Translations

Translations should be added to the translations.csv file.

This file contains the following columns:

  • base: this is the keyword you will use in your code, which will be translated based on the selected language
  • en: the translation of the word into English
  • fr: the translation into French. Currently, only English and French are supported. Additional languages may be added in the future.

Click on the translations.csv file, and update 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

Run the code again. You should see the following result.

The keywords have been replaced with their French translations.

Now we will make everything dynamic by coding the backend!

Server / backend

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

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

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

  • concept_id: the ID of the concept, either standard (found on Athena) or non-standard (in this case, greater than 2000000000 / 2B)
  • concept_name: the name of the concept
  • domain_id: the name of the OMOP Domain, often corresponding to the OMOP table (e.g., the ‘Measurement’ domain for the d$measurement variable)
  • vocabulary_id: the name of the terminology corresponding to the concept
  • mapped_to_concept_id & merge_mapped_concepts: not used in the current version of LinkR

To test a plugin and enable the backend to work, a project containing data must be loaded. For example, launch the project used for the quick start.

Next, to simulate the creation of a widget, we will select concepts to test our plugin.

On the left side of the screen, click the “Select concepts” button.

This will open the same menu used to select concepts when creating a widget.

For this example, select the Heart rate concept from the LOINC terminology, then click “Validate.”

Let’s test it: open the server.R file and copy the following code:

print(selected_concepts)

Rerun the plugin code, and you should see the following result.

You can see the backend output appear at the bottom of the screen. This only happens during plugin testing, which helps facilitate debugging. This output is hidden when plugins are used within projects.

The concept Heart rate is displayed along with its concept_id.

Now, let’s write the code to update the dropdown menu for concepts.

# Adding a row with the 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))

# Converting concepts to a list format
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")

# Adding a delay to ensure the dropdown updates after it is 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 prevent errors if the dropdown menu is empty.

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

We add a 500 ms execution delay for the update using shinyjs::delay(). This ensures the dropdown is created in the UI before being updated.

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

Now, all that’s left is to display our figure.

We will use the observeEvent function, which triggers code upon detecting an event.

observeEvent(input$show_plot_%widget_id%, {

    # The code inside this function will execute
    # every time the button with the id 'show_plot_%widget_id%' is clicked
    # (i.e., the "Show plot" button)
})

Important

Always add the %req% tag at the beginning of an observeEvent.
This tag will be replaced with code that ensures previous observers are invalidated when the widget is updated.


When editing plugins, every time you click “Run plugin,” previously created observers will be invalidated, preventing conflicts.

Here are the steps for our code:

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

    # Always add this tag at the start of an observer
    %req%

    # Protect the code in case of an error with a tryCatch
    tryCatch({

        # 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) Check if a concept is selected and if the domain_id equals 'Measurement'
        if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){

            # 3) Ensure the tibble of data filtered by 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)) +
                    # Use the number of bins from our input$num_bins_%widget_id%
                    ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
                    ggplot2::theme_minimal() +
                    # Modify the X and Y axis labels
                    ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))

                no_data_available <- FALSE
            }
        }

        # Display an empty graph if no data is available
        if (no_data_available){
                plot <-
                    ggplot2::ggplot() +
                    ggplot2::theme_void() +
                    ggplot2::labs(title = i18np$t("no_data_available"))
        }
        
        # 5) Update the output
        output$plot_%widget_id% <- renderPlot(plot)

    # Error messages will appear in the R console
    }, error = function(e) cat(paste0("\n", now(), " - ", toString(e))))
})

Update 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 see the following result.

We are now visualizing the distribution of heart rate across all patients using the d$measurement variable.

Here are the three complete files:

tagList(
    div(
        div(
            id = ns("split_layout_left_%widget_id%"),
            plotOutput(ns("plot_%widget_id%")), # Always wrap IDs in ns() and include a %widget_id% attribute
            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 a 10 px space between the title and the dropdown
            div(strong(i18np$t("concept")), style = "margin-bottom:10px;"),
            # Dropdown menu with the concepts
            div(shiny.fluent::Dropdown.shinyInput(ns("concept_%widget_id%")), style = "width:300px;"), br(),
        
            # Numeric input to select the number of bars in the histogram
            # With a value of 50, a minimum of 10, and a 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%;", # Displays the two div elements side by side
    )
)
# Adding a row with the 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))

# Converting concepts to a list format
concepts <- convert_tibble_to_list(concepts, key_col = "concept_id", text_col = "concept_name")

# Adding a delay to ensure the dropdown updates after it is created
shinyjs::delay(500, shiny.fluent::updateDropdown.shinyInput(session, "concept_%widget_id%", options = concepts, value = 0L))

observeEvent(input$show_plot_%widget_id%, {

    # Always add this tag at the start of an observer
    %req%

    # Protect the code in case of an error with a tryCatch
    tryCatch({

        # 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) Check if a concept is selected and if the domain_id equals 'Measurement'
        if (nrow(selected_concept) > 0 && selected_concept$domain_id == "Measurement"){

            # 3) Ensure the tibble of data filtered by 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)) +
                    # Use the number of bins from our input$num_bins_%widget_id%
                    ggplot2::geom_histogram(colour = "white", fill = "#377EB8", bins = input$num_bins_%widget_id%) +
                    ggplot2::theme_minimal() +
                    # Modify the X and Y axis labels
                    ggplot2::labs(x = selected_concept$concept_name, y = i18np$t("occurrences"))

                no_data_available <- FALSE
            }
        }

        # Display an empty graph if no data is available
        if (no_data_available){
                plot <-
                    ggplot2::ggplot() +
                    ggplot2::theme_void() +
                    ggplot2::labs(title = i18np$t("no_data_available"))
        }
        
        # 5) Update the output
        output$plot_%widget_id% <- renderPlot(plot)

    # Error messages will appear in the R console
    }, 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
Congratulations! You’ve just created your first plugin. You can now use it in a project and, most importantly, improve it.

The advantage of plugins is that anything possible in R or Python can be integrated into LinkR as a plugin.

This process can be challenging and requires knowledge of the Shiny library. To learn more about Shiny, we recommend the excellent book Mastering Shiny.

Plugins can quickly become complex, which is why we’ve created a development template to provide a solid and consistent base for coding more advanced plugins. We’ll explore this in the next chapter.

Share the plugin

Before sharing your plugin, it’s essential to document it so that users know what it does and how to use it.

To do this, go to the Summary page of your plugin. You’ll notice that the “Short Description” field on the left and the “Description” field on the right are empty.

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

You can now edit the plugin information, including the authors who contributed to its creation and a short description that will appear on the plugin page.

For example, we might provide the following short description for our plugin: “A plugin for visualizing structured data as a histogram”.

You can also edit the Full description by clicking the icon in the top right of the screen.

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

Once you’ve finalized the description, click the “Save updates” icon on the right side of the screen.

To confirm changes to the plugin information, click the “Save” icon on the left side of the screen.

Now that your plugin information is complete, you can share it via the “Share” tab at the top right of the screen, following this tutorial.