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.
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.
There are 4 steps required to create a custom chart for use in safetyGraphics
:
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.
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.
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.
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.
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.
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.
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.
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.data
parameter is saved as a data.frame
, and a preview of the current data is conveniently available in the data tab.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.mapped_data
. This isn’t required, but it simplifies the code somewhat and helps to avoid non-standard evaluation in the chart function.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.ggplot()
with the newly derived mapped_data
.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.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
.
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
.
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.
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.
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.
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.
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"
)
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.
Charts Metadata (chartsMetadata.csv): This file informs the Shiny app about all of the chart modules that should be made available to the user. The structure of this file is 1 row per chart. Columns contain chart-specific details such as chart name, type, and size.
Settings Metadata for Charts (settingsMetadataCharts.csv): This file helps the Shiny app understand which settings are needed for the different charts. This information is incorporated in the Settings configuration and Reporting modules. The structure of this file is 1 row per unique setting and 1 column per chart. TRUE
/FALSE
values are used to indicate whether the setting is relevant to a given chart.
Settings Metadata (settingsMetadata.csv): This file informs the Shiny app about all of the possible settings across all of the charts. Specifically, the file helps to populate the Settings tab of the app, and it also ensures that settings are successfully handed off to the charts. The structure is 1 row per setting and the columns contain information such as a description of the setting, type of setting (e.g. data mapping), and whether the setting is optional or required.
Settings Defaults (generateSettingsMetadataDefaults.R): Ths file contains information about the default values for the settings (specifically for non-data mapping settings). A .R
file is used in place of a .csv
to preserve the R value type (numeric, string, list, etc) of the defaults. While all settings are represented (structure of 1 row per setting), all data mapping defaults should be set to NULL
and handled in the Standards metadata file.
Standards Metadata (standardsMetadata.csv): This file contains information about the default values for the settings for each data standard (specifically for data mapping settings). The file helps the Shiny app automatically detect data standards in uploaded files and automate settings configuration if the data is in a standard format. The structure of the file is 1 row per setting and 1 column per data standard. While all settings are represented, all rows corresponding to non-data mapping settings should be left blank.
Make a new branch of the safetyGraphics master repository.
Create a new charting function using the guidelines in the examples above
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).
data-raw/chartsMetadata.csv
data-raw/settingsMetadataCharts.csv
.data-raw/settingsMetadata.csv
data-raw/generateSettingsMetadataDefaults.R
and re-run the file.data-raw/settingsMetadataCharts.csv
.data_raw/standardsMetaData.csv
data-raw/csv_to_rda.R
to save the files to data/
.R/settingsMetadata.R
. Append chart_
to the name of your chart function/file and add it as an item in the list.library(plotly)
to top of global.R
file under inst/safetyGraphics_app
Update package documentation with devtools::document()
.
Rebuild the R package, test out the Shiny app, and make a PR to the safetyGraphics repo.
# 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”,
)
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"
)
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"
)