safetyGraphics Shiny App - Custom Workflows

Jeremy Wildfire

2020-01-15

Overview

The safetyGraphics package is designed to serve as a flexible and extensible platform for analyzing clinical trial safety. This vignette shows how users can configure the safetyGraphics shiny app with workflows using customized charts and data.

Version 1.1 of the package allows users to preload their own charts and data sets for use in the safetyGraphics Shiny Application. Many types of clinical trial lab data are supported in conjunction with 4 types of charts - Static charts, Plotly charts, Shiny Modules and HTML widgets. As shown in the next section, loading data is simple, but adding custom charts is slightly more complex. Behind the scenes, all 4 chart types have two major technical components: code and metadata. As such, making a custom chart requires the user to create an R script that defines the chart’s code and metadata. We provide several examples below that describe the step-by-step process for creating a chart.

Adding Custom Data

Run safetyGraphicsApp(loadData=TRUE) to preload all data.frames from your current R session in a new instance of the safetyGraphics Shiny app. While users can still manually load additional .csv or .sas7bdat files in the app as needed, this programatic alternative allows for more flexibility for loading other data sources, and can help to automate situations where users want to load multiple files. For example, running the code below will preload several example data sets saved on our GitHub site.

#Load in data sets with different data standards
data_url <- 'https://raw.githubusercontent.com/SafetyGraphics/SafetyGraphics.github.io/master/pilot/'
SDTM<-read.csv(paste0(data_url,'SampleData_SDTM.csv'))
SDTM_Partial<-read.csv(paste0(data_url,'SampleData_PartialSDTM.csv'))
AdAM_Partial<-read.csv(paste0(data_url,'SampleData_PartialADaM.csv'))
NoStandard<-read.csv (paste0(data_url,'SampleData_NoStandard.csv'))
AdAM <- adlbc #loadData=TRUE overrides the default behavior where adlbc is automatically preloaded into the app

#Initialize the app with data from the session
safetyGraphicsApp(loadData=TRUE)

Clicking on the data tab shows the 5 pre-loaded data sets with their different data standards.

This data load process can easily be combined with the chart workflow described below (including customSettings.R programs) to create easily reusable, custom workflows.

Steps to Create a Custom Chart

There are 4 steps required to create a custom chart for use in safetyGraphics:

  1. Create custom chart code
  2. Add new settings to the app (if needed)
  3. Add the chart to the app
  4. Initialize the app

The remainder of this vignette is dedicated to 4 examples that describe the process for creating custom charts in more detail. Note that full code for all examples are available in Appendix 3 at the end of this vignette.

Example 1 - Hello World

To create a trivially simple custom chart, make a file called customSettings.R with the following code:

# Step 1 - Write custom chart code 
helloWorld <- function(data,settings){
  plot(-1:1, -1:1)
  text(runif(20, -1,1),runif(20, -1,1),"Hello World")
}

# Step 2 - Initialize Custom Settings 
# Not Applicable!

# Step 3 - Initialize the custom chart 
addChart( 
  chart=”hello_world”,
  main=”helloWorld",
  label=”Hello World”, 
)

Then initialize the app (Step 4) by running:

setwd('/path/to/the/file/above')
safetyGraphicsApp()

Once the app opens, click the charts tab to view the new custom “hello_world” chart.

Example 2 - Detailed Walkthrough

Our second example goes through the chart creation process step-by-step for a more realistic example. An understanding of the underlying infrastructure for safetyGraphics will help here, so we recommend reviewing the first introductory vignette before diving in.

Step 1 - Create custom chart code

The first, and most complicated, step is to write the code for the custom chart. It’s easiest to break this process down in to 3 smaller steps.

  1. Create customSettings.R file
  2. Make static code using sample data
  3. Covert static chart code to Function

Step 1.1 - Create customSettings.R file

First, create a file called customSettings.R and save it in a designated directory. All of the code in the following steps goes in this file. Note that when you run the app, custom medatadata files will be saved in the same directory as the customSettings.R file.

Step 1.2 - Make Static Code Using Sample Data

Next, write code that creates your chart using a sample data set (the adlbc data set included with safetyGraphics is a good option). Note that all common charting packages are supported including ggplot2, lattice and base R graphics. Our example makes a histogram showing the distribution of lab values:

library(safetyGraphics)
library(ggplot2)
ggplot(
    data = adlbc, 
    aes(x = PARAM, y = AVAL)
) +
 geom_boxplot(fill =‘blue’) +
 scale_y_log10() +
 theme_bw() +
 theme(
  axis.text.x = element_text(angle = 25, hjust = 1),    
  axis.text = element_text(size = 12), 
  axis.title = element_text(size = 12)
)

As expected, running this code creates a simple box plot.

Step 1.3 - Convert Static Chart Code to a Function

After the chart is working using the sample data, you need to update it to a function that will work with any data set that the user loads in the shiny app. Replace hard coded parameters with references to the settings defined in the safetyGraphics settings object as shown below.

This is the hardest part of creating a custom chart, so we’ve provided some additional notes about this process below. There is also a technical appendix at the end of this document that provides more details about the metadata/settings objects used in this step. Finally, feel free to ask ask us questions if you run in to problems.

  • The code to create the chart must be wrapped in a function that takes 2 parameters as inputs: data and settings as shown in line 1 above. When the chart function is called by Shiny, these parameters are generated dynamically within the app; data represents the user selection in the Data module, and settings represents the selections in the Settings Module.
  • The data parameter is saved as a data.frame, and a preview of the current data is conveniently available in the data tab.
  • The settings parameter is saved as a list and is slightly more complex. Each setting shown on the settings page has a unique ID (called a text_key in the package) that gives its position in the settings list. In our example, the “Measure column” setting has the ID measure_col and is accessed in the charting function via settings$measure_col. Additional technical documentation about the settings list is provided in Appendix 1.
  • Lines 2-7 above create a new data frame called mapped_data. This isn’t required, but it simplifies the code somewhat and helps to avoid non-standard evaluation in the chart function.
  • Note that we dynamically identify the Value and Measure columns in lines 4 and 5 by referencing the settings object. This code, as opposed to directly specifying a column name like data$PARAM, allows the chart to work with any data standard.
  • Line 9 initializes the chart using ggplot() with the newly derived mapped_data.
  • Line 13 introduces a custom setting - settings[["boxcolor"]] - so that the user can specify a color for the bars in the shiny app if desired. All custom settings must be initialized using the addSetting() function; this process is described in Step 2 below.
  • Finally, note that the process for defining custom “htmlwidget” and “shiny module” charts is slightly different than the sample code above; example 3 provides basic example for a shiny module, and htmlwidgets are discussed in Appendix 2.

Step 2 - Add custom settings to the App

Next, we’ll add any new settings to the app by calling the addSetting() function in our customSettings.R function.

First, we need to determine which settings are already defined. As noted above, our chart uses 3 settings: value_col, measure_col and box_color. In most cases you can just examine the settingsMetadata data frame, which contains all settings available in safetyGraphics, along with details about each. Either view it with view(settingsMetadata) or use code like this:

In some complex cases, it might be easier to examine an example settings object directly (rather than the data frame representation). To do this, you can create a shell settings object and then check to see if the settings exist:

As shown in the output above, our example uses 2 pre-loaded settings (measure_col and value_col) and one new setting (boxcolor). We add boxcolor to the metadata using the addSetting() function as shown below.

addSetting( 
  text_key="boxcolor", 
  label="Box Plot Color", 
  description="The color of the boxes",
  setting_type="character",
  setting_cat="appearance", 
  default="gray"
) 

You could repeat as needed for multiple settings. For full details about each parameter see ?addSetting, which matches up with the column names in ?settingsMetadata.

Step 3 - Add the chart to the App

Next, we need to add the chart itself to the app using addChart(). We’d add the example above as follows:

addChart( 
  chart="labdist", 
  main="labdist", 
  label="Lab Distribution - static", 
  requiredSettings=c("boxcolor","value_col","measure_col"),
  type="static”
 )

For full details about each parameter see ?addChart, which matches up with the column names in ?chartMetadata.

Step 4 - Initialize the app

Finally, make sure your working directory is set to the location of you customSettings.R and call safetyGraphicsApp(). The settings page for the new “Lab Distribution – Static” chart will look like this:

And here is the chart:

There are options that will let you save the custom code in other locations and/or automatically add a chart each time you run the app. See ?safetyGraphicsApp, ?addChart and ?addSettings for details.

Example 3 - Interactive Histogram

We can expand on our static boxplot from Example 2 by adding some interactivity. Placing our boxplot in a Shiny module will allow the user to make chart-specific aesthetic adjustments on the fly. In this example, we’ve added the ability to add/remove individual data points, transform the y-axis scale, and show/hide outliers:

This example expands on the static boxplot example in the following ways: - Most of the underlying code for manipulating the dataset and creating the ggplot2 figure is identical to the static boxplot example. However, we’ve added some conditional statements to modify the boxplot based on the user selections. - Instead of a single chart function, we now have two: the module UI function, and the module server function. The UI function has the same name as the server function, with _UI appended. These functions should be specified (or sourced from an external file) within customSettings.R. - data and settings are passed to the module server function as reactive objects.

The full code for the custom script is saved in Appendix 3.

Example 4 - Custom htmlwidget (Coming soon!)

Custom htmlwidgets are not currently supported via addChart() and addSetting()`, but we plan to add support in future release.

For now, please contact the package development team or file an issue on GitHub if you would like to add a custom htmlwidget to safetyGraphics, and we can discuss technical details.

Appendix 1 - Technical Details

This appendix provides a detailed technical description of the settings framework used in the application. As described in the introductory vignette, the settings Shiny module allows users to make a wide range of customizations to the charts for any given study. Understanding the underlying technical details of this settings customization process is perhaps the most complicated aspect of designing custom charts for safetyGraphics. This appendix is broken in to 2 sections, the first describes the structure of the settings themselves, the second describes the metadata used to generate the settings when the package is built.

Settings Structure

Behind the scenes, the settings are stored as a list, which is updated in real time as the user makes changes in the Shiny settings module. A small chunk of a typical settings list is shown below. Note that this was generated using generateSettings(standard="adam"). More details on this and other functions related to settings is provided below.

As described in the examples above, the current settings list is passed directly to the chart function whenever a user views a chart in the shiny app. Each control in the settings module is tied to a single component in the setting list using a unique key. That unique key defines the setting’s position in the list. You can see the unique key for any setting by hovering over the title for the control in the shiny app. As an example, the “ID column” control has a unique key of “id_col”, which is accessible in the settings list via settings$id_col, which would resolve to “USUBJID”, the default value for the ADaM data standard, in our sample settings above. The settings framework also supports nested settings. A double dash “–” indicates a level of nesting in the list, so a setting ID of “measure_values–ALT” would be accessed as settings[["measure_values"]][["ALT"]], which would resolve to “Alanine Aminotransferase (U/L)” in our sample settings.

You can see additional details about pre-loaded settings by viewing the safetyGraphics::settingsMetadata data file that contains the default settings for the shiny app. ?settingsMetadata provides detailed data specifications for the metadata file, which also match the options available in the addSetting() function used in the examples above.

The safetyGraphics package includes several functions specifically designed for use with settings objects. As mentioned above, you can generate a default settings list for a data standard using the generateSettings() function:

safetyGraphics::generateSettings()  # no standard
safetyGraphics::generateSettings(standard="ADaM") # ADaM standard

Other functions for working with settings (all used liberally by the shiny app) include: getRequiredSettings(), generateShell(), getSettingKeys(), getSettingValue(), getSettingsMetadata(), setSettingsValue(), trimSettings() and validateSettings(). The R documentation for each of these functions provides technical details and examples for common use cases. Note also that all of the charts included with the application have detailed standalone documentation referenced in the repo_url and settings_url fields found in the chartsMetadata file; for example, the hep-explorer’s configuration page is here and provides a lot of additional context about how each setting is used by the chart.

Finally, note that for htmlwidgets the settings list is converted to a json object with the following code:

settings_list <- list(...)
jsonlite::toJSON(
      settings_list,
      auto_unbox = TRUE,
      null = "null"
    )

Metadata framework

In general, there are 3 primary types of metadata used in the shiny app: chart metadata, setting metadata and data standard metadata. The metadata files that provide key information to the Shiny app and allow for the app to automatically be updated with new charts, settings, and standards.

The underlying technical framework for the metadata is somewhat complex. In general, we follow the recommendations from the Data chapter in Hadley Wickham’s R Packages book. Using that workflow, we combine 5 “raw” metadata files (saved in data-raw\) in to 3 data files that are available as part of the package (saved in data\ with documentation in R\). An R script to convert the 5 “raw” files to the 3 files in the package is saved as data-raw\csv-to-rda.R, which is re-run whenever the metadata framework is updated.

The addChart() and addSetting() functions (and their evil twins removeCharts() and removeSettings()) allow users to customize these metadata files without actually rebuilding the entire package. These functions simply edit, add or remove rows from the settingsMetadata and chartsMetadata files saved in data\ and then save local copies of the files that are used in place of the default versions. This customization is likely enough for most users, but developers looking to make permanent changes to the apps default must go a level deeper and edit the files saved in data-raw/. More detail about those raw files is provided below, and step-by-step instructions for creating a new default chart are provided in Appendix 2.

Appendix 2 - Step-by-Step Process for Contributing a New Default Chart to safetyGraphics Package

  1. Make a new branch of the safetyGraphics master repository.

  2. Create a new charting function using the guidelines in the examples above

  3. Drop the file containing the charting function in the inst/custom directory, under the subfolder that matches the chart type (static, plotly, or Shiny module).

  4. Update metadata
  1. Update package dependencies. If you’ve added a plotly chart:
  1. Update package documentation with devtools::document().

  2. Rebuild the R package, test out the Shiny app, and make a PR to the safetyGraphics repo.

Appendix 3 - Full Custom Scripts for Examples

Full Code for Example 1 - Hello World

# Step 1 - Write custom chart code 
helloWorld <- function(data,settings){
  plot(-1:1, -1:1)
  text(runif(20, -1,1),runif(20, -1,1),"Hello World")
}

# Step 2 - Initialize Custom Settings 
# Not Applicable!

# Step 3 - Initialize the custom chart 
addChart( 
  chart=”hello_world”,
  main=”helloWorld",
  label=”Hello World”, 
)

Full Code for Example 2 - Static Histogram

custom_location<-"customBoxplot/"

#####################################################################
# Step 1 - Write custom chart code
#####################################################################
labdist<-function(data,settings){
  mapped_data <- data %>%
    select(Value = settings[["value_col"]], Measure = settings[["measure_col"]])%>%
    filter(!is.na(Value))
  
  ggplot(data = mapped_data, aes(x = Measure, y = Value)) + 
    geom_boxplot(fill = settings[["boxcolor"]]) +
    scale_y_log10() +
    theme_bw() + 
    theme(axis.text.x = element_text(angle = 25, hjust = 1),
          axis.text = element_text(size = 12),
          axis.title = element_text(size = 12))
}

#####################################################################
# Step 2 - Initialize Custom Settings
#####################################################################
addSetting(
  text_key="boxcolor", 
  label="Box Plot Color", 
  description="The color of the boxes", 
  setting_type="character", 
  setting_cat="appearance", 
  default="gray", 
  settingsLocation=custom_location
)


#####################################################################
# Step 3 - Initialize the custom chart
#####################################################################
addChart(
  chart="labdist",
  main="labdist", 
  label="Lab Distribution - static",
  settingsLocation = custom_location,
  requiredSettings=c("boxcolor","value_col","measure_col"),
  type="static"
)

Full Code for Example 3 - Interactive Histogram

custom_location<-"customBoxplot/"

#####################################################################
# Step 1 - Write custom chart module code
#####################################################################
labdist_module_UI <- function(id) {
  ns <- NS(id) 
  tagList(
    checkboxInput(ns("show_points"), "Show points?", value=FALSE),
    checkboxInput(ns("show_outliers"), "Show outliers?", value=TRUE),
    selectInput(ns("scale"), "Scale Transform", choices=c("Log-10","None")),
    plotOutput(ns("labdist"), width = "1000px")
  )
}

labdist_module <- function(input, output, session, data, settings) {
  
  ns <- session$ns
  
  mapped_data <- reactive({
    data() %>%
      select(Value = settings()[["value_col"]],
             Measure = settings()[["measure_col"]])%>%
      filter(!is.na(Value)) 
  })
  
  output$labdist <- renderPlot({
    
    req(mapped_data())
    
    # set up the plot
    p <- ggplot(data = mapped_data(), aes(x = Measure, y = Value)) +
      theme_bw() +
      theme(axis.text.x = element_text(angle = 25, hjust = 1),
            axis.text=element_text(size=12),
            axis.title = element_text(size = 12))
    
    # add/remove outliers
    if (input$show_outliers){
      p <- p + geom_boxplot(fill = settings()[["boxcolor"]]) 
    } else {
      p <- p + geom_boxplot(fill = settings()[["boxcolor"]], outlier.shape = NA) 
    }
    
    # log-transform scale
    if (input$scale=="Log-10"){
      p <- p + scale_y_log10()
    }
    
    # show individual data points
    if (input$show_points){
      p <- p + geom_jitter(width = 0.2)
    }  
 
    p
  })
}

#####################################################################
# Step 2 - Initialize Custom Settings
#####################################################################
addSetting(
  text_key="boxcolor", 
  label="Box Plot Color", 
  description="The color of the boxes", 
  setting_type="character", 
  setting_cat="appearance", 
  default="gray", 
  settingsLocation=custom_location
)

#####################################################################
# Step 3 - Initialize the custom chart
#####################################################################
addChart(
  chart="labdist_module",
  main="labdist_module", 
  label="Lab Distribution - shiny module",
  settingsLocation = custom_location,
  requiredSettings=c("boxcolor","value_col","measure_col"),
  type="module"
)