Learn how to contribute useful code to the individual explorer tab, by topic.
This tutorial will walk you through how to add / display additional events on the Individual Explorer Tab. Currently, the app supports events from ADMH, ADAE, ADCM, ADSL, ADLB, and ADLBC. You’ll note that most are occurrence (OCCD) class data sets which are designed to be date-oriented with the exception of ADSL, ADLB, & ADLBC. Regardless of data set class, the events tab’s module extracts important dates that you (the developer) chooses from any data set for display.
When incorporating a new date / data set into the events module you’ll not only need to consider additional UI elements, but there are two R functions to become familiar with which are introduced below.
In mod_indvExp_ui.R
, the input called “checkGroup” &
“overlay_events” are introduced to the app’s user interface. The former
is for the Events
tab and the latter is for the
Visits
tab.
# Events Tab
checkboxGroupInput(
inputId = ns("checkGroup"),
label = "For additional patient events, load an AE, LB, LBC, CM, or MH",
choices = c(" "),
selected = NULL,
inline = TRUE
)
# Visits Tab
checkboxGroupInput(
ns("overlay_events"),
label = HTML("<br/>Overlay Events:"),
choices = c(" ")
)
Later, these values will get populated with server side logic that
depends on what the ADaMs the user uploads & the USUBJID selected.
That logic can be found in mod_indvExpPat.R
and a snapshot
is shown below. To summarize, the code checks if a supported ADaM
exists, and if it does, that ADaM is assigned an domain abbreviation to
be fed to the choices
argument for our two inputs:
checkGroup and overlay_events. Notice how overlay_events only receives a
subset of these choices, because Labs and Medical History aren’t
terribly valuable vertical lines to overlay on the Visits
tab.
# update checkboxes on both Events and Visits Tabs
# Initialize
checked1 <- NA
checked2 <- NA
checked3 <- NA
checked4 <- NA
checked5 <- NA
mh_names <- NA
# check for "adsl" (required), "adae", "adcm", and "adlb"
if ("ADSL" %in% loaded_adams()) { checked1 <- "DS" }
if ("ADAE" %in% loaded_adams()) { checked2 <- "AE" }
if ("ADCM" %in% loaded_adams()) { checked3 <- "CM" }
if ("ADLB" %in% loaded_adams()) { checked4 <- "LB" }
if ("ADMH" %in% loaded_adams()) {
# For ADMH, we want to create separate checkboxes for each type of
# Medical History Category that exist in the ADMH for the selected patient.
mh_names <-
datafile()[["ADMH"]] %>%
filter(USUBJID == input$selPatNo) %>%
distinct(MHCAT) %>%
pull()%>%
stringr::str_to_title()
checked5 <- paste0("MH_",sapply(strsplit(mh_names, " "), function(x){
toupper(paste(substring(x, 1, 1), collapse = ""))}))
}
# Combine all into a list
choices <- as.list(unlist(c(list(checked1,checked2,checked3,checked4,as.list(checked5)))))
names <- c("Milestones","Adverse Events","Concomitant Meds","Labs",mh_names) # ac: labels
# build a named list & Remove NULLs from the list
choices <- setNames(choices,names)
choices <- choices[!sapply(choices,is.na)]
# update the checkbox group
updateCheckboxGroupInput(
session = session,
inputId = "checkGroup",
choices = unlist(choices),
selected = NULL,
inline = TRUE)
#######################################
# Version for vlines on Visits Graph
#######################################
# You can only overlay Milestones, Adverse Events, and Con Meds (currently)
choices2 <- as.list(unlist(c(list(checked1,checked2,checked3))))
names2 <- names[1:3]
# Setting up colors too
vline_eventtype_cols <- my_cols[1:3] # my_cols defined in utils_strObjs.R
v_event_cols <- setNames(vline_eventtype_cols,names2)
dashes <- c("solid","dotted","dashed")
v_event_lines <- setNames(dashes,names2)
# build a named list & Remove NULLs from the list
choices2 <- setNames(choices2,names2)
choices2 <- choices2[!sapply(choices2,is.na)]
updateCheckboxGroupInput(
session = session,
inputId = "overlay_events",
choices = unlist(choices2), # optionally convert list to array
selected = NULL)
Notably, to add another date-oriented data frame into the mix, you
(the developer) will need to add an additional checkedx
where x
is the next sequential number of “checked” objects.
For example, if your data set is called “ADMD” where “MD” is the
abbreviation for “My Data”, then you’d need to add these chunks of code
into their respective positions above:
checked6 <- NA
# ...
if ("ADMD" %in% loaded_adams()) { checked6 <- "MD" }
# ...
choices <- as.list(unlist(c(list(checked1,checked2,checked3,checked4,as.list(checked5),checked6))))
names <- c("Milestones","Adverse Events","Concomitant Meds","Labs",mh_names, "My Data") # ac: labels
# ...
choices2 <- as.list(unlist(c(list(checked1,checked2,checked3,checked6))))
names2 <- c(names[1:3],"My Data")
Note that adjusting the objects choices2
&
names2
are optional; only adjust if you (the developer) or
subject matter experts (SMEs) deem it’s appropriate to overlay these
types of events on the plot in the Visits
tab.
Now that the groundwork has been established, it’s time to introduce
the function called org_df_events()
which has various (well
documented) arguments in the
mod_indvExp_fct_organizeEvent.R
file. This function takes a
data set and manipulates it into a standard form. After being
standardized, the app can (and will) combine it with other standardized
event data frames as necessary.
Essentially, this function uses lots of shiny inputs to execute the following (among other things):
In the example below, an Adverse Events (ADAE) is standardized using
org_df_events()
. The arguments that are pre-fixed with mi_
all come from shiny, but below they’ve been populated with examples to
show the typical values they accept. That said, when implementing a new
events data set, you (the developer) will need to concern yourself with
the first 6 arguments, all of which are well documented on the
function’s help page.
ae_dat <- org_df_events(
df_name = "ADAE"
, df_domain_abbr = "AE"
, df_desc = "Adverse Events"
, df_st_date_vars = c("AESTDT","ASTDT")
, event_desc_vars = c("AEDECOD","AESEV","AESER")
, event_desc = 'paste0(AEDECOD, ", AESEV: ", AESEV, ", AESER: ", AESER)'
, mi_input_checkbox = c("DS","AE")
, mi_input_apply_filter = FALSE
, mi_usubjid = "01-701-1015"
, mi_loaded_adams = c("ADSL","ADAE")
, mi_datafile = list(ADSL = adsl, ADAE = adae)
, mi_filtered_dat = filtered_dat
)
What’s absolutely imperative when calling this function is that the
df_domain_abbr
matches the values for the named-list of
choices passed to the updateCheckGroupInput()
function used
to update input$checkGroup
and
input$overlay_events
discussed in
UI Considerations
.
Running a publicly available ADAE data set (available through PHUSE’s
gitHub) through org_df_events()
, the output
ae_dat
might look something like the data frame below.
It will always contain these variables, but the amount of rows will
certainly vary. In the example output below, we see that the column
“EVENTTYP” was populated with the df_desc
argument. Later,
this will be the Event type label in the timevis
object
displayed on the Events
tab.
The next column is START which used the “ASTDT” column, which makes
it evident that the ADAE data frame we provided in
mi_datafile
(the list of data frames uploaded by the user),
didn’t have the preferred “AESTDT”, so the function selected the next
best date choice: “ASTDT”. It’s important that no matter how many
potential dates you provide in the df_st_date_vars
vector,
that at least one of them is a required field according to the current
ADaMIG.
Continuing on, the END variable exists only for date-oriented data
sets that would benefit from seeing a visual start and end of an event.
Typically, “events” happen in one day/ instant. However, an exception to
this would be medical history data where events could include medical
conditions that last several days, months, or years. As such, if you
(the developer) wish to implement new event data that requires showing
the start AND the end of an event, follow the precedent the Medical
History code establishes in the next section called
Build Events
. At the end of the day, you just want to
ensure the final output mimics the output above, for reasons that will
also become obvious in the next section.
tab_st and tab_en are just character versions of START and END which play better with DT output tables in the app.
Last, DECODE contains the event description (event_desc
)
you want attached to those dates. So, in this instance, the app will
actually display lots of information. Above, the function weaves
together content from 3 different variables using the paste function:
the adverse event itself, it’s severity and seriousness. However, a
single column that summarizes the event would be sufficient. If you (the
developer) want to supply your own static description for an event, you
should include single quotes within your double quotes. For example:
event_desc = "'Headache'"
. Doing so, will make all your
adverse events take the string ‘Headache’.
Now that we’ve organized our data into a standard format, we need to
combine the events the user selects into a singular data frame to be
used in the app. This task is performed in the
mod_indvExp_fct_buildEvents.R
file. By browsing this file,
you’ll witness the function just runs org_df_events()
on
all the applicable date-oriented data sets currently supported (ADMH,
ADAE, ADCM, ADSL, and ADLB). The only exception to these is the ADSL and
ADMH files, which require custom considerations when building the
standardized data frame shown above. Therefore, if you (the developer)
wish to add another date-oriented data set into the mix, add your new
org_df_events() function. Then, include the name of your data frame
(let’s say it’s called “md_rec”) when creating this list:
uni_list <- list(ds_rec, ae_rec, cm_rec, lb_rec, mh_rec, md_rec)
That’s all it takes! A few lines later, uni_list
is
rbind-ed into a single data frame (hence the need for uniformity) that
build_events()
will sort and return in the app. Once that
data is in the app, it will automatically flow through to the
timevis
object on the Events
tab or the
Visits
tab, if you (the developer) allowed it to do so.
The absolutely last steps is going to be just adjust a few colors
schemes on both the timeviz
object on the
Events
tab and the plot on the Visits
tab.
Why? Because consistency between the tabs will help the users quickly
recognize what type of data they’re observing when the color is always
consistent.
First, to update the colors on the timeviz
object on the
Events
tab, find the file called
utils_strObjs.R
in the R directory. It’s there you’ll find
the following code:
my_cols <- RColorBrewer::brewer.pal(7,"Pastel2")
css <- paste0("
.nav li a.disabled {
background-color: #aaa !important;
color: #333 !important;
cursor: not-allowed !important;
border-color: #aaa !important;
}
.vis-item.DS { background-color: ",my_cols[1],"; }
.vis-item.CM { background-color: ",my_cols[2],"; }
.vis-item.AE { background-color: ",my_cols[3],"; }
.vis-item.LB { background-color: ",my_cols[4],"; }
.vis-item.MH_MH { background-color: ",my_cols[5],"; }
.vis-item.MH_FDH { background-color: ",my_cols[6],"; }
.vis-item.MH_DH { background-color: ",my_cols[7],"; }
")
If you are adding another date-oriented data set, you’ll manually add a new color by increasing brewer.pal’s first argument by 1 (from 7 to 8 in this case). Then, you’ll add another row at the bottom. Continuing with our example, an ADMD would add a row that looks like this:
".vis-item.MH_DH { background-color: ",my_cols[8],"; }"
Similarly, for the Visits
tab, you’ll need to add the
name of the new event description to an object called
names2
, found in
R/mod_indvExpPatVisits_fct_plot.R
, assuming this type of
event is appropriate to overlay on such a plot. Here, we’ve actually
taken the same colors as found in the my_cols
vector above,
but tinted them slightly darker so that they are more visible on the
default grey ggplot2 background. Use your discretion whether you should
tint or not. When ready, add the hex digit color to the vector below
called vline_eventtype_cols, in the same element position as in
names2
.
# mod_indvExpPatVisits_fct_plot
names2 <- c("Milestones","Concomitant Meds","Adverse Events")
vline_eventtype_cols <- c("#80d1ad", "#f5ae7d", "#a8bde6") # dark version of my_cols
Congratulations! After completing the above steps, you should now have access to the pertinent event/dates information in the app. With any questions, please feel free to reach out to app authors/ maintainers listed in the package documentation.