From 33c096dbe7628ae9e8244b0d0347553e49d33651 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Tue, 21 May 2019 10:13:40 -0400 Subject: [PATCH 01/39] update readme --- README.md | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7e148e01..73f8b312 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # safetyGraphics: Clinical Trial Safety Graphics with R -The **safetyGraphics** package provides a framework for evaluation of clinical trial safety in R. The initial release focuses on Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH). A prototype of the eDish interactive graphic is available [here](https://safetygraphics.github.io/safety-eDISH/test/) and is shown below. +The **safetyGraphics** package provides a framework for evaluation of clinical trial safety in R. It includes several safety-focused visualizations to empower clinical data monitoring. Chief among these is the Hepatic Explorer, based on the [Evaluation of the Drug-Induced Serious Hepatotoxicity (eDISH)](https://www.ncbi.nlm.nih.gov/pubmed/21332248) visualization. A demo of the Hepatic Explorer interactive graphic is available [here](https://safetygraphics.github.io/hep-explorer/test-page/example1/) and is shown below. -This package is being built in conjunction with the [safety-eDISH](https://github.com/safetyGraphics/safety-eDISH) javascript library. Both packages are under active development with beta testing and an initial release planned for early 2019. +This package is being built in conjunction with the [hep-explorer](https://github.com/SafetyGraphics/hep-explorer) javascript library. ![edishgif](https://user-images.githubusercontent.com/3680095/45834450-02b3a000-bcbc-11e8-8172-324c2fe43521.gif) @@ -23,7 +23,7 @@ The Shiny app provides a simple interface for: ```r devtools::install_github("SafetyGraphics/safetyGraphics") library("safetyGraphics") -chartBuilderApp() #open the shiny application +safetyGraphicsApp() #open the shiny application ``` ### Standalone charts @@ -33,17 +33,22 @@ Users can also initialize customized standalone charts with a few lines of code. ```r devtools::install_github("safetyGraphics/safetyGraphics") library("safetyGraphics") -eDISH(data=adlbc, - id_col = "USUBJID", - value_col = "AVAL", - measure_col = "PARAM", - visit_col = "VISIT", - visitn_col = "VISITNUM", - studyday_col = "ADY", - normal_col_low = "A1LO", - normal_col_high = "A1HI", - measure_values = list(ALT = "Alanine Aminotransferase (U/L)", - AST = "Aspartate Aminotransferase (U/L)", - TB = "Bilirubin (umol/L)", - ALP = "Alkaline Phosphatase (U/L)")) + +settings <- list( + id_col = "USUBJID", + value_col = "AVAL", + measure_col = "PARAM", + visit_col = "VISIT", + visitn_col = "VISITNUM", + studyday_col = "ADY", + normal_col_low = "A1LO", + normal_col_high = "A1HI", + measure_values = list(ALT = "Alanine Aminotransferase (U/L)", + AST = "Aspartate Aminotransferase (U/L)", + TB = "Bilirubin (umol/L)", + ALP = "Alkaline Phosphatase (U/L)") + ) + +chartRenderer(data=adlbc, settings=settings, chart="hepexplorer") + ``` From f04caffe59b1221ecd60af223146a895a7f116fc Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Tue, 21 May 2019 10:15:08 -0400 Subject: [PATCH 02/39] update vignette --- vignettes/shinyUserGuide.Rmd | 106 +++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/vignettes/shinyUserGuide.Rmd b/vignettes/shinyUserGuide.Rmd index e5c28c85..b0c31e1b 100644 --- a/vignettes/shinyUserGuide.Rmd +++ b/vignettes/shinyUserGuide.Rmd @@ -18,18 +18,40 @@ knitr::opts_chunk$set( # Overview -The `safetyGraphics` Shiny app provides an easy-to-use point-and-click interface to create shareable safety graphics for any study. You can run the app in your local R session using the code below, or visit a hosted version [here](https://becca-krouse.shinyapps.io/safetyGraphicsApp/). +The `safetyGraphics` Shiny app provides an easy-to-use point-and-click interface to create shareable safety graphics for any study. + +# Starting the Shiny App + +After opening RStudio and making sure you are running R version 3.5 or higher, the application can be started with just a few lines of code. In general, you'll want to load the latest stable release from CRAN: ``` -#Code to initialize shiny application -devtools::install_github("ASA-DIA-InteractiveSafetyGraphics/safetyGraphics") -library("safetyGraphics") +install.packages("safetyGraphics") +library("safetyGraphics") +safetyGraphicsApp() +``` + +To load the latest development release from GitHub with `devtools`: + +``` +install.packages("devtools") +library("devtools") +devtools::install_github("SafetyGraphics/safetyGraphics") +library("safetyGraphics") safetyGraphicsApp() ``` +## Loading Large Files + +By default, Shiny only allows users to load files smaller than 5mb. If you want to load a larger file, run this code before opening the app: + +``` +maxFileSize<-100 #Update 100 desired max file size in megabytes +options(shiny.maxRequestSize=(maxFileSize*1024^2)) +``` + # Typical Workflow -After opening the app, you will typically follow the workflow below. In short, you will load data (once), tweak settings and view charts (maybe more than once), and then export a snapshot of the charts for other users. +After opening the app, you will typically follow the workflow below. In short, you will load data (once), tweak settings and view charts (maybe more than once), and then export a snapshot of the charts for other users. @@ -45,28 +67,28 @@ When you open the app, you are taken to the Data Tab with "Data Upload" and "Dat -To load your own data, simply click the browse button and select a `.csv` or `.sas7bdat` data set. Once the file is loaded, select it in the list at the bottom of the "Data Upload Panel". Once selected, the "Data Preview" panel will update automatically (along with the Settings and Chart tabs). +To load your own data, simply click the browse button and select a `.csv` or `.sas7bdat` data set. Once the file is loaded, select it in the list at the bottom of the "Data Upload Panel". Once selected, the "Data Preview" panel will update automatically (along with the Settings and Chart tabs). The charts in the safetyGraphics app are specifically designed for clinical trial safety monitoring, and require laboratory datasets that contain one row per participant per time point per measure. Data mappings for two common [CDISC](https://www.cdisc.org/) data standards - [SDTM](https://www.cdisc.org/standards/foundational/sdtm) and [ADaM](https://www.cdisc.org/standards/foundational/adam) - are pre-loaded in the application. As described below, the app can automatically generate charts for data sets using these standards; other data sets require some user configuration. ## Update Settings -After loading your data, navigate to the Settings tab to customize the behavior of the charts. This tab includes panels for different types of chart settings. For example, the "Data Mapping" panel (shown below for the "Example Data" ADaM data set) can be used to specify the column that contains the unique subject ID, and on the more general "Appearance Settings" panel, there is an option to specify a warning message to be displayed when the chart loads. You can hover the mouse over any setting label to get more details. +After loading your data, navigate to the Settings tab to customize the behavior of the charts. This tab includes a Charts panel for selecting the charts you want to visualize and other panels for different types of chart settings. For example, the "Data Mappings" panel (shown below for the "Example Data" ADaM data set) can be used to specify the column that contains the unique subject ID, and on the more general "Appearance Settings" panel, there is an option to specify a warning message to be displayed when the chart loads. You can hover the mouse over any setting label to get more details. The small numbers to the right of the settings labels indicate the number of charts that use the relevant setting. Mousing over them presents a list of these charts. -When possible, items on the settings tab are pre-populated based on the data standard of the selected data set. See the Case Studies below for more details regarding working with non-standard data and adding customizations to the charts. +When possible, items on the settings tab are pre-populated based on the data standard of the selected data set. See the Case Studies below for more details regarding working with non-standard data and adding customizations to the charts. ## View Chart -Once the settings configuration is complete and a green check is shown in the navigation bar, navigate to the chart tab to view the chart. The chart tab updates automatically when settings are changed or new data is loaded. +Once the settings configuration is complete, click on the Charts tab to view a drop-down of the available charts. A green check will display by charts that are ready to be visualized and a red X will indicate that settings need to be changed in order to render the chart. Simply click one of the options to view it. The chart tab updates automatically when settings are changed or new data is loaded. More details about chart functionality will be documented in separate vignettes. ## Export Results -Finally, click the "Export Chart" button in the upper right corner to create a standalone copy of the chart using the current configuration. The export functionality combines the data, code, and settings for the chart in to a single file. In addition to the chart itself, the export includes a summary of the tool, and code to recreate the customized chart in R. +Navigate to the Reports tab to choose reports for export and click the "Export Chart(s)" button at the bottom to create a standalone copy of the charts using the current configuration. The export functionality combines the data, code, and settings for the charts in to a single file. In addition to the charts themselves, the export includes a summary of the tool, and code to recreate the customized charts in R. @@ -74,10 +96,10 @@ Finally, click the "Export Chart" button in the upper right corner to create a s ## Overview -When a new data file is loaded, the app will detect whether the dataset is formatted according to ADaM or SDTM data standards. If the uploaded dataset matches one of these standards, the settings tab will be pre-populated accordingly, and little or no custom user customization will be needed to generate a basic chart. However, no data standard is strictly required; the app also works with data in other formats. The only firm data requirements for the eDish chart are: +When a new data file is loaded, the app will detect whether the dataset is formatted according to ADaM or SDTM data standards. If the uploaded dataset matches one of these standards, the settings tab will be pre-populated accordingly, and little or no custom user customization will be needed to generate a basic chart. However, no data standard is strictly required; the app also works with data in other formats. The specific data columns required varies between charts. For example, the only firm data requirements for the Hepatic Explorer chart are: - The data must have one record per participant per timepoint per lab test. That is, the data should be long, not wide. -- The data must have columns for: +- The data must have columns for: - Unique Subject Identifier (ID Column) - Name of Measure or Test (Measure Column) - Numeric finding or result (Value Column) @@ -90,36 +112,36 @@ When a new data file is loaded, the app will detect whether the dataset is forma - Alkaline phosphatase (ALP) - Total Bilirubin -The app also supports data sets that partially match the pre-loaded data standards. The step-by-step instructions below outline how to create a chart for one such data set. - +The app also supports data sets that partially match the pre-loaded data standards. The step-by-step instructions below outline how to create a chart for one such data set. + ## Step-by-step ### 1. Open the App -Paste the following code into RStudio: +Paste the following code into RStudio: ``` #Code to initialize shiny application -devtools::install_github("ASA-DIA-InteractiveSafetyGraphics/safetyGraphics") -library("safetyGraphics") +install.packages("safetyGraphics") +library("safetyGraphics") safetyGraphicsApp() ``` ### 2. Load Data -Use the "Browse.." button on the data upload section of the data tab to load a non-standard data set. We'll use the `.csv` saved [here](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/raw/master/inst/eDISH_app/tests/partialSDTM.csv), but the process is similar for other data sets. Notice that once the data is loaded, the app will detect whether the data matches one of those pre-loaded standards, and a note is added to indicate whether a match is found. Our sample data is a partial match for the SDTM standard. Once you select the newly loaded data set, the app should look like the screen capture below. Note the red x's in the toolbar indicating that user customization is needed. +Use the "Browse.." button on the data upload section of the data tab to load a non-standard data set. We'll use the `.csv` saved [here](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/raw/master/inst/eDISH_app/tests/partialSDTM.csv), but the process is similar for other data sets. Notice that once the data is loaded, the app will detect whether the data matches one of those pre-loaded standards, and a note is added to indicate whether a match is found. Our sample data is a partial match for the SDTM standard. Once you select the newly loaded data set, the app should look like the screen capture below. Click on the Charts tab and note the red X's in the drop-down indicating that user customization is needed. ### 3. Select Columns -Next, click the "Settings" tab (with the red x) in the nav bar at the top of the page. The page should look something like this: +Next, click the "Settings" tab in the nav bar at the top of the page. The page should look something like this: -Behind the scenes, a validation process is run to check if the selected settings match up with the selected data set to create a valid chart. Green (for valid) and red (for invalid) status messages are shown after each label in the settings tab - you can hover the mouse over the status to get more details. +Behind the scenes, a validation process is run to check if the selected settings match up with the selected data set to create a valid chart. Green (for valid) and red (for invalid) status messages are shown after each label in the Settings tab - you can hover the mouse over the status to get more details. -As you can see, we've got several invalid settings with red status messages. To make a long story short, we now need to go through and update each invalid setting and turn its status message in to a green "ok". Once all of the individual settings are valid, the red Xs in the toolbar will turn to green checks, and the chart will be created. Let's hover over the first red error message to see the detailed description of the failed check: +As you can see, we've got several invalid settings with red status messages. We now need to go through and update each invalid setting and turn its status message in to a green "ok". Once all of the individual settings are valid, the red Xs in the Charts drop-down will turn to green checks, and the chart will be created. Let's hover over the first red error message to see the detailed description of the failed check: @@ -131,41 +153,41 @@ Now select LBTEST for Measure Column and LBDY for the Study Day Column option. Y -Now we need to fill in the 4 inputs beneath Measure Column. You may have noticed that there were no options available for these inputs when the page loaded. This is because these options are field level data that depend on the Measure Column option. Once you selected a Measure Column, the options for these inputs were populated using the unique values found in that data column. To fill them in, just type the first few letters of lab in the text box. For example, type "Alan" for the Alanine Aminotransferase value input and select the correct option. +Now we need to fill in the 4 inputs beneath Measure Column. You may have noticed that there were no options available for these inputs when the page loaded. This is because these options are field level data that depend on the Measure Column option. Once you selected a Measure Column, the options for these inputs were populated using the unique values found in that data column. To fill them in, just type the first few letters of lab in the text box. For example, type "Alan" for the Alanine Aminotransferase value input and select the correct option. -Repeat the process for the other 3 "value" inputs and viola, the red x changes to a green check, and our chart is ready. +Repeat the process for the other 3 "value" inputs and viola, the red x changes to a green check, and the Hepatic Explorer chart is ready. ### 4. View Chart -Now that we've got the data mapping complete, just click the Chart tab in the header to see the navigation bar at the top of the page. +Now that we've got the data mapping complete, just select "Hepatic Explorer" from the Chart tab drop-down. -The chart has lots of useful interactive features built in, but we'll cover those in a separate vignette. +The chart has lots of useful interactive features built in, but we'll cover those in a separate vignette. ### 5. Export Chart -To export the chart, click the "Export Chart" button in the upper right hand corner of the page. Your chart, saved as an `.html` file, will be downloaded to your machine. +To export the chart, click the Reports Tab, make sure that the Hepatic Explorer has a check by it, and click the "Export Chart(s)" button. Your chart, along with the other valid charts, will be saved in an `.html` file and downloaded to your machine. -Open the downloaded file in a new tab in your browser and you'll see two tabs. The "Chart" tab, will be identical to the chart shown above, with all of your customizations intact. The "Info" tab, shown below, has a brief description of the safetyGraphics package and source code that you can use to recreate the chart in R. +Open the downloaded file in a new tab in your browser and you'll see tabs for each of the charts and an "Info" tab. The Hepatic Explorer tab will be identical to the chart shown above, with all of your customizations intact. The "Info" tab, shown below, has a brief description of the safetyGraphics package and source code that you can use to recreate the charts in R. -The html file contains all of the data and code for the chart and is easy to share. Just send the file to the person you're sharing with, and tell them to open it in their web browser (just double-click the file) - they don't even need R. +The html file contains all of the data and code for the chartw and is easy to share. Just send the file to the person you're sharing with, and tell them to open it in their web browser (just double-click the file) - they don't even need R. -## Summary +## Summary -This case study shows how to create a shareable chart created using custom settings in just a few clicks. Continue reading to find out how to add customizations to your chart. +This case study shows how to create a shareable chart created using custom settings in just a few clicks. Continue reading to find out how to add customizations to your chart. # Case Study #2 - Adding Customizations ## Overview -Only the most basic settings used by the `safetyGraphics` displays are populated by default, but users can also add a wide variety of additional customization. We'll walk through a few common customizations for the eDish chart in this case study including: +Only the most basic settings used by the `safetyGraphics` displays are populated by default, but users can also add a wide variety of additional customization. We'll walk through a few common customizations for the Hepatic Explorer chart in this case study including: - Adding grouping variables - Adding filter variables @@ -176,41 +198,41 @@ Only the most basic settings used by the `safetyGraphics` displays are populated ### 1. Open the App -Just paste this code in to RStudio: +Just paste this code in to RStudio: ``` #Code to initialize shiny application -devtools::install_github("ASA-DIA-InteractiveSafetyGraphics/safetyGraphics") -library("safetyGraphics") +install.packages("safetyGraphics") +library("safetyGraphics") safetyGraphicsApp() ``` -We'll use the pre-loaded example data for this case study, so there is no need to load your own data file. +We'll use the pre-loaded example data for this case study, so there is no need to load your own data file. ### 2. Add Filters and Groups -The `SafetyGraphics` eDish chart offers native support for data-driven groups and filtering. Any data column can be used to add filter and grouping controls to the chart. One common use case is to add grouping by treatment arm and filtering by site, race and sex. All of this can be done with just a few clicks. As you might have guessed, you just update the "Filter columns" and "Group columns" inputs as shown: +The `SafetyGraphics` Hepatic Explorer chart offers native support for data-driven groups and filtering. Any data column can be used to add filter and grouping controls to the chart. One common use case is to add grouping by treatment arm and filtering by site, race and sex. All of this can be done with just a few clicks. As you might have guessed, you just update the "Filter columns" and "Group columns" inputs as shown: -Click the charts tab to see the following chart (with orange boxes added around the newly created filters and groups for emphasis): +Select "Hepatic Explorer from the Charts drop-down tab to see the following chart (with orange boxes added around the newly created filters and groups for emphasis): -A word of warning - both grouping and filtering works best using categorical variables with a relatively small number of groups (less than 10 or so). With that said, there is no official limit on the number of unique values to include in a group or filter, so if you followed the example above but chose "AGE" (with over a dozen unique integer values) instead of "AGEGR1" (with 3 categorical levels), you might not love the functionality in the chart. Fortunately, it's easy to go back and update the chart to use the categorized variable instead - just go back to the settings tab and update the corresponding setting. +A word of warning - both grouping and filtering works best using categorical variables with a relatively small number of groups (less than 10 or so). With that said, there is no official limit on the number of unique values to include in a group or filter, so if you followed the example above but chose "AGE" (with over a dozen unique integer values) instead of "AGEGR1" (with 3 categorical levels), you might not love the functionality in the chart. Fortunately, it's easy to go back and update the chart to use the categorized variable instead - just go back to the settings tab and update the corresponding setting. ### 3. Flag Rows of Special Interest -You can also use the settings page to identify important values in the data. For the eDish chart, you can flag baseline values (using the "Baseline column" and "Baseline values" inputs) and values included in the analysis population (using "Analysis Flag column" and "Analysis Flag values" inputs). In both cases, you need to choose the "column" first, and then choose 1 or more corresponding "values". Here are some suggested settings using our sample data: +You can also use the settings page to identify important values in the data. For the Hepatic Explorer chart, you can flag baseline values (using the "Baseline column" and "Baseline values" inputs) and values included in the analysis population (using "Analysis Flag column" and "Analysis Flag values" inputs). In both cases, you need to choose the "column" first, and then choose 1 or more corresponding "values". Here are some suggested settings using our sample data: -In the eDish chart, adding a baseline flag enables the users to view a baseline-adjusted version of the chart. Click the chart tab, and then change the "Display Type" control to "Baseline Adjusted (mDish)". +In the Hepatic Explorer chart, adding a baseline flag enables the users to view a baseline-adjusted version of the chart. Click the chart tab, and then change the "Display Type" control to "Baseline Adjusted (mDish)". -We're following ADaM conventions and using "flag" columns ending in "FL" and "Y" values for the configuration here, but any column/value combination is allowed. For example, you could use study day 0 to define baseline by setting baseline column to "ADY" and baseline value to "0". +We're following ADaM conventions and using "flag" columns ending in "FL" and "Y" values for the configuration here, but any column/value combination is allowed. For example, you could use study day 0 to define baseline by setting baseline column to "ADY" and baseline value to "0". ### Summary -This case study shows how to add some basic customizations to your eDish chart with a few clicks in the shiny application. Note that not all customizations are available in the shiny app. You can access more granular settings using the `htmlwidget` that creates the chart (e.g. see `?eDISH`) or even by looking at the documentation for the underlying [safety-eDish github repo](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safety-eDISH) javascript library. +This case study shows how to add some basic customizations to your Hepatic Explorer chart with a few clicks in the shiny application. Note that not all customizations are available in the shiny app. You can access more granular settings using the `htmlwidget` that creates the chart (e.g. see `?hep_explorer`) or even by looking at the documentation for the underlying [hep-explorer github repo](https://github.com/SafetyGraphics/hep-explorer) javascript library. From 4956675c5f97634dd526fd187a023a749734c477 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Tue, 21 May 2019 10:25:22 -0400 Subject: [PATCH 03/39] change to version 1.0.0 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9fdcc7d3..8347bd33 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: safetyGraphics Title: Create Interactive Graphics Related to Clinical Trial Safety -Version: 0.11.0 +Version: 1.0.0 Authors@R: c( person("Jeremy", "Wildfire", email = "jeremy_wildfire@rhoworld.com", role = c("cre","aut")), person("Becca", "Krouse", role="aut"), From 8f2e2dce33d4a52f51c7aae14018511a6e3bb08e Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 22 May 2019 11:41:54 -0400 Subject: [PATCH 04/39] add sas labels #290 --- data-raw/csv_to_rda.R | 52 ++++++++++++++++++++++++++++++++++++++++++ data/adlbc.rda | Bin 97267 -> 98609 bytes 2 files changed, 52 insertions(+) diff --git a/data-raw/csv_to_rda.R b/data-raw/csv_to_rda.R index f31affe1..50202b31 100644 --- a/data-raw/csv_to_rda.R +++ b/data-raw/csv_to_rda.R @@ -1,5 +1,6 @@ library(usethis) library(dplyr) +library(Hmisc) ### Prepare settingsMetadata and save to /data ### @@ -29,4 +30,55 @@ usethis::use_data(chartsMetadata, overwrite = TRUE) ### Save sample data set to /data ### # This is loaded by default in the app and used for testing adlbc <- read.csv("data-raw/adlbc.csv", stringsAsFactors = FALSE) + +### Add labels to sample data set +adlbc_labels <- c("STUDYID" = "Study Identifier", + "SUBJID" = "Subject Identifier for the Study", + "USUBJID" = "Unique Subject Identifier", + "TRTP" = "Planned Treatment", + "TRTPN" = "Planned Treatment (N)", + "TRTA" = "Actual Treatment", + "TRTAN" = "Actual Treatment (N)", + "TRTSDT" = "Date of First Exposure to Treatment", + "TRTEDT" = "Date of Last Exposure to Treatment", + "AGE" = "Age", + "AGEGR1" = "Age Group", + "AGEGR1N" = "Age Group (N)", + "RACE" = "Race", + "RACEN" = "Race (N)", + "SEX" = "Sex", + "COMP24FL" = "Completers Flag", + "DSRAEFL" = "Discontinued due to AE?", + "SAFFL" = "Safety Population Flag", + "AVISIT" = "Analysis Visit", + "AVISITN" = "Analysis Visit (N)", + "ADY" = "Analysis Relative Day", + "ADT" = "Analysis Relative Date", + "VISIT" = "Visit", + "VISITNUM" = "Visit (N)", + "PARAM" = "Parameter", + "PARAMCD" = "Parameter Code", + "PARAMN" = "Parameter (N)", + "PARCAT1" = "Parameter Category", + "AVAL" = "Analysis Value", + "BASE" = "Baseline Value", + "CHG" = "Change from Baseline", + "A1LO" = "Analysis Normal Range Lower Limit", + "A1HI" = "Analysis Normal Range Upper Limit", + "R2A1LO" = "Ratio to Low limit of Analysis Range", + "R2A1HI" = "Ratio to High limit of Analysis Range", + "BR2A1LO" = "Base Ratio to Analysis Range 1 Lower Lim", + "BR2A1HI" = "Base Ratio to Analysis Range 1 Upper Lim", + "ANL01FL" = "Analysis Population Flag", + "ALBTRVAL" = "Amount Threshold Range", + "ANRIND" = "Analysis Reference Range Indicator", + "BNRIND" = "Baseline Reference Range Indicator", + "ABLFL" = "Baseline Record Flag", + "AENTMTFL" = "Analysis End Date Flag", + "LBSEQ" = "Lab Sequence Number", + "LBNRIND" = "Reference Range Indicator", + "LBSTRESN" = "Numeric Result/Finding in Std Units") + +label(adlbc) = as.list(adlbc_labels[match(names(adlbc), names(adlbc_labels))]) + usethis::use_data(adlbc, overwrite = TRUE) diff --git a/data/adlbc.rda b/data/adlbc.rda index 03bf8b05e569f70714a9030850e85c5c9f68f158..16ed420f296c61e21b03ea08d554506968634e07 100644 GIT binary patch delta 97116 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

qD&9p7TWX1Hkr5ivPzPhV4ZT&^hmA+JAc&x$ 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 From 3a947681a907de506215286a310aa8e18394a1a6 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Wed, 22 May 2019 12:03:49 -0400 Subject: [PATCH 05/39] add home info page --- inst/safetyGraphics_app/server.R | 40 ++++++++++++++++++++++++++++++++ inst/safetyGraphics_app/ui.R | 7 ++++++ 2 files changed, 47 insertions(+) diff --git a/inst/safetyGraphics_app/server.R b/inst/safetyGraphics_app/server.R index d26d30ad..89559020 100644 --- a/inst/safetyGraphics_app/server.R +++ b/inst/safetyGraphics_app/server.R @@ -125,5 +125,45 @@ for(chart in all_charts){ charts = reactive(labeledCharts[labeledCharts %in% settings_new$charts()]) ) + + + + output$about <- renderUI({ + HTML('

Welcome to the Safety Graphics Shiny App

+

The Safety Graphics Shiny app is an interactive tool for evaluating clinical trial safety using + a flexible data pipeline. This application and corresponding + safetyGraphics + R package have been developed as part of the Interactive Safety Graphics (ISG) workstream + of the ASA Biopharm-DIA Safety Working Group.

+

Using the app

+

Detailed instructions about using the app can be found in our + vignette. In short, + the user will begin by loading a data file, adjust settings as needed and view the interactive charts. + Finally, the user may export a self-contained, fully reproducible snapshot of the charts that can be easily shared with others.

+

Interactive Charts

+

The included interactive charts are built using the htmlwidgets framework in R. The code libraries + and configuration details for the underlying JavaScript charts are located below. +

+

') + + }) session$onSessionEnded(stopApp) } diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index c40c87fb..0c20ba10 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -17,6 +17,13 @@ tagList( navbarPage( "safetyGraphics Shiny app", id="nav_id", + tabPanel( + title = "Home", icon=icon("home"), + fluidRow( + # tags$style(type='text/css', '#about {font-size:23px;}'), + column(width=9, style='font-size:20px', uiOutput(outputId = "about")) + ) + ), tabPanel( title = "Data", dataUploadUI("datatab") From 8556714a2f8f72e56624c66a76fc118e01ea1a33 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 22 May 2019 09:39:13 -0700 Subject: [PATCH 06/39] tweaks generate settings flow. fix #315 --- R/generateSettings.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/generateSettings.R b/R/generateSettings.R index cb8a6f7c..0c8ad3fe 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -126,7 +126,7 @@ generateSettings <- function(standard="None", charts=NULL, useDefaults=TRUE, par #Coerce empty string to NULL for (i in names(shell)){ if (!is.null(shell[[i]])){ - if (shell[[i]][1]==""){ + if (shell[[i]]==""){ shell[i] <- list(NULL) } } From 9c5f6919499f92bbd3210a9189e9053d512fc4fb Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 22 May 2019 10:40:23 -0700 Subject: [PATCH 07/39] refactor logic for data_mapping. fix #238 --- R/generateSettings.R | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/R/generateSettings.R b/R/generateSettings.R index 0c8ad3fe..32042f41 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -123,14 +123,21 @@ generateSettings <- function(standard="None", charts=NULL, useDefaults=TRUE, par } } - #Coerce empty string to NULL - for (i in names(shell)){ - if (!is.null(shell[[i]])){ - if (shell[[i]]==""){ - shell[i] <- list(NULL) - } + #Coerce empty string to NULL for data mappings + + data_mappings <- safetyGraphics::getSettingsMetadata( + charts = charts, + cols="text_key", + filter_expr=column_mapping + ) + for(text_key in data_mappings){ + key <- textKeysToList(text_key)[[1]] + current <- getSettingValue(key,shell) + if (!is.null(current)){ + if(current == ""){ + shell<-setSettingsValue(key=key, value=NULL, settings=shell) + } } } - return(shell) } From 821f4ba366b2c1e67dde4c3824f8b3a19d9c7818 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Wed, 22 May 2019 13:55:10 -0400 Subject: [PATCH 08/39] add hex and more links --- inst/safetyGraphics_app/server.R | 16 +++++++++++++--- inst/safetyGraphics_app/ui.R | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/inst/safetyGraphics_app/server.R b/inst/safetyGraphics_app/server.R index 89559020..6f44ccfb 100644 --- a/inst/safetyGraphics_app/server.R +++ b/inst/safetyGraphics_app/server.R @@ -135,12 +135,12 @@ for(chart in all_charts){ safetyGraphics R package have been developed as part of the Interactive Safety Graphics (ISG) workstream of the ASA Biopharm-DIA Safety Working Group.

-

Using the app

+

Using the app

Detailed instructions about using the app can be found in our vignette. In short, the user will begin by loading a data file, adjust settings as needed and view the interactive charts. Finally, the user may export a self-contained, fully reproducible snapshot of the charts that can be easily shared with others.

-

Interactive Charts

+

Interactive Charts

The included interactive charts are built using the htmlwidgets framework in R. The code libraries and configuration details for the underlying JavaScript charts are located below.

-

') +

+
+

For more information about safetyGraphics, please visit our +GitHub repository. We also welcome your suggestions in our +issue tracker. +

') }) + + output$hex <- renderImage({ + list(src = system.file("safetyGraphicsHex/safetyGraphicsHex.png", package = "safetyGraphics"), width="60%") + }, deleteFile = FALSE) + session$onSessionEnded(stopApp) } diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index 0c20ba10..01a554ba 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -20,8 +20,8 @@ tagList( tabPanel( title = "Home", icon=icon("home"), fluidRow( - # tags$style(type='text/css', '#about {font-size:23px;}'), - column(width=9, style='font-size:20px', uiOutput(outputId = "about")) + column(width=8, style='font-size:20px', uiOutput(outputId = "about")), + column(width=4, imageOutput(outputId = "hex")) ) ), tabPanel( From e2d521263cacf8f23944ab6ba01b1ddde212f81a Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 22 May 2019 14:11:03 -0400 Subject: [PATCH 09/39] update vignette pics and few tweaks --- vignettes/shinyUserGuide.Rmd | 43 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/vignettes/shinyUserGuide.Rmd b/vignettes/shinyUserGuide.Rmd index b0c31e1b..c962f269 100644 --- a/vignettes/shinyUserGuide.Rmd +++ b/vignettes/shinyUserGuide.Rmd @@ -42,11 +42,10 @@ safetyGraphicsApp() ## Loading Large Files -By default, Shiny only allows users to load files smaller than 5mb. If you want to load a larger file, run this code before opening the app: +By default, Shiny only allows users to load files smaller than 5mb. If you want to load a larger file, use the maxFileSize setting when calling the app: ``` -maxFileSize<-100 #Update 100 desired max file size in megabytes -options(shiny.maxRequestSize=(maxFileSize*1024^2)) +safetyGraphicsApp(maxFileSize = 100) ``` # Typical Workflow @@ -57,15 +56,15 @@ After opening the app, you will typically follow the workflow below. In short, y This workflow lines up with the items in the toolbar for the App. - + More information about each step in the process is shown below. ## Load Data -When you open the app, you are taken to the Data Tab with "Data Upload" and "Data Preview" panels. The preview for the pre-loaded "Example data" dataset is shown by default. +When you open the app, you are taken to the Home Tab which contains some general information about the app and links to helpful documentation. Click on the Data Tab and you should see the "Data Upload" and "Data Preview" panels. The preview for the pre-loaded "Example data" dataset is shown by default. - + To load your own data, simply click the browse button and select a `.csv` or `.sas7bdat` data set. Once the file is loaded, select it in the list at the bottom of the "Data Upload Panel". Once selected, the "Data Preview" panel will update automatically (along with the Settings and Chart tabs). @@ -75,7 +74,7 @@ The charts in the safetyGraphics app are specifically designed for clinical tria After loading your data, navigate to the Settings tab to customize the behavior of the charts. This tab includes a Charts panel for selecting the charts you want to visualize and other panels for different types of chart settings. For example, the "Data Mappings" panel (shown below for the "Example Data" ADaM data set) can be used to specify the column that contains the unique subject ID, and on the more general "Appearance Settings" panel, there is an option to specify a warning message to be displayed when the chart loads. You can hover the mouse over any setting label to get more details. The small numbers to the right of the settings labels indicate the number of charts that use the relevant setting. Mousing over them presents a list of these charts. - + When possible, items on the settings tab are pre-populated based on the data standard of the selected data set. See the Case Studies below for more details regarding working with non-standard data and adding customizations to the charts. @@ -84,13 +83,13 @@ When possible, items on the settings tab are pre-populated based on the data sta Once the settings configuration is complete, click on the Charts tab to view a drop-down of the available charts. A green check will display by charts that are ready to be visualized and a red X will indicate that settings need to be changed in order to render the chart. Simply click one of the options to view it. The chart tab updates automatically when settings are changed or new data is loaded. More details about chart functionality will be documented in separate vignettes. - + ## Export Results Navigate to the Reports tab to choose reports for export and click the "Export Chart(s)" button at the bottom to create a standalone copy of the charts using the current configuration. The export functionality combines the data, code, and settings for the charts in to a single file. In addition to the charts themselves, the export includes a summary of the tool, and code to recreate the customized charts in R. - + # Case Study #1 - Mapping Non-Standard data @@ -131,35 +130,35 @@ safetyGraphicsApp() Use the "Browse.." button on the data upload section of the data tab to load a non-standard data set. We'll use the `.csv` saved [here](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/raw/master/inst/eDISH_app/tests/partialSDTM.csv), but the process is similar for other data sets. Notice that once the data is loaded, the app will detect whether the data matches one of those pre-loaded standards, and a note is added to indicate whether a match is found. Our sample data is a partial match for the SDTM standard. Once you select the newly loaded data set, the app should look like the screen capture below. Click on the Charts tab and note the red X's in the drop-down indicating that user customization is needed. - + ### 3. Select Columns Next, click the "Settings" tab in the nav bar at the top of the page. The page should look something like this: - + Behind the scenes, a validation process is run to check if the selected settings match up with the selected data set to create a valid chart. Green (for valid) and red (for invalid) status messages are shown after each label in the Settings tab - you can hover the mouse over the status to get more details. -As you can see, we've got several invalid settings with red status messages. We now need to go through and update each invalid setting and turn its status message in to a green "ok". Once all of the individual settings are valid, the red Xs in the Charts drop-down will turn to green checks, and the chart will be created. Let's hover over the first red error message to see the detailed description of the failed check: +As you can see, we've got several invalid settings with red status messages. We now need to go through and update each invalid setting and turn its status message in to a green "ok". Once all of the individual settings are valid, the red Xs in the Charts drop-down will turn to green checks, and the chart will be created. Let's hover over the red X by the Measure Column Setting to see the detailed description of the failed check: - + As you might've guessed from the empty select box, the check failed because no value is specified for the setting. Choosing the measure column is simple. Click the select box to see pre-populated options corresponding to the columns in the data. - + Now select LBTEST for Measure Column and LBDY for the Study Day Column option. Your setting page should look something like this: - + Now we need to fill in the 4 inputs beneath Measure Column. You may have noticed that there were no options available for these inputs when the page loaded. This is because these options are field level data that depend on the Measure Column option. Once you selected a Measure Column, the options for these inputs were populated using the unique values found in that data column. To fill them in, just type the first few letters of lab in the text box. For example, type "Alan" for the Alanine Aminotransferase value input and select the correct option. - + Repeat the process for the other 3 "value" inputs and viola, the red x changes to a green check, and the Hepatic Explorer chart is ready. - + ### 4. View Chart @@ -175,7 +174,7 @@ To export the chart, click the Reports Tab, make sure that the Hepatic Explorer Open the downloaded file in a new tab in your browser and you'll see tabs for each of the charts and an "Info" tab. The Hepatic Explorer tab will be identical to the chart shown above, with all of your customizations intact. The "Info" tab, shown below, has a brief description of the safetyGraphics package and source code that you can use to recreate the charts in R. - + The html file contains all of the data and code for the chartw and is easy to share. Just send the file to the person you're sharing with, and tell them to open it in their web browser (just double-click the file) - they don't even need R. @@ -213,11 +212,11 @@ We'll use the pre-loaded example data for this case study, so there is no need t The `SafetyGraphics` Hepatic Explorer chart offers native support for data-driven groups and filtering. Any data column can be used to add filter and grouping controls to the chart. One common use case is to add grouping by treatment arm and filtering by site, race and sex. All of this can be done with just a few clicks. As you might have guessed, you just update the "Filter columns" and "Group columns" inputs as shown: - + Select "Hepatic Explorer from the Charts drop-down tab to see the following chart (with orange boxes added around the newly created filters and groups for emphasis): - + A word of warning - both grouping and filtering works best using categorical variables with a relatively small number of groups (less than 10 or so). With that said, there is no official limit on the number of unique values to include in a group or filter, so if you followed the example above but chose "AGE" (with over a dozen unique integer values) instead of "AGEGR1" (with 3 categorical levels), you might not love the functionality in the chart. Fortunately, it's easy to go back and update the chart to use the categorized variable instead - just go back to the settings tab and update the corresponding setting. @@ -225,11 +224,11 @@ A word of warning - both grouping and filtering works best using categorical var You can also use the settings page to identify important values in the data. For the Hepatic Explorer chart, you can flag baseline values (using the "Baseline column" and "Baseline values" inputs) and values included in the analysis population (using "Analysis Flag column" and "Analysis Flag values" inputs). In both cases, you need to choose the "column" first, and then choose 1 or more corresponding "values". Here are some suggested settings using our sample data: - + In the Hepatic Explorer chart, adding a baseline flag enables the users to view a baseline-adjusted version of the chart. Click the chart tab, and then change the "Display Type" control to "Baseline Adjusted (mDish)". - + We're following ADaM conventions and using "flag" columns ending in "FL" and "Y" values for the configuration here, but any column/value combination is allowed. For example, you could use study day 0 to define baseline by setting baseline column to "ADY" and baseline value to "0". From b637a97769460f9607c1b024fb44c5884af28371 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 22 May 2019 14:20:03 -0400 Subject: [PATCH 10/39] add info about SAS labels --- vignettes/shinyUserGuide.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/shinyUserGuide.Rmd b/vignettes/shinyUserGuide.Rmd index c962f269..ee7b5422 100644 --- a/vignettes/shinyUserGuide.Rmd +++ b/vignettes/shinyUserGuide.Rmd @@ -76,7 +76,7 @@ After loading your data, navigate to the Settings tab to customize the behavior -When possible, items on the settings tab are pre-populated based on the data standard of the selected data set. See the Case Studies below for more details regarding working with non-standard data and adding customizations to the charts. +When possible, items on the settings tab are pre-populated based on the data standard of the selected data set. If a SAS data set with labels is provided, the relevant column labels will appear within brackets [] next to their variable names. You can see this in the Data Mappings screenshot above. See the Case Studies below for more details regarding working with non-standard data and adding customizations to the charts. ## View Chart From fb505c7d7f2bee2b5f7cc042da52258da40835aa Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 22 May 2019 11:24:24 -0700 Subject: [PATCH 11/39] update to hep explorer v1.0. fix #311 --- data-raw/chartsMetadata.csv | 2 +- data-raw/settingsMetadataCharts.csv | 2 +- data/chartsMetadata.rda | Bin 834 -> 817 bytes data/settingsMetadata.rda | Bin 2210 -> 2214 bytes inst/htmlwidgets/chartRenderer.yaml | 4 +- .../hepexplorer.js} | 189 ++++++++++++++---- 6 files changed, 157 insertions(+), 40 deletions(-) rename inst/htmlwidgets/lib/{safety-eDISH-0.17.0/safetyedish.js => hep-explorer-1.0.0/hepexplorer.js} (96%) diff --git a/data-raw/chartsMetadata.csv b/data-raw/chartsMetadata.csv index 9c943973..5808bbc2 100644 --- a/data-raw/chartsMetadata.csv +++ b/data-raw/chartsMetadata.csv @@ -1,5 +1,5 @@ chart,main,label,description,repo_url,settings_url,type,maxWidth -edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 +hepexplorer,hepexplorer,Hepatic Safety Explorer,Interactive Graphic for Exploring Liver Function Data in Clinical Trials,https://github.com/SafetyGraphics/hep-explorer,https://github.com/SafetyGraphics/hep-explorer/wiki/Configuration,htmlwidget,620 safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 diff --git a/data-raw/settingsMetadataCharts.csv b/data-raw/settingsMetadataCharts.csv index ba404f6b..1e5809da 100644 --- a/data-raw/settingsMetadataCharts.csv +++ b/data-raw/settingsMetadataCharts.csv @@ -1,4 +1,4 @@ -text_key,edish,safetyhistogram,safetyoutlierexplorer,safetyshiftplot,safetyresultsovertime,paneledoutlierexplorer +text_key,hepexplorer,safetyhistogram,safetyoutlierexplorer,safetyshiftplot,safetyresultsovertime,paneledoutlierexplorer id_col,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE value_col,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE measure_col,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE diff --git a/data/chartsMetadata.rda b/data/chartsMetadata.rda index bfeeb2bd573ad166030d71b9694ecbed5e35a414..ead2821ed7192b6cc45849a73d12ea839967c704 100644 GIT binary patch literal 817 zcmV-11J3+HT4*^jL0KkKS=DSE#sC4K|H1!%>%m|juoExl+`zx@-{3$106+i{&;vQ) zDypC%#!Q+q4Ky$lLlZ`sF#{tBqfIb_COFBHMj@t#0%&4r(I{uGk?9!?111nXstz(_(THiGfSMSZG|7k=7)=^!gc&fALTDyQ=}*Z| zAk<`fAOWCg01qf)XoW|A73>0urE8D}PgEj0IDdI-wW0NrR1eIJdXD6|#lY@JNTnT|uEa(Zkj+mgd*@>)*9&*2RcC>hFLs4M4Vc}iPyEFcmhS_^Wo z?=UWLWd+O%qZAe?VReRwSSA|&PWZawrG67(X)Sxw!JtPz8yocZE0I^aEPS4>Iag?i z8O6y}SsJ>3Y*yl4HC>YYmxM8ax47JfYEw&DQTx`3E>aA)i5s`LluS^&S^WgdmB<)y zd?F~d0-jlsT_9+T{IeC4j5KJn>PSyG!@<@jjOT5U60ogAfIy0Y3ZO!{HW~Umq*Ht% z{pQppRnL%!M~6U>kXhGu(MTvLG=gO!lm&+l!OY(zWnH5%-P-Rl&LV;xfo=Qj*~!gD z&9m=u}d0H%JgD9a)n41{*b99KdqHPlt5OVd& zPQw8g+YxnvRwDsnsc)b-SgJfh!Q+9&r*Pjg`MJyTQDZSu^q?@fq5!3)DJI(n(6kE< zmAorWRDeYhu)4F3)lO=Nd@oe7=VpqHBZ%0@m7CzcQZT&G2R}f@J328jq$|F9QfWaC zfuj)JF@&*^9MGjxQ386YBnnj*8m*Qs8Qz@{fQgGSm{-;cBxzX~*?l0GBU>N}-a1n0 v-V_nkarGI&jLbm;bRN*RMFwhXZ8pZx@}!Oa!2fQHfAM!DQ-ui)R>9n0yVG^Y literal 834 zcmV-I1HJr0T4*^jL0KkKS#=)yaR33N|H1!%>p@_DuoEuk-oU@_-{3$106+i{&;vdM zf`AZ`MLkA@Jw{|lr~n73Vgn;h05p0)&~OYR38o={Kr#Sf8f3|YU?HKvFpMUch5-P` z0fcFjCJ}&!hXBGbnqn9P10V(wrc9Vd0vZVt5F<>Hqbhqwh?~-$rqX(9Vl?#)8fepG znt0U^uyfVcNY?;XSWKaB*0a5UO6s~#JS@nl^r z?!;ANa-|#j)Z2D0nfqO}QmcrZCtpS2EomVz0Aa@n#iRkPq#*b{wh2jryDw|Rmmh00 zsmnYa)EtLNcUdb9S5DKocNq$q`B42CWsRSj8(-^V}qdKt1!)A_fDPpo+@D&sEmr_)m~ETb!NkN1L{a4 z)jh*>_QCQoE?RO&BxdT0tL@VS#)FL6F`TCA()$Zf^TiWhvKfCeILIg`x~$ytD=rTNaXoc5-59 z;1L1aBFT=5I6EOmNKJmoETYz1lPXspVHfP;9+OIWg+X)J+k1G=2~c(>BK~NNJ5M>7 zZan6|R#~)O^6yK;Se;f->F9)9BQ+G#JwqF`tOwL4k3c8SIF1;f1j%njWp)r%vn7QAT3&3v-UgPfTA)Qj$Y&KXB13qOFt!Q}4!q+`%&dr0{0H`+!+(pp MBAh5lI*)v~fR~4P>Hq)$ diff --git a/data/settingsMetadata.rda b/data/settingsMetadata.rda index 09da9b522120ad18246ba30f13bc7dcc90c483eb..06ed2b434b7d887688c8427afe376fa141b0b03c 100644 GIT binary patch delta 2201 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"); + var min_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[0]'; + }); + var min_r_ratio_input = min_r_ratio.select('input'); + + var max_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[1]'; + }); + var max_r_ratio_input = max_r_ratio.select('input'); + + min_r_ratio_input.attr('id', 'r_ratio_min'); + max_r_ratio_input.attr('id', 'r_ratio_max'); + + //move the max r ratio control next to the min control + min_r_ratio.append('span').text(' - '); + min_r_ratio.append(function() { + return max_r_ratio_input.node(); + }); + + max_r_ratio.remove(); + + //add a reset button + min_r_ratio + .append('button') + .style('padding', '0.2em 0.5em 0.2em 0.4em') + .style('margin-left', '0.5em') + .style('border-radius', '0.4em') + .text('Reset') + .on('click', function() { + config.r_ratio[0] = 0; + min_r_ratio.select('input#r_ratio_min').property('value', config.r_ratio[0]); + config.r_ratio[1] = config.max_r_ratio; + min_r_ratio.select('input#r_ratio_max').property('value', config.r_ratio[1]); + chart.draw(); + }); } } @@ -1408,6 +1446,7 @@ var initial_container = chart.element; var initial_settings = chart.initial_settings; var initial_data = chart.initial_data; + chart.emptyChartWarning.remove(); chart.destroy(); chart = null; @@ -1743,14 +1782,15 @@ return f.type != 'subsetter'; }) .filter(function(f) { - return f.option != 'r_ratio_cut'; + return f.option != 'r_ratio[0]'; }) .filter(function(f, i) { return i == 0; - }); + }) + .attr('class', 'first-setting'); - this.controls.setting_header = first_setting - .insert('div', '*') + this.controls.setting_header = this.controls.wrap + .insert('div', '.first-setting') .attr('class', 'subtitle') .style('border-top', '1px solid black') .style('border-bottom', '1px solid black') @@ -1770,16 +1810,13 @@ .selectAll('div') .filter(function(controlInput) { return ( - controlInput.label === 'Minimum R Ratio' || - controlInput.type === 'subsetter' + controlInput.label === 'R Ratio Range' || controlInput.type === 'subsetter' ); }) .classed('subsetter', true); - var first_filter = this.controls.wrap.select('div.subsetter'); - - this.controls.filter_header = first_filter - .insert('div', '*') + this.controls.filter_header = this.controls.wrap + .insert('div', 'div.subsetter') .attr('class', 'subtitle') .style('border-top', '1px solid black') .style('border-bottom', '1px solid black') @@ -1819,12 +1856,13 @@ } function addFootnote() { - this.wrap + this.footnote = this.wrap .append('div') .attr('class', 'footnote') .text('Use controls to update chart or click a point to see participant details.') .style('font-size', '0.7em') .style('padding-top', '0.1em'); + this.footnote.timing = this.footnote.append('p'); } function addDownloadButton() { @@ -1865,6 +1903,21 @@ } } + function initEmptyChartWarning() { + console.log(this); + this.emptyChartWarning = d3 + .select(this.element) + .append('span') + .text('No data selected. Try updating your settings or resetting the chart. ') + .style('display', 'none') + .style('color', '#a94442') + .style('background-color', '#f2dede') + .style('border', '1px solid #ebccd1') + .style('padding', '0.5em') + .style('margin', '0 2% 12px 2%') + .style('border-radius', '0.2em'); + } + function onLayout() { layoutPanels.call(this); @@ -1877,7 +1930,7 @@ addDownloadButton.call(this); addFootnote.call(this); - addRRatioSpan.call(this); + formatRRatioControl.call(this); initQuadrants.call(this); initRugs.call(this); initVisitPath.call(this); @@ -1885,6 +1938,7 @@ initResetButton.call(this); initDisplayControl.call(this); initControlLabels.call(this); + initEmptyChartWarning.call(this); } function updateAxisSettings() { @@ -1930,9 +1984,43 @@ .text(this.config.y.column + ' Reference Line'); } - function updateRRatioSpan() { + function setMaxRRatio() { + var chart = this; + var config = this.config; + var r_ratio_wrap = chart.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[0]'; + }); + + //if no max value is defined, use the max value from the data if (this.config.r_ratio_filter) { - this.controls.wrap.select('#r-ratio').text('(ALT/ULN) / (ALP/ULN)'); + if (!config.r_ratio[1]) { + var raw_max_r_ratio = d3.max(this.raw_data, function(d) { + return d.rRatio; + }); + config.max_r_ratio = Math.ceil(raw_max_r_ratio * 10) / 10; //round up to the nearest 0.1 + config.r_ratio[1] = config.max_r_ratio; + chart.controls.wrap + .selectAll('.control-group') + .filter(function(d) { + return d.option === 'r_ratio[0]'; + }) + .select('input#r_ratio_max') + .property('value', config.max_r_ratio); + } + + //make sure r_ratio[0] <= r_ratio[1] + if (config.r_ratio[0] > config.r_ratio[1]) { + config.r_ratio = config.r_ratio.reverse(); + r_ratio_wrap.select('input#r_ratio_min').property('value', config.r_ratio[0]); + r_ratio_wrap.select('input#r_ratio_max').property('value', config.r_ratio[1]); + } + + //Define flag given r-ratio minimum. + this.raw_data.forEach(function(participant_obj) { + var aboveMin = participant_obj.rRatio >= config.r_ratio[0]; + var belowMax = participant_obj.rRatio <= config.r_ratio[1]; + participant_obj.rRatioFlag = aboveMin & belowMax ? 'Y' : 'N'; + }); } } @@ -1967,10 +2055,6 @@ //R-ratio should be the ratio of ALT to ALP, i.e. the x-axis to the z-axis. participant_obj.rRatio = participant_obj['ALT_relative_uln'] / participant_obj['ALP_relative_uln']; - - //Define flag given r-ratio minimum. - participant_obj.rRatioFlag = - participant_obj.rRatio > this.config.r_ratio_cut ? 'Y' : 'N'; } } @@ -2251,8 +2335,8 @@ function onPreprocess() { updateAxisSettings.call(this); //update axis label based on display type updateControlCutpointLabels.call(this); //update cutpoint control labels given x- and y-axis variables - updateRRatioSpan.call(this); this.raw_data = flattenData.call(this); //convert from visit-level data to participant-level data + setMaxRRatio.call(this); setLegendLabel.call(this); //update legend label based on group variable dropMissingValues.call(this); } @@ -2485,6 +2569,12 @@ } } + function hideEmptyChart() { + var emptyChart = this.filtered_data.length == 0; + this.wrap.style('display', emptyChart ? 'none' : 'inline-block'); + this.emptyChartWarning.style('display', emptyChart ? 'inline-block' : 'none'); + } + function onDraw() { //clear participant Details clearParticipantDetails.call(this); @@ -2504,6 +2594,7 @@ //update the count in the filter label updateFilterLabel.call(this); + hideEmptyChart.call(this); } function drawQuadrants() { @@ -3986,8 +4077,11 @@ ' @ Day ' + raw[yvar + '_' + config.studyday_col], dayDiff = raw['day_diff'] + ' days apart', - idLabel = 'Participant ID: ' + raw[config.id_col]; - return idLabel + '\n' + xLabel + '\n' + yLabel + '\n' + dayDiff; + idLabel = 'Participant ID: ' + raw[config.id_col], + rRatioLabel = config.r_ratio_filter + ? '\n' + 'Overall R Ratio: ' + d3.format('0.2f')(raw.rRatio) + : ''; + return idLabel + rRatioLabel + '\n' + xLabel + '\n' + yLabel + '\n' + dayDiff; }); } @@ -4431,6 +4525,26 @@ }); } + function updateTimingFootnote() { + var config = this.config; + var windowText = + config.visit_window == 0 + ? 'on the same day' + : config.visit_window == 1 + ? 'within 1 day' + : 'within ' + config.visit_window + ' days'; + var timingFootnote = + ' Points where maximum ' + + config.measure_values[config.x.column] + + ' and ' + + config.measure_values[config.y.column] + + ' values were collected ' + + windowText + + ' are filled, others are empty.'; + + this.footnote.timing.text(timingFootnote); + } + function onResize$1() { //add point interactivity, custom title and formatting addPointMouseover.call(this); @@ -4455,6 +4569,9 @@ //axis formatting adjustTicks.call(this); + + //add timing footnote + updateTimingFootnote.call(this); } var callbacks = { From 422249870c02845adb36b3360206203b41013b38 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 22 May 2019 11:28:02 -0700 Subject: [PATCH 12/39] update outlier explorer. fix #311 --- .../safetyOutlierExplorer.js | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/inst/htmlwidgets/lib/safety-outlier-explorer-2.5.4/safetyOutlierExplorer.js b/inst/htmlwidgets/lib/safety-outlier-explorer-2.5.4/safetyOutlierExplorer.js index 214c3885..45120c8f 100644 --- a/inst/htmlwidgets/lib/safety-outlier-explorer-2.5.4/safetyOutlierExplorer.js +++ b/inst/htmlwidgets/lib/safety-outlier-explorer-2.5.4/safetyOutlierExplorer.js @@ -376,61 +376,6 @@ settings.unscheduled_visit_regex = new RegExp(pattern, flags); } - //Define default details. - var defaultDetails = [{ value_col: settings.id_col, label: 'Participant ID' }]; - if (Array.isArray(settings.filters)) - settings.filters - .filter(function(filter) { - return filter.value_col !== settings.id_col; - }) - .forEach(function(filter) { - return defaultDetails.push({ - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter - }); - }); - defaultDetails.push({ value_col: settings.value_col, label: 'Result' }); - if (settings.normal_col_low) - defaultDetails.push({ - value_col: settings.normal_col_low, - label: 'Lower Limit of Normal' - }); - if (settings.normal_col_high) - defaultDetails.push({ - value_col: settings.normal_col_high, - label: 'Upper Limit of Normal' - }); - - //If [settings.details] is not specified: - if (!settings.details) settings.details = defaultDetails; - else { - //If [settings.details] is specified: - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - settings.details.forEach(function(detail) { - if ( - defaultDetails - .map(function(d) { - return d.value_col; - }) - .indexOf(detail.value_col ? detail.value_col : detail) === -1 - ) - defaultDetails.push({ - value_col: detail.value_col ? detail.value_col : detail, - label: detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail - }); - }); - settings.details = defaultDetails; - } - return settings; } From ce410195c2ec1b2d8f429c76534c452e55224d77 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 22 May 2019 15:23:51 -0400 Subject: [PATCH 13/39] becca fixes --- vignettes/shinyUserGuide.Rmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vignettes/shinyUserGuide.Rmd b/vignettes/shinyUserGuide.Rmd index ee7b5422..9098270b 100644 --- a/vignettes/shinyUserGuide.Rmd +++ b/vignettes/shinyUserGuide.Rmd @@ -128,7 +128,7 @@ safetyGraphicsApp() ### 2. Load Data -Use the "Browse.." button on the data upload section of the data tab to load a non-standard data set. We'll use the `.csv` saved [here](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/raw/master/inst/eDISH_app/tests/partialSDTM.csv), but the process is similar for other data sets. Notice that once the data is loaded, the app will detect whether the data matches one of those pre-loaded standards, and a note is added to indicate whether a match is found. Our sample data is a partial match for the SDTM standard. Once you select the newly loaded data set, the app should look like the screen capture below. Click on the Charts tab and note the red X's in the drop-down indicating that user customization is needed. +Use the "Browse.." button on the data upload section of the data tab to load a non-standard data set. We'll use the `.csv` saved [here](https://github.com/SafetyGraphics/safetyGraphics/tree/master/inst/safetyGraphics_app/tests/partialSDTM.csv), but the process is similar for other data sets. Notice that once the data is loaded, the app will detect whether the data matches one of those pre-loaded standards, and a note is added to indicate whether a match is found. Our sample data is a partial match for the SDTM standard. Once you select the newly loaded data set, the app should look like the screen capture below. Click on the Charts tab and note the red X's in the drop-down indicating that user customization is needed. @@ -176,7 +176,7 @@ Open the downloaded file in a new tab in your browser and you'll see tabs for ea -The html file contains all of the data and code for the chartw and is easy to share. Just send the file to the person you're sharing with, and tell them to open it in their web browser (just double-click the file) - they don't even need R. +The html file contains all of the data and code for the charts and is easy to share. Just send the file to the person you're sharing with, and tell them to open it in their web browser (just double-click the file) - they don't even need R. ## Summary @@ -234,4 +234,4 @@ We're following ADaM conventions and using "flag" columns ending in "FL" and "Y" ### Summary -This case study shows how to add some basic customizations to your Hepatic Explorer chart with a few clicks in the shiny application. Note that not all customizations are available in the shiny app. You can access more granular settings using the `htmlwidget` that creates the chart (e.g. see `?hep_explorer`) or even by looking at the documentation for the underlying [hep-explorer github repo](https://github.com/SafetyGraphics/hep-explorer) javascript library. +This case study shows how to add some basic customizations to your Hepatic Explorer chart with a few clicks in the shiny application. Note that not all customizations are available in the shiny app. You can access more granular settings by looking at the documentation for the underlying [hep-explorer github repo](https://github.com/SafetyGraphics/hep-explorer) javascript library. From 23123a4f6e439d302048b248ea84aeefc01c65a0 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 22 May 2019 15:52:29 -0400 Subject: [PATCH 14/39] minor tweaks --- vignettes/shinyUserGuide.Rmd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vignettes/shinyUserGuide.Rmd b/vignettes/shinyUserGuide.Rmd index 9098270b..8efc3030 100644 --- a/vignettes/shinyUserGuide.Rmd +++ b/vignettes/shinyUserGuide.Rmd @@ -66,7 +66,7 @@ When you open the app, you are taken to the Home Tab which contains some general -To load your own data, simply click the browse button and select a `.csv` or `.sas7bdat` data set. Once the file is loaded, select it in the list at the bottom of the "Data Upload Panel". Once selected, the "Data Preview" panel will update automatically (along with the Settings and Chart tabs). +To load your own data, simply click the browse button and select a `.csv` or `.sas7bdat` data set. Once the file is loaded, select it in the list at the bottom of the "Data Upload Panel". Once selected, the "Data Preview" panel will update automatically (along with the Settings, Charts, and Reports tabs). The charts in the safetyGraphics app are specifically designed for clinical trial safety monitoring, and require laboratory datasets that contain one row per participant per time point per measure. Data mappings for two common [CDISC](https://www.cdisc.org/) data standards - [SDTM](https://www.cdisc.org/standards/foundational/sdtm) and [ADaM](https://www.cdisc.org/standards/foundational/adam) - are pre-loaded in the application. As described below, the app can automatically generate charts for data sets using these standards; other data sets require some user configuration. @@ -140,7 +140,7 @@ Next, click the "Settings" tab in the nav bar at the top of the page. The page s Behind the scenes, a validation process is run to check if the selected settings match up with the selected data set to create a valid chart. Green (for valid) and red (for invalid) status messages are shown after each label in the Settings tab - you can hover the mouse over the status to get more details. -As you can see, we've got several invalid settings with red status messages. We now need to go through and update each invalid setting and turn its status message in to a green "ok". Once all of the individual settings are valid, the red Xs in the Charts drop-down will turn to green checks, and the chart will be created. Let's hover over the red X by the Measure Column Setting to see the detailed description of the failed check: +As you can see, we've got several invalid settings with red status messages. We now need to go through and update each invalid setting and turn its status icon into a green check. Once all of the individual settings are valid, the red Xs in the Charts drop-down will turn to green checks, and the chart will be created. Let's hover over the red X by the Measure Column Setting to see the detailed description of the failed check: @@ -148,15 +148,15 @@ As you might've guessed from the empty select box, the check failed because no v -Now select LBTEST for Measure Column and LBDY for the Study Day Column option. Your setting page should look something like this: +Now select ID for the ID column, LBTEST for the Measure Column, and LBDY for the Study Day Column option. Your setting page should look something like this: -Now we need to fill in the 4 inputs beneath Measure Column. You may have noticed that there were no options available for these inputs when the page loaded. This is because these options are field level data that depend on the Measure Column option. Once you selected a Measure Column, the options for these inputs were populated using the unique values found in that data column. To fill them in, just type the first few letters of lab in the text box. For example, type "Alan" for the Alanine Aminotransferase value input and select the correct option. +Now we need to fill in the 4 inputs beneath Measure Column. You may have noticed that there were no options available for these inputs when the page loaded. This is because these options are field level data that depend on the Measure Column option. Once you selected a Measure Column, the options for these inputs were populated using the unique values found in that data column. To fill them in, just type the first few letters of the lab measure in the text box. For example, type "Alan" for the Alanine Aminotransferase value input and select the correct option. -Repeat the process for the other 3 "value" inputs and viola, the red x changes to a green check, and the Hepatic Explorer chart is ready. +Repeat the process for the other 3 "value" inputs and viola, the red X changes to a green check, and the Hepatic Explorer chart is ready. @@ -214,11 +214,11 @@ The `SafetyGraphics` Hepatic Explorer chart offers native support for data-drive -Select "Hepatic Explorer from the Charts drop-down tab to see the following chart (with orange boxes added around the newly created filters and groups for emphasis): +Select "Hepatic Explorer" from the Charts drop-down tab to see the following chart (with orange boxes added around the newly created filters and groups for emphasis): -A word of warning - both grouping and filtering works best using categorical variables with a relatively small number of groups (less than 10 or so). With that said, there is no official limit on the number of unique values to include in a group or filter, so if you followed the example above but chose "AGE" (with over a dozen unique integer values) instead of "AGEGR1" (with 3 categorical levels), you might not love the functionality in the chart. Fortunately, it's easy to go back and update the chart to use the categorized variable instead - just go back to the settings tab and update the corresponding setting. +A word of warning - both grouping and filtering work best using categorical variables with a relatively small number of groups (less than 10 or so). With that said, there is no official limit on the number of unique values to include in a group or filter, so if you followed the example above but chose "AGE" (with over a dozen unique integer values) instead of "AGEGR1" (with 3 categorical levels), you might not love the functionality in the chart. Fortunately, it's easy to go back and update the chart to use the categorized variable instead - just go back to the settings tab and update the corresponding setting. ### 3. Flag Rows of Special Interest From 15848bb7e1e74bc9415fe88ee99dee1a37c8cf32 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Thu, 23 May 2019 10:26:18 -0400 Subject: [PATCH 15/39] fix #318 --- .../modules/renderSettings/renderSettings.R | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R index 287495ef..c6c0e910 100644 --- a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R +++ b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R @@ -49,8 +49,8 @@ renderSettings <- function(input, output, session, data, settings, status){ ns <- session$ns - charts<-as.vector(filter(chartsMetadata, chart %in% all_charts)[["chart"]]) - labels<-as.vector(filter(chartsMetadata, chart %in% all_charts)[["label"]]) + charts<-as.vector(chartsMetadata[["chart"]]) + labels<-as.vector(chartsMetadata[["label"]]) names(charts)<-labels output$charts_wrap_ui <- renderUI({ @@ -190,35 +190,43 @@ renderSettings <- function(input, output, session, data, settings, status){ filter_expr = field_column_key==!!col ) - # Toggle field-level inputs: - # ON - if column-level input is selected) - # OFF - if column-level input is not yet selected - for (fk in field_keys){ - toggleState(id = fk, condition = !input[[col]]=="") - } - - if (is.null(isolate(settings()[[col]])) || ! input[[col]] == isolate(settings()[[col]])){ - - if (input[[col]] %in% colnames(data())){ - choices <- unique(data()[,input[[col]]]) - placeholder <- "Please select a value" - } else { - choices <- NULL - placeholder <- paste0("Please select a ", getSettingsMetadata(col="label", text_key=col)) - } - for (key in field_keys){ + # Toggle field-level inputs: + # ON - if column-level input is selected) + # OFF - if column-level input is not yet selected + toggleState(id = key, condition = !input[[col]]=="") + + # If it is the default column - populate standards + if(input[[col]] == isolate(settings()[[col]]) && !is.null(isolate(settings()[[col]]))) { + setting_key <- as.list(strsplit(key,"\\-\\-")) + setting_value <- safetyGraphics:::getSettingValue(key=setting_key, settings= isolate(settings())) + choices <- unique(c(setting_value, sort(as.character(data()[,input[[col]]])))) %>% unlist + placeholder <- list (onInitialize = I('function() { }')) + + # If it's another column display placeholder message and set to empty + } else if(input[[col]] %in% colnames(data())) { + choices <- unique(data()[,input[[col]]]) + placeholder <- list( + placeholder = "Please select a value", + onInitialize = I('function() { + this.setValue("");}') + ) + # If empty display different placeholder message + } else { + choices <- NULL + placeholder <- list( + placeholder = paste0("Please select a ", getSettingsMetadata(col="label", text_key=col)), + onInitialize = I('function() { + this.setValue("");}') + ) + } updateSelectizeInput( session, inputId = key, choices = choices, - options = list( - placeholder = placeholder, - onInitialize = I('function() {this.setValue("");}') - ) + options = placeholder ) #update SelectizeInput } #for loop - }#if #1 } #observeEvent (inner) ) #observeEvent (outer) }) #lapply @@ -244,7 +252,7 @@ renderSettings <- function(input, output, session, data, settings, status){ req(input_names()) keys <- input_names() values<- keys %>% map(~getValues(.x)) - + inputDF <- tibble(text_key=keys, customValue=values)%>% rowwise %>% filter(!is.null(customValue[[1]])) From 8e68a2e81ba5b90ab8601bc4c52765c587019dab Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Thu, 23 May 2019 12:51:32 -0400 Subject: [PATCH 16/39] revert mysterious change --- .../modules/renderSettings/renderSettings.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R index c6c0e910..43929361 100644 --- a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R +++ b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R @@ -49,8 +49,8 @@ renderSettings <- function(input, output, session, data, settings, status){ ns <- session$ns - charts<-as.vector(chartsMetadata[["chart"]]) - labels<-as.vector(chartsMetadata[["label"]]) + charts<-as.vector(filter(chartsMetadata, chart %in% all_charts)[["chart"]]) + labels<-as.vector(filter(chartsMetadata, chart %in% all_charts)[["label"]]) names(charts)<-labels output$charts_wrap_ui <- renderUI({ From 79b6239a6a07e3b8f72f37b1e122050cbd947208 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Thu, 23 May 2019 14:53:45 -0400 Subject: [PATCH 17/39] slight tweak to workflow --- .../modules/renderSettings/renderSettings.R | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R index 43929361..c2899ad8 100644 --- a/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R +++ b/inst/safetyGraphics_app/modules/renderSettings/renderSettings.R @@ -190,36 +190,44 @@ renderSettings <- function(input, output, session, data, settings, status){ filter_expr = field_column_key==!!col ) + ### SET UP CHOICES/PLACEHOLDERS FOR SELECT INPUT UPDATES + # If it is the default column - populate standards + if(input[[col]] == isolate(settings()[[col]]) && !is.null(isolate(settings()[[col]]))) { + choices <- unique(data()[,input[[col]]]) + placeholder <- list (onInitialize = I('function() { }')) + + # If it's another column display placeholder message and set to empty + } else if(input[[col]] %in% colnames(data())) { + choices <- unique(data()[,input[[col]]]) + placeholder <- list( + placeholder = "Please select a value", + onInitialize = I('function() { + this.setValue("");}') + ) + # If empty display different placeholder message + } else { + choices <- NULL + placeholder <- list( + placeholder = paste0("Please select a ", getSettingsMetadata(col="label", text_key=col)), + onInitialize = I('function() { + this.setValue("");}') + ) + } + + # update selectInput for each field value for (key in field_keys){ # Toggle field-level inputs: # ON - if column-level input is selected) # OFF - if column-level input is not yet selected toggleState(id = key, condition = !input[[col]]=="") - # If it is the default column - populate standards + # if specified in original settings object - append value to choices if(input[[col]] == isolate(settings()[[col]]) && !is.null(isolate(settings()[[col]]))) { setting_key <- as.list(strsplit(key,"\\-\\-")) setting_value <- safetyGraphics:::getSettingValue(key=setting_key, settings= isolate(settings())) - choices <- unique(c(setting_value, sort(as.character(data()[,input[[col]]])))) %>% unlist - placeholder <- list (onInitialize = I('function() { }')) - - # If it's another column display placeholder message and set to empty - } else if(input[[col]] %in% colnames(data())) { - choices <- unique(data()[,input[[col]]]) - placeholder <- list( - placeholder = "Please select a value", - onInitialize = I('function() { - this.setValue("");}') - ) - # If empty display different placeholder message - } else { - choices <- NULL - placeholder <- list( - placeholder = paste0("Please select a ", getSettingsMetadata(col="label", text_key=col)), - onInitialize = I('function() { - this.setValue("");}') - ) + choices <- unique(c(setting_value, choices)) } + updateSelectizeInput( session, inputId = key, From c50df6bead09848f621903334fa7de117e357637 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Fri, 24 May 2019 08:29:08 -0400 Subject: [PATCH 18/39] import shiny::shinyOptions --- NAMESPACE | 1 + 1 file changed, 1 insertion(+) diff --git a/NAMESPACE b/NAMESPACE index 5f863bbb..cd911745 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,7 @@ importFrom(purrr,map_lgl) importFrom(rlang,.data) importFrom(rlang,parse_expr) importFrom(shiny,runApp) +importFrom(shiny,shinyOptions) importFrom(shinyWidgets,materialSwitch) importFrom(stringr,str_detect) importFrom(stringr,str_split) From f45d66330bec5b27960ff81311f86bb2f8ebd851 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Fri, 24 May 2019 08:29:28 -0400 Subject: [PATCH 19/39] fix binding for global var --- R/generateSettings.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/generateSettings.R b/R/generateSettings.R index 32042f41..5f01d57a 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -128,7 +128,7 @@ generateSettings <- function(standard="None", charts=NULL, useDefaults=TRUE, par data_mappings <- safetyGraphics::getSettingsMetadata( charts = charts, cols="text_key", - filter_expr=column_mapping + filter_expr=.data$column_mapping ) for(text_key in data_mappings){ key <- textKeysToList(text_key)[[1]] From cfa18e5a76269195178aec753d484cfaaae9876c Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Fri, 24 May 2019 14:34:04 -0400 Subject: [PATCH 20/39] add visit settings to metadata --- data-raw/settingsMetadata.csv | 5 ++++- data-raw/settingsMetadataCharts.csv | 5 ++++- data-raw/standardsMetadata.csv | 5 ++++- data/chartsMetadata.rda | Bin 834 -> 841 bytes data/settingsMetadata.rda | Bin 2210 -> 2259 bytes data/standardsMetadata.rda | Bin 628 -> 658 bytes 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/data-raw/settingsMetadata.csv b/data-raw/settingsMetadata.csv index 9108439f..ec8a8cd9 100644 --- a/data-raw/settingsMetadata.csv +++ b/data-raw/settingsMetadata.csv @@ -27,4 +27,7 @@ warningText,Warning text,"Informational text to be displayed near the top of the unit_col,Unit column,Unit of measure variable name,character,FALSE,TRUE,character,FALSE,,data start_value,Measure start value,Value of variable defined in measure_col to be rendered in the histogram when the widget loads,character,FALSE,FALSE,NA,TRUE,measure_col,data details,Details columns,"An optional list of specifications for details listing. Each column to be added to details listing is a nested, named list (containing the variable name: ""value_col"" and associated label: ""label"") within the larger list.",vector,FALSE,TRUE,NA,FALSE,,data -missingValues,Missing values,Values defining a missing value in the selected 'value' column,vector,FALSE,FALSE,NA,FALSE,,data \ No newline at end of file +missingValues,Missing values,Values defining a missing value in the selected 'value' column,vector,FALSE,FALSE,NA,FALSE,,data +visits_without_data,Visits without data,Controls display of visits without data for the current measure,logical,FALSE,FALSE,NA,FALSE,,data +unscheduled_visits,Unscheduled visits,Controls display of unscheduled visits,logical,FALSE,FALSE,NA,FALSE,,data +unscheduled_visit_pattern,Unscheduled visit pattern,A regular expression that identifies unscheduled visits,character,FALSE,FALSE,NA,FALSE,,data diff --git a/data-raw/settingsMetadataCharts.csv b/data-raw/settingsMetadataCharts.csv index ba404f6b..009a001e 100644 --- a/data-raw/settingsMetadataCharts.csv +++ b/data-raw/settingsMetadataCharts.csv @@ -27,4 +27,7 @@ warningText,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE unit_col,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE start_value,FALSE,TRUE,TRUE,TRUE,TRUE,FALSE details,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE -missingValues,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE \ No newline at end of file +missingValues,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE +visits_without_data,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE +unscheduled_visits,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE +unscheduled_visit_pattern,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE diff --git a/data-raw/standardsMetadata.csv b/data-raw/standardsMetadata.csv index 57f74f6c..07739c31 100644 --- a/data-raw/standardsMetadata.csv +++ b/data-raw/standardsMetadata.csv @@ -27,4 +27,7 @@ warningText,, unit_col,STRESU, start_value,, details,, -missingValues,, \ No newline at end of file +missingValues,, +visits_without_data,, +unscheduled_visits,, +unscheduled_visit_pattern,, diff --git a/data/chartsMetadata.rda b/data/chartsMetadata.rda index bfeeb2bd573ad166030d71b9694ecbed5e35a414..4ae7ffdd2720187feadb420d49d69d1ca1115ac9 100644 GIT binary patch delta 834 zcmV-I1HJsh2FV5yLRx4!F+o`-Q&}ww-&6nrrI8U9e}ECt13ntI3eif2l4y+qp!9=H z00001pa2;F2_%!zpr)Ra6KV|r&}aYv01r?AIBBK`WHi$wCJ+rY0ff<}OaLHgaMMf> z$Z4iVOduL)0|}!{m;gY~NRWaWWJ79G-lJ1Qc}+bHOpP$o8Z;Y79%W4m_nPXnr4$2H zmWGA#f9-@vZ2y>(9MTr;{r8K5bbI6EKa2h-k`ctyXu_?Qm_>1HuyvxU+m$Ha^;2!w zyp#32YNc6-oF`w8Kno-dp&@_+4p>F(Kp8ZI9|z}Pl$aZ|cDxB?+RaLG)dg=FA)gg+ ziUm_xl&ooq$mS*Y9eTkeV~ztCb0x$LSQdjVe{IarKunRplK@@}+q(gOqSgzr7annd zqSHS0Gnq{QOcy$*6pPXBdxn{YYlUgpVxze-2y1O%t>#`FC!r^%(|0-Dpb+LqNE;@l z4ub{$xi5X| zfA3QTR|N|+lNuu*&Vrd^inLtO8n`f@N9^#DC$kb4H6yPRLOie{^5aPX1c)Ov8A|Fl z9G%`mNxwk9?7bi<@ix3CFbi0+TXhOcp7BWO1j_|oQWp)&FYIz#RwV-&S#W|E1QZER z1m{-UtkR9RqZwj=UqMVNOcHkv8U^yee~BdckEm1^O^vtF%6H0xu`w5O#ADEP8b;%& z%moNw7il)zQYuZTJlEB7CJ|-o!bb4JG^Bt&u{3!>KZV~ZU@OrD!iwh7V>~!|#8x^& zC@4$r;U`N?Vld7~TESW)&MpnBB##png_ZS&Uu+&MZ!6R2?lWiR&%&qDk!{6Le+qnZ zNNgCvUKO_cGetpuM2L|T#w4Wasf?4e6+Mk7U1B&D4nt!&%*H^yvo+viBfGqPjt8H= z=vxhzhJG6ANu>yU%@~IvvM0qdTH><12rAp2!ho$VHr;L3N~)@Gh{ZW*QPE7mo<3DW z48<~nvf(2G6PPS2zoyi=1kt>|D5H4*#(o#bklq8*Euw&L8e*h8C13mJJw}8*Mr22* z00*dI10ziUGfVcNY?;XSWKaB*0a5UO6s~#JS@nl^r?!;ANa-|#j)Z2D0 znfqO}QmcrZCtpS2EomVz0Aa@n#iRkPq#*b{wh2jryDw|Rmmh00smnYa)EtLNcUdb9 zS5DKocNq$vd+scLVB3Bh@{_boRmWF)mti zMeBlQPxHkSUa}c~GC0U6C%UZN1c5U1Dh8$3`$&w-*b1%s ze->sYG)A9w1v1AKWxb*^@8dnc2aJ(E9V9MFM^=+6JrE-T^DzMgh$84U%KA1OUHv4J zePaK0`oL4*ZFvqr7Vx#LnZ+f|fTVMRWrD7$3q|W+%yL^6l7n_~VrbwI0ox+Uj*2)t zAx20|e#k7M)?1S*R~}&(?BX7iN_mAre{^3u~=DOP;2zT<4 zeg`PBoRy?W61cw8B6!qS@4-=?Xy9d58iygVnv0EKyC!d9X%XMxKh41MczsKR;j}Vn zuB4h!hse>SIF1;f1j%njWp)r%vn7QAT3&3v-UgPfTA)Qj$Y&KXAtFO~k1)0h4Gz5HOw6o^Q~U?^pu>NQxgwk>NIH*v FxPUAEa=HKj diff --git a/data/settingsMetadata.rda b/data/settingsMetadata.rda index 09da9b522120ad18246ba30f13bc7dcc90c483eb..54c4dbf1d2a52479f1e941f3a4a2a0f9a4339f65 100644 GIT binary patch literal 2259 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 Date: Fri, 24 May 2019 15:30:39 -0400 Subject: [PATCH 21/39] remove char --- data-raw/chartsMetadata.csv | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/data-raw/chartsMetadata.csv b/data-raw/chartsMetadata.csv index 9c943973..eee344c1 100644 --- a/data-raw/chartsMetadata.csv +++ b/data-raw/chartsMetadata.csv @@ -1,7 +1 @@ -chart,main,label,description,repo_url,settings_url,type,maxWidth -edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 -safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 -safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 -safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 -safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 -paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 \ No newline at end of file +chart,main,label,description,repo_url,settings_url,type,maxWidth edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 From d01ca509634be71940d2c3d21cf9c5584f5bd196 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Fri, 24 May 2019 15:30:55 -0400 Subject: [PATCH 22/39] replace double || with single | --- R/evaluateStandard.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/evaluateStandard.R b/R/evaluateStandard.R index ffe13068..1fdebcdf 100644 --- a/R/evaluateStandard.R +++ b/R/evaluateStandard.R @@ -37,7 +37,7 @@ evaluateStandard <- function(data, standard, includeFields=TRUE, domain="labs"){ # Get metadata for settings using the specified standard and see if required data elements are found standardChecks <- getSettingsMetadata(cols=c("text_key", "column_mapping", "field_mapping", "field_column_key", "setting_required","standard_val",standard)) %>% rename("standard_val"=standard) %>% - filter(.data$column_mapping == TRUE || .data$field_mapping ==TRUE) %>% + filter(.data$column_mapping == TRUE | .data$field_mapping ==TRUE) %>% filter(.data$setting_required==TRUE) %>% mutate(type = ifelse(.data$column_mapping, "column", "field")) %>% rowwise %>% From 0a95af2be127cb437a9c7f829ba27e2e6735fc8c Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Fri, 24 May 2019 15:32:46 -0400 Subject: [PATCH 23/39] fix weird character --- data-raw/chartsMetadata.csv | 8 +++++++- data/chartsMetadata.rda | Bin 841 -> 834 bytes 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/data-raw/chartsMetadata.csv b/data-raw/chartsMetadata.csv index eee344c1..8041dce4 100644 --- a/data-raw/chartsMetadata.csv +++ b/data-raw/chartsMetadata.csv @@ -1 +1,7 @@ -chart,main,label,description,repo_url,settings_url,type,maxWidth edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 +chart,main,label,description,repo_url,settings_url,type,maxWidth +edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 +safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 +safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 +safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 +safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 +paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 diff --git a/data/chartsMetadata.rda b/data/chartsMetadata.rda index 4ae7ffdd2720187feadb420d49d69d1ca1115ac9..bfeeb2bd573ad166030d71b9694ecbed5e35a414 100644 GIT binary patch delta 827 zcmV-B1H}Bv2EqmrLRx4!F+o`-Q(1K$`EdXNq>&L8e*h8C13mJJw}8*Mr22* z00*dI10ziUGfVcNY?;XSWKaB*0a5UO6s~#JS@nl^r?!;ANa-|#j)Z2D0 znfqO}QmcrZCtpS2EomVz0Aa@n#iRkPq#*b{wh2jryDw|Rmmh00smnYa)EtLNcUdb9 zS5DKocNq$vd+scLVB3Bh@{_boRmWF)mti zMeBlQPxHkSUa}c~GC0U6C%UZN1c5U1Dh8$3`$&w-*b1%s ze->sYG)A9w1v1AKWxb*^@8dnc2aJ(E9V9MFM^=+6JrE-T^DzMgh$84U%KA1OUHv4J zePaK0`oL4*ZFvqr7Vx#LnZ+f|fTVMRWrD7$3q|W+%yL^6l7n_~VrbwI0ox+Uj*2)t zAx20|e#k7M)?1S*R~}&(?BX7iN_mAre{^3u~=DOP;2zT<4 zeg`PBoRy?W61cw8B6!qS@4-=?Xy9d58iygVnv0EKyC!d9X%XMxKh41MczsKR;j}Vn zuB4h!hse>SIF1;f1j%njWp)r%vn7QAT3&3v-UgPfTA)Qj$Y&KXAtFO~k1)0h4Gz5HOw6o^Q~U?^pu>NQxgwk>NIH*v FxPUAEa=HKj delta 834 zcmV-I1HJsh2FV5yLRx4!F+o`-Q&}ww-&6nrrI8U9e}ECt13ntI3eif2l4y+qp!9=H z00001pa2;F2_%!zpr)Ra6KV|r&}aYv01r?AIBBK`WHi$wCJ+rY0ff<}OaLHgaMMf> z$Z4iVOduL)0|}!{m;gY~NRWaWWJ79G-lJ1Qc}+bHOpP$o8Z;Y79%W4m_nPXnr4$2H zmWGA#f9-@vZ2y>(9MTr;{r8K5bbI6EKa2h-k`ctyXu_?Qm_>1HuyvxU+m$Ha^;2!w zyp#32YNc6-oF`w8Kno-dp&@_+4p>F(Kp8ZI9|z}Pl$aZ|cDxB?+RaLG)dg=FA)gg+ ziUm_xl&ooq$mS*Y9eTkeV~ztCb0x$LSQdjVe{IarKunRplK@@}+q(gOqSgzr7annd zqSHS0Gnq{QOcy$*6pPXBdxn{YYlUgpVxze-2y1O%t>#`FC!r^%(|0-Dpb+LqNE;@l z4ub{$xi5X| zfA3QTR|N|+lNuu*&Vrd^inLtO8n`f@N9^#DC$kb4H6yPRLOie{^5aPX1c)Ov8A|Fl z9G%`mNxwk9?7bi<@ix3CFbi0+TXhOcp7BWO1j_|oQWp)&FYIz#RwV-&S#W|E1QZER z1m{-UtkR9RqZwj=UqMVNOcHkv8U^yee~BdckEm1^O^vtF%6H0xu`w5O#ADEP8b;%& z%moNw7il)zQYuZTJlEB7CJ|-o!bb4JG^Bt&u{3!>KZV~ZU@OrD!iwh7V>~!|#8x^& zC@4$r;U`N?Vld7~TESW)&MpnBB##png_ZS&Uu+&MZ!6R2?lWiR&%&qDk!{6Le+qnZ zNNgCvUKO_cGetpuM2L|T#w4Wasf?4e6+Mk7U1B&D4nt!&%*H^yvo+viBfGqPjt8H= z=vxhzhJG6ANu>yU%@~IvvM0qdTH><12rAp2!ho$VHr;L3N~)@Gh{ZW*QPE7mo<3DW z48<~nvf(2G6PPS2zoyi=1kt>|D5H4*#(o#bklq8*Euw Date: Fri, 24 May 2019 22:18:45 -0400 Subject: [PATCH 24/39] fix chartsMetadata.csv and update rda files --- data-raw/chartsMetadata.csv | 10 ---------- data/chartsMetadata.rda | Bin 817 -> 813 bytes data/settingsMetadata.rda | Bin 2259 -> 2267 bytes 3 files changed, 10 deletions(-) diff --git a/data-raw/chartsMetadata.csv b/data-raw/chartsMetadata.csv index c16f9519..500468c1 100644 --- a/data-raw/chartsMetadata.csv +++ b/data-raw/chartsMetadata.csv @@ -1,12 +1,3 @@ -<<<<<<< HEAD -chart,main,label,description,repo_url,settings_url,type,maxWidth -edish,safetyedish,eDish,Interactive graphic for the Evaluation of Drug-Induced Serious Hepatotoxicity (eDISH),https://github.com/SafetyGraphics/safety-eDISH,https://github.com/SafetyGraphics/safety-eDISH/wiki/Configuration,htmlwidget,620 -safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 -safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 -safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 -safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 -paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 -======= chart,main,label,description,repo_url,settings_url,type,maxWidth hepexplorer,hepexplorer,Hepatic Safety Explorer,Interactive Graphic for Exploring Liver Function Data in Clinical Trials,https://github.com/SafetyGraphics/hep-explorer,https://github.com/SafetyGraphics/hep-explorer/wiki/Configuration,htmlwidget,620 safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 @@ -14,4 +5,3 @@ safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlig safetyshiftplot,safetyShiftPlot,Shift Plot,Shift Plot for Safety Explorer,https://github.com/RhoInc/safety-shift-plot,https://github.com/RhoInc/safety-shift-plot/wiki/Configuration,htmlwidget,620 safetyresultsovertime,safetyResultsOverTime,Results Over Time,Population Test Results Over Time,https://github.com/RhoInc/safety-results-over-time,https://github.com/RhoInc/safety-results-over-time/wiki/Configuration,htmlwidget,1000 paneledoutlierexplorer,paneledOutlierExplorer,Paneled Outlier Explorer,Charts showing clinical participant results for multiple measures,https://github.com/RhoIncpaneled-outlier-explorer,https://github.com/RhoInc/paneled-outlier-explorer/wiki/Configuration,htmlwidget,1000 ->>>>>>> update-hep-explorer diff --git a/data/chartsMetadata.rda b/data/chartsMetadata.rda index ead2821ed7192b6cc45849a73d12ea839967c704..d29a438fe677d84103592e27105cb6567d7754c0 100644 GIT binary patch delta 806 zcmV+>1KIqs2CW7WLRx4!F+o`-Q&~!*1Lpt%qLC36e}ECt132+0w!kbjiakKc0Av~s z00ThNKmdAx0l+YfCYXi+0LTG^X_F=qfQEuZgeIq@KS58@k0{U@X!RNz4GjPurcDDi z2LQq_nqn9P10V(wrc9Vd0vZVvBT1&3r{zy-F*MX zsXACh;X1QgBxy;sk{Ey(YJ_BB0MO7N`aU*xubhTy6b$4pR2aXEZqjYJ6_5!Le?Ph{1$}*dpt=QcE>KmQXf9L2`%O-; zOgO#{(Q`!Au$v6zxV6i3P>q*Z-;YglC+mwJ9|x*cjaHpbBNq(awV8T`O73^(z2BvXY61gbDTaB(_Pt^fc4 delta 810 zcmV+_1J(Sk2C)VaLRx4!F+o`-Q(4t)9mW6wp^*_5e*h8C13BR;s-PjpOqwwbG%yoG z6GoXa10xBeO)!HdILVVnA*O}`Xkuv7CLm;CG-;*~WWq#~Q!02*)X`5V>M;T842?FC z=@|_JCJ;TU4l-oXh-smKni!fi$%q*kO&V#088DGTXeLSNPsvXp)MR=f0ib9A4=7@2 zg-3rCf9wK@rE8D}PgEj0IDdI-wW0NrR1eIJdXD6|#lY@JNT znT|uEa(Zkj+mgd*@>)*9&*2RcC>hFLs4M4Ve|btW{!aM1;-!8QVQDRU(!rodJ{ueK_bZWCx-5L2t~pm|h#AGnRaqLke{5Fb zUNv2k{Fj6=fw#EahH6twSyB7ei7rwMw}~6KxRgv#yIK7N%azC&aC{;twE~`5kzF8Y ze~kRI6_boKXtL@^PdLND)+UVSZIKeNtwex8ih&BCLb)~>`a7gkd?Nkk)Ff5UkcdZz zK#`DH*LKlJC@3_7Wg?UXhYrEa-y~&SqcGjt?=j9If*pZv`|R1t%|^|$?{Vb?aJ%6e zcLE^g7R})jN%qemrY|jRx2J7%+bNE)e_}YCyTzk?o#Rj`*~l-6Uxn@^xOlSAc{`Ad z0x7?FO#!H5ch!J;#R%X8{+nNUS}bUTD4|T4n;7_WbcnX1Z4(p_a`nkh!vPoD5p{u9 zBLQKlZ=g6>syso#N}-a1n0-V_nkarGI& ojLbm;bRN*RMFwhXZ8pZx@}!Oa5WxR#jDPWWBvXY64OYS2VB`I6IsgCw diff --git a/data/settingsMetadata.rda b/data/settingsMetadata.rda index 54c4dbf1d2a52479f1e941f3a4a2a0f9a4339f65..1a225a8e3f2ee117ec1ff2925fdc07d42413abfa 100644 GIT binary patch delta 2253 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 Tr9hgMKlrsvb|cumb{X From 6c36ee568789b5813944cb0a3c6a85256f3ee7e8 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Fri, 24 May 2019 22:39:41 -0400 Subject: [PATCH 25/39] begin fixing tests for hepexplorer --- tests/testthat/test_generateSettings.R | 6 +++--- tests/testthat/test_getSettingsMetadata.R | 16 ++++++++-------- tests/testthat/test_trimSettings.R | 2 +- tests/testthat/test_validateSettings.R | 18 +++++++++--------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/testthat/test_generateSettings.R b/tests/testthat/test_generateSettings.R index 95e4c494..a5300269 100644 --- a/tests/testthat/test_generateSettings.R +++ b/tests/testthat/test_generateSettings.R @@ -18,9 +18,9 @@ test_that("a list with the expected properties and structure is returned for all test_that("a warning is thrown if chart isn't found in the chart list",{ expect_error(generateSettings(chart="aeexplorer")) expect_error(generateSettings(chart="")) - expect_silent(generateSettings(chart="eDish")) - expect_silent(generateSettings(chart="edish")) - expect_silent(generateSettings(chart="EdIsH")) + expect_silent(generateSettings(chart="hepExplorer")) + expect_silent(generateSettings(chart="hepexplorer")) + expect_silent(generateSettings(chart="HepexploreR")) }) test_that("data mappings are null when setting=none, character otherwise",{ diff --git a/tests/testthat/test_getSettingsMetadata.R b/tests/testthat/test_getSettingsMetadata.R index b4539891..535e5dbe 100644 --- a/tests/testthat/test_getSettingsMetadata.R +++ b/tests/testthat/test_getSettingsMetadata.R @@ -43,12 +43,12 @@ test_that("Pulling from a custom metadata file works as expected",{ test_that("charts parameter works as expected",{ #return a dataframe for valid input - expect_is(safetyGraphics:::getSettingsMetadata(charts=c("edish")),"data.frame") - expect_is(safetyGraphics:::getSettingsMetadata(charts="edish"),"data.frame") + expect_is(safetyGraphics:::getSettingsMetadata(charts=c("hepexplorer")),"data.frame") + expect_is(safetyGraphics:::getSettingsMetadata(charts="hepexplorer"),"data.frame") #error if charts isn't a character expect_error(safetyGraphics:::getSettingsMetadata(charts=123)) - expect_error(safetyGraphics:::getSettingsMetadata(charts=list("edish"))) + expect_error(safetyGraphics:::getSettingsMetadata(charts=list("hepexplorer"))) #return null if no valid charts are passed expect_true(is.null(safetyGraphics:::getSettingsMetadata(charts=c("")))) @@ -57,13 +57,13 @@ test_that("charts parameter works as expected",{ expect_true(is.null(safetyGraphics:::getSettingsMetadata(charts=c("notachart","stillnotachart")))) #no partial matches supported - expect_true(is.null(safetyGraphics:::getSettingsMetadata(charts=c("edi")))) + expect_true(is.null(safetyGraphics:::getSettingsMetadata(charts=c("hepexplore")))) #return a dataframe if at least one valid chart type is passed - expect_is(safetyGraphics:::getSettingsMetadata(charts=c("notachart","edish")),"data.frame") + expect_is(safetyGraphics:::getSettingsMetadata(charts=c("notachart","hepexplorer")),"data.frame") #capitalization doesn't matter - expect_is(safetyGraphics:::getSettingsMetadata(charts=c("EdIsH")),"data.frame") + expect_is(safetyGraphics:::getSettingsMetadata(charts=c("HepexploreR")),"data.frame") #get the right number of records with various combinations lineonly <- safetyGraphics:::getSettingsMetadata(charts=c("linechart"),metadata=mergedMetadata) @@ -134,8 +134,8 @@ test_that("filter_expr parameters works as expected",{ safetyGraphics:::getSettingsMetadata(text_key="id_col") ) expect_equal(safetyGraphics:::getSettingsMetadata(filter_expr=text_key=="id_col",cols="description"),"Unique subject identifier variable name.") - expect_length(safetyGraphics:::getSettingsMetadata(filter_expr=column_type=="numeric",cols="text_key",chart="edish"),5) - expect_length(safetyGraphics:::getSettingsMetadata(filter_expr=setting_required,cols="text_key",chart="edish"),12) + expect_length(safetyGraphics:::getSettingsMetadata(filter_expr=column_type=="numeric",cols="text_key",chart="hepexplorer"),5) + expect_length(safetyGraphics:::getSettingsMetadata(filter_expr=setting_required,cols="text_key",chart="hepexplorer"),12) }) test_that("add_standards parameters works as expected",{ diff --git a/tests/testthat/test_trimSettings.R b/tests/testthat/test_trimSettings.R index f979701b..5855609a 100644 --- a/tests/testthat/test_trimSettings.R +++ b/tests/testthat/test_trimSettings.R @@ -9,7 +9,7 @@ test_that("returns a list with settings from all charts",{ }) test_that("subsets vector appropriately",{ - expect_equal(length(trimSettings(settings=testsettings, charts=c("edish","safetyhistogram"))),24) + expect_equal(length(trimSettings(settings=testsettings, charts=c("hepexplorer","safetyhistogram"))),24) }) test_that("subsets single chart appropriately",{ diff --git a/tests/testthat/test_validateSettings.R b/tests/testthat/test_validateSettings.R index 8ec05ed6..03ee3b62 100644 --- a/tests/testthat/test_validateSettings.R +++ b/tests/testthat/test_validateSettings.R @@ -129,13 +129,13 @@ test_that("validateSettings returns the expected charts object",{ # At least one chart is invalid when overal status is invalid expect_false(failed[["charts"]]%>%map_lgl(~.x)%>%all) - # eDish is the only invalid chart when a measure value is invalidated - edishFail_settings <- validSettings - edishFail_settings[["measure_values"]][["AST"]]<-"INVALID!" - edishFail_validation<-validateSettings(data=adlbc, settings=edishFail_settings) - expect_false(edishFail_validation$valid) - expect_false(edishFail_validation$charts%>%map_lgl(~.x)%>%all) - expect_false(edishFail_validation[["charts"]][["edish"]]) #edish is invalid - edishFail_validation[["charts"]][["edish"]]<-NULL - expect_true(edishFail_validation$charts%>%map_lgl(~.x)%>%all) #all other charts are valid + # hepexplorer is the only invalid chart when a measure value is invalidated + hepexplorerFail_settings <- validSettings + hepexplorerFail_settings[["measure_values"]][["AST"]]<-"INVALID!" + hepexplorerFail_validation<-validateSettings(data=adlbc, settings=hepexplorerFail_settings) + expect_false(hepexplorerFail_validation$valid) + expect_false(hepexplorerFail_validation$charts%>%map_lgl(~.x)%>%all) + expect_false(hepexplorerFail_validation[["charts"]][["hepexplorer"]]) #hepexplorer is invalid + hepexplorerFail_validation[["charts"]][["hepexplorer"]]<-NULL + expect_true(hepexplorerFail_validation$charts%>%map_lgl(~.x)%>%all) #all other charts are valid }) From c46f18df950550d162bbb7d2d70fe9b117ea27de Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Tue, 28 May 2019 09:54:19 -0400 Subject: [PATCH 26/39] clearn warnings and errors --- R/generateShell.R | 2 +- R/settingsMetadata.R | 2 +- data-raw/chartsMetadata.csv | 2 +- data-raw/generateSettingsMetadataDefaults.R | 2 ++ data-raw/settingsMetadataDefaults.Rds | Bin 607 -> 607 bytes data/chartsMetadata.rda | Bin 813 -> 817 bytes man/generateShell.Rd | 2 +- man/settingsMetadata.Rd | 2 +- tests/testthat/test_getSettingsMetadata.R | 2 +- 9 files changed, 8 insertions(+), 6 deletions(-) diff --git a/R/generateShell.R b/R/generateShell.R index f0dac6d7..bcb94961 100644 --- a/R/generateShell.R +++ b/R/generateShell.R @@ -9,7 +9,7 @@ #' #' @examples #' -#' safetyGraphics:::generateShell(charts = "eDish") +#' safetyGraphics:::generateShell(charts = "hepexplorer") #' #' @keywords internal diff --git a/R/settingsMetadata.R b/R/settingsMetadata.R index 8bee0c56..3a8000cb 100644 --- a/R/settingsMetadata.R +++ b/R/settingsMetadata.R @@ -4,7 +4,7 @@ #' #' @format A data frame with 29 rows and 17 columns #' \describe{ -#' \item{chart_edish}{Flag indicating if the settings apply to the eDish Chart} +#' \item{chart_hepexplorer}{Flag indicating if the settings apply to the Hepatic Explorer Chart} #' \item{chart_paneledoutlierexplorer}{Flag indicating if the settings apply to the Paneled Safety Outlier Explorer Chart} #' \item{chart_safetyhistogram}{Flag indicating if the settings apply to the Safety Histogram Chart} #' \item{chart_safetyoutlierexplorer}{Flag indicating if the settings apply to the Safety Outlier Explorer Chart} diff --git a/data-raw/chartsMetadata.csv b/data-raw/chartsMetadata.csv index 500468c1..40df362e 100644 --- a/data-raw/chartsMetadata.csv +++ b/data-raw/chartsMetadata.csv @@ -1,4 +1,4 @@ -chart,main,label,description,repo_url,settings_url,type,maxWidth +chart,main,label,description,repo_url,settings_url,type,maxWidth hepexplorer,hepexplorer,Hepatic Safety Explorer,Interactive Graphic for Exploring Liver Function Data in Clinical Trials,https://github.com/SafetyGraphics/hep-explorer,https://github.com/SafetyGraphics/hep-explorer/wiki/Configuration,htmlwidget,620 safetyhistogram,safetyHistogram,Histogram,"Histogram showing distribution of lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-histogram,https://github.com/RhoInc/safety-histogram/wiki/Configuration,htmlwidget,1000 safetyoutlierexplorer,safetyOutlierExplorer,Outlier Explorer,"Line Chart highlighting abnormal lab measures, vital signs and other measures related to safety in clinical trials.",https://github.com/RhoInc/safety-outlier-explorer,https://github.com/RhoInc/safety-outlier-explorer/wiki/Configuration,htmlwidget,1000 diff --git a/data-raw/generateSettingsMetadataDefaults.R b/data-raw/generateSettingsMetadataDefaults.R index 186eb234..9bc50ec2 100644 --- a/data-raw/generateSettingsMetadataDefaults.R +++ b/data-raw/generateSettingsMetadataDefaults.R @@ -1,3 +1,5 @@ +library(tibble) + defaults <- tribble(~text_key, ~default, "id_col", NULL, "value_col", NULL, diff --git a/data-raw/settingsMetadataDefaults.Rds b/data-raw/settingsMetadataDefaults.Rds index b6784b6caa2faaa583d611d6540c449ba09573fc..988aeabe83074b137c5937e5912e8d02e105684b 100644 GIT binary patch delta 19 Xcmcc5a-W4uzMF#q4A?eug)#vEEm{Mc delta 19 Xcmcc5a-W4uzMF#q445}^g)#vEElUHK diff --git a/data/chartsMetadata.rda b/data/chartsMetadata.rda index d29a438fe677d84103592e27105cb6567d7754c0..ead2821ed7192b6cc45849a73d12ea839967c704 100644 GIT binary patch delta 810 zcmV+_1J(Sk2C)VaLRx4!F+o`-Q(4t)9mW6wp^*_5e*h8C13BR;s-PjpOqwwbG%yoG z6GoXa10xBeO)!HdILVVnA*O}`Xkuv7CLm;CG-;*~WWq#~Q!02*)X`5V>M;T842?FC z=@|_JCJ;TU4l-oXh-smKni!fi$%q*kO&V#088DGTXeLSNPsvXp)MR=f0ib9A4=7@2 zg-3rCf9wK@rE8D}PgEj0IDdI-wW0NrR1eIJdXD6|#lY@JNT znT|uEa(Zkj+mgd*@>)*9&*2RcC>hFLs4M4Ve|btW{!aM1;-!8QVQDRU(!rodJ{ueK_bZWCx-5L2t~pm|h#AGnRaqLke{5Fb zUNv2k{Fj6=fw#EahH6twSyB7ei7rwMw}~6KxRgv#yIK7N%azC&aC{;twE~`5kzF8Y ze~kRI6_boKXtL@^PdLND)+UVSZIKeNtwex8ih&BCLb)~>`a7gkd?Nkk)Ff5UkcdZz zK#`DH*LKlJC@3_7Wg?UXhYrEa-y~&SqcGjt?=j9If*pZv`|R1t%|^|$?{Vb?aJ%6e zcLE^g7R})jN%qemrY|jRx2J7%+bNE)e_}YCyTzk?o#Rj`*~l-6Uxn@^xOlSAc{`Ad z0x7?FO#!H5ch!J;#R%X8{+nNUS}bUTD4|T4n;7_WbcnX1Z4(p_a`nkh!vPoD5p{u9 zBLQKlZ=g6>syso#N}-a1n0-V_nkarGI& ojLbm;bRN*RMFwhXZ8pZx@}!Oa5WxR#jDPWWBvXY64OYS2VB`I6IsgCw delta 806 zcmV+>1KIqs2CW7WLRx4!F+o`-Q&~!*1Lpt%qLC36e}ECt132+0w!kbjiakKc0Av~s z00ThNKmdAx0l+YfCYXi+0LTG^X_F=qfQEuZgeIq@KS58@k0{U@X!RNz4GjPurcDDi z2LQq_nqn9P10V(wrc9Vd0vZVvBT1&3r{zy-F*MX zsXACh;X1QgBxy;sk{Ey(YJ_BB0MO7N`aU*xubhTy6b$4pR2aXEZqjYJ6_5!Le?Ph{1$}*dpt=QcE>KmQXf9L2`%O-; zOgO#{(Q`!Au$v6zxV6i3P>q*Z-;YglC+mwJ9|x*cjaHpbBNq(awV8T`O73^(z2BvXY61gbDTaB(_Pt^fc4 diff --git a/man/generateShell.Rd b/man/generateShell.Rd index d66a4d57..2b21ed60 100644 --- a/man/generateShell.Rd +++ b/man/generateShell.Rd @@ -20,7 +20,7 @@ The function is designed to work with valid safetyGraphics charts. } \examples{ -safetyGraphics:::generateShell(charts = "eDish") +safetyGraphics:::generateShell(charts = "hepexplorer") } \keyword{internal} diff --git a/man/settingsMetadata.Rd b/man/settingsMetadata.Rd index b013c986..f24e35b4 100644 --- a/man/settingsMetadata.Rd +++ b/man/settingsMetadata.Rd @@ -6,7 +6,7 @@ \title{Settings Metadata} \format{A data frame with 29 rows and 17 columns \describe{ - \item{chart_edish}{Flag indicating if the settings apply to the eDish Chart} + \item{chart_hepexplorer}{Flag indicating if the settings apply to the Hepatic Explorer Chart} \item{chart_paneledoutlierexplorer}{Flag indicating if the settings apply to the Paneled Safety Outlier Explorer Chart} \item{chart_safetyhistogram}{Flag indicating if the settings apply to the Safety Histogram Chart} \item{chart_safetyoutlierexplorer}{Flag indicating if the settings apply to the Safety Outlier Explorer Chart} diff --git a/tests/testthat/test_getSettingsMetadata.R b/tests/testthat/test_getSettingsMetadata.R index 535e5dbe..416d6f5e 100644 --- a/tests/testthat/test_getSettingsMetadata.R +++ b/tests/testthat/test_getSettingsMetadata.R @@ -20,7 +20,7 @@ customMetadata<- data.frame( mergedMetadata = suppressWarnings(bind_rows( rawMetadata%>%mutate(chart_linechart= FALSE)%>%mutate(chart_barchart= FALSE), - customMetadata%>%mutate(chart_edish= FALSE, chart_safetyhistogram=FALSE) + customMetadata%>%mutate(chart_hepexplorer= FALSE, chart_safetyhistogram=FALSE) )) From 22da357ed0df2990211b5041e25d97c22a362990 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Tue, 28 May 2019 10:16:41 -0400 Subject: [PATCH 27/39] switch reports rmd to use hepexplorer --- .../renderReports/safetyGraphicsReport.Rmd | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/inst/safetyGraphics_app/modules/renderReports/safetyGraphicsReport.Rmd b/inst/safetyGraphics_app/modules/renderReports/safetyGraphicsReport.Rmd index 819c0265..5689debe 100644 --- a/inst/safetyGraphics_app/modules/renderReports/safetyGraphicsReport.Rmd +++ b/inst/safetyGraphics_app/modules/renderReports/safetyGraphicsReport.Rmd @@ -17,9 +17,9 @@ params: ```{r, echo = FALSE, message=FALSE, warning = FALSE} library(safetyGraphics) library(knitr) -check_edish <- "edish" %in% params$charts +check_hepexplorer <- "hepexplorer" %in% params$charts -edish_settings <- trimSettings(settings=params$settings, charts="edish") +hepexplorer_settings <- trimSettings(settings=params$settings, charts="hepexplorer") check_histogram <- "safetyhistogram" %in% params$charts @@ -44,14 +44,14 @@ paneled_settings <- trimSettings(settings=params$settings, charts="paneledoutlie ``` -```{r, eval=check_edish, echo=FALSE} -asis_output("### eDISH\\n") +```{r, eval=check_hepexplorer, echo=FALSE} +asis_output("### Hepatic Explorer\\n") ``` -```{r, fig.width=12, fig.height=15, eval=check_edish, echo = FALSE} +```{r, fig.width=12, fig.height=15, eval=check_hepexplorer, echo = FALSE} chartRenderer(data = params$data, - settings = edish_settings, chart="edish") + settings = hepexplorer_settings, chart="hepexplorer") ``` ```{r, eval=check_histogram, echo=FALSE} @@ -122,14 +122,14 @@ writeLines("my_data <- read.csv(file.path(path, 'data.csv'))") ``` -```{r, eval=check_edish, echo=FALSE} -asis_output("**Code to render eDISH chart**\\n") +```{r, eval=check_hepexplorer, echo=FALSE} +asis_output("**Code to render Hepatic Explorer chart**\\n") ``` -```{r, eval=check_edish, comment=NA, echo=FALSE} +```{r, eval=check_hepexplorer, comment=NA, echo=FALSE} graphic_code <- bquote(chartRenderer(data = my_data, - settings = .(edish_settings), chart="edish")) + settings = .(hepexplorer_settings), chart="hepexplorer")) graphic_code From dfd9f70b68bae73c83ce584e2c339af6d21b6e98 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Thu, 30 May 2019 09:41:04 -0400 Subject: [PATCH 28/39] add clinical workflow to homepage --- inst/safetyGraphics_app/server.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inst/safetyGraphics_app/server.R b/inst/safetyGraphics_app/server.R index 6f44ccfb..e0d3d146 100644 --- a/inst/safetyGraphics_app/server.R +++ b/inst/safetyGraphics_app/server.R @@ -146,7 +146,9 @@ for(chart in all_charts){
  • Hepatic Safety Explorer - Library, -Configuration
  • +Configuration, +Clinical Workflow +
  • Histogram - Library, Configuration
  • From 24121eeaba57448d9b94d730380f9e45a19104a4 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Tue, 4 Jun 2019 14:07:44 -0400 Subject: [PATCH 29/39] home page tweaks #326 --- inst/safetyGraphics_app/server.R | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inst/safetyGraphics_app/server.R b/inst/safetyGraphics_app/server.R index e0d3d146..9a2a5f71 100644 --- a/inst/safetyGraphics_app/server.R +++ b/inst/safetyGraphics_app/server.R @@ -140,14 +140,16 @@ for(chart in all_charts){ vignette. In short, the user will begin by loading a data file, adjust settings as needed and view the interactive charts. Finally, the user may export a self-contained, fully reproducible snapshot of the charts that can be easily shared with others.

    -

    Interactive Charts

    +

    Clinical Workflow

    + This shiny app has been developed in parallel with a well-documented clinical workflow + for monitoring hepatotoxicity. The workflow, written by expert physicians, provides a detailed description of how the interactive graphics can be used as part of a safety clinician’s monitoring practice. +

    Interactive Charts

    The included interactive charts are built using the htmlwidgets framework in R. The code libraries and configuration details for the underlying JavaScript charts are located below.

    • Hepatic Safety Explorer - Library, -Configuration, -Clinical Workflow +Configuration
    • Histogram - Library, @@ -157,7 +159,7 @@ for(chart in all_charts){ Configuration
    • Shift Plot - Library, -Shift Plot
    • +Configuration
    • Results Over Time - Library, Configuration
    • From ab445676d1bce27be2523f9e7fbbc68550fe4c4f Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 5 Jun 2019 11:11:31 -0400 Subject: [PATCH 30/39] clear filters warning #327 --- R/generateSettings.R | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/R/generateSettings.R b/R/generateSettings.R index 5f01d57a..a3f30fad 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -134,9 +134,11 @@ generateSettings <- function(standard="None", charts=NULL, useDefaults=TRUE, par key <- textKeysToList(text_key)[[1]] current <- getSettingValue(key,shell) if (!is.null(current)){ - if(current == ""){ - shell<-setSettingsValue(key=key, value=NULL, settings=shell) - } + if(length(current) <=1) { + if(current == ""){ + shell<-setSettingsValue(key=key, value=NULL, settings=shell) + } + } } } return(shell) From 1e5bca006fc032b1114c002321cad07cd5812fec Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Wed, 5 Jun 2019 11:29:59 -0400 Subject: [PATCH 31/39] create tabs for home page --- inst/safetyGraphics_app/ui.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index 01a554ba..d5ddee66 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -19,9 +19,17 @@ tagList( id="nav_id", tabPanel( title = "Home", icon=icon("home"), - fluidRow( - column(width=8, style='font-size:20px', uiOutput(outputId = "about")), - column(width=4, imageOutput(outputId = "hex")) + tabsetPanel( + tabPanel("About", + fluidRow( + column(width=8, style='font-size:20px', uiOutput(outputId = "about")), + column(width=4, imageOutput(outputId = "hex")) + ) + ), + tabPanel("Clinical workflow", + tags$iframe(style="height:400px; width:100%; scrolling=yes", + src = "http://github.com/SafetyGraphics/SafetyGraphics.github.io/blob/master/ISG%20Hepatic%20Safety%20Explorer%20User's%20Manual%20%26%20Workflow%20v1.0.pdf") + ) ) ), tabPanel( From 743c8ad651cab32c4b6fbdae08d6ea9af3ccf452 Mon Sep 17 00:00:00 2001 From: Preston Burns Date: Wed, 5 Jun 2019 12:26:38 -0400 Subject: [PATCH 32/39] move pdf to jsdelivr --- inst/safetyGraphics_app/ui.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index d5ddee66..61ee113a 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -27,8 +27,8 @@ tagList( ) ), tabPanel("Clinical workflow", - tags$iframe(style="height:400px; width:100%; scrolling=yes", - src = "http://github.com/SafetyGraphics/SafetyGraphics.github.io/blob/master/ISG%20Hepatic%20Safety%20Explorer%20User's%20Manual%20%26%20Workflow%20v1.0.pdf") + tags$iframe(style="height:400px; width:100%; scrolling=yes;", `data-type`="iframe", + src = "https://cdn.jsdelivr.net/gh/SafetyGraphics/SafetyGraphics.github.io/ISG%20Hepatic%20Safety%20Explorer%20User's%20Manual%20%26%20Workflow%20v1.0.pdf") ) ) ), From 61be81a9435a003a7c1c914c937328b891cb6895 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 5 Jun 2019 10:11:05 -0700 Subject: [PATCH 33/39] update hep-explorer --- inst/htmlwidgets/chartRenderer.yaml | 2 +- .../lib/hep-explorer-1.0.0/hepexplorer.js | 4633 ----------------- 2 files changed, 1 insertion(+), 4634 deletions(-) delete mode 100644 inst/htmlwidgets/lib/hep-explorer-1.0.0/hepexplorer.js diff --git a/inst/htmlwidgets/chartRenderer.yaml b/inst/htmlwidgets/chartRenderer.yaml index 1ab3a40e..61da5119 100644 --- a/inst/htmlwidgets/chartRenderer.yaml +++ b/inst/htmlwidgets/chartRenderer.yaml @@ -10,7 +10,7 @@ dependencies: stylesheet: webcharts.css - name: safety-eDish version: 0.17.0 - src: htmlwidgets/lib/hep-explorer-1.0.0 + src: htmlwidgets/lib/hep-explorer-1.0.1 script: hepexplorer.js - name: safety-histogram version: 2.3.0 diff --git a/inst/htmlwidgets/lib/hep-explorer-1.0.0/hepexplorer.js b/inst/htmlwidgets/lib/hep-explorer-1.0.0/hepexplorer.js deleted file mode 100644 index 30a7ffbc..00000000 --- a/inst/htmlwidgets/lib/hep-explorer-1.0.0/hepexplorer.js +++ /dev/null @@ -1,4633 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['webcharts'], factory) - : (global.hepexplorer = factory(global.webCharts)); -})(this, function(webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - // https://github.com/wbkd/d3-extended - d3.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); - }; - - d3.selection.prototype.moveToBack = function() { - return this.each(function() { - var firstChild = this.parentNode.firstChild; - if (firstChild) { - this.parentNode.insertBefore(this, firstChild); - } - }); - }; - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - var defineProperty = function(obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; - }; - - /*------------------------------------------------------------------------------------------------\ - Clone a variable (http://stackoverflow.com/a/728694). - \------------------------------------------------------------------------------------------------*/ - - function clone(obj) { - var copy; - - //Handle the 3 simple types, and null or undefined - if (null == obj || 'object' != (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) - return obj; - - //Handle Date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - //Handle Array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - - //Handle Object - if (obj instanceof Object) { - copy = {}; - for (var attr in obj) { - if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); - } - return copy; - } - - throw new Error("Unable to copy obj! Its type isn't supported."); - } - - function settings() { - return { - //LB domain settings - id_col: 'USUBJID', - studyday_col: 'DY', - value_col: 'STRESN', - measure_col: 'TEST', - normal_col_low: null, - normal_col_high: 'STNRHI', - visit_col: null, - visitn_col: null, - - //DM domain settings - group_cols: null, - filters: null, - details: null, - - //EX domain settings - exposure_stdy_col: 'EXSTDY', - exposure_endy_col: 'EXENDY', - exposure_trt_col: 'EXTRT', - exposure_dose_col: 'EXDOSE', - exposure_dosu_col: 'EXDOSU', - - //analysis settings - analysisFlag: { - value_col: null, - values: [] - }, - baseline: { - value_col: null, //synced with studyday_col in syncsettings() - values: [0] - }, - measure_values: { - ALT: 'Aminotransferase, alanine (ALT)', - AST: 'Aminotransferase, aspartate (AST)', - TB: 'Total Bilirubin', - ALP: 'Alkaline phosphatase (ALP)' - }, - x_options: ['ALT', 'AST', 'ALP'], - y_options: ['TB'], - point_size: 'Uniform', - point_size_options: ['ALT', 'AST', 'ALP', 'TB'], - cuts: { - ALT: { - relative_baseline: 3.8, - relative_uln: 3 - }, - AST: { - relative_baseline: 3.8, - relative_uln: 3 - }, - TB: { - relative_baseline: 4.8, - relative_uln: 2 - }, - ALP: { - relative_baseline: 3.8, - relative_uln: 1 - }, - xMeasure: null, //set in syncSettings - yMeasure: null, //set in syncSettings - display: null //set in syncSettings - }, - imputation_methods: { - ALT: 'data-driven', - AST: 'data-driven', - TB: 'data-driven', - ALP: 'data-driven' - }, - imputation_values: null, - display: 'relative_uln', //or "relative_baseline" - display_options: [ - { label: 'Upper limit of normal adjusted (eDish)', value: 'relative_uln' }, - { label: 'Baseline adjusted (mDish)', value: 'relative_baseline' } - ], - measureBounds: [0.01, 0.99], - populationProfileURL: null, - participantProfileURL: null, - r_ratio_filter: true, - r_ratio: [0, null], - visit_window: 30, - title: 'Hepatic Safety Explorer', - downloadLink: true, - filters_multiselect: true, - warningText: - "This graphic has been thoroughly tested, but is not validated. Any clinical recommendations based on this tool should be confirmed using your organization's standard operating procedures.", - //all values set in onLayout/quadrants/*.js - quadrants: [ - { - label: "Possible Hy's Law Range", - position: 'upper-right', - dataValue: 'xHigh:yHigh', - count: null, - total: null, - percent: null - }, - { - label: 'Hyperbilirubinemia', - position: 'upper-left', - dataValue: 'xNormal:yHigh', - count: null, - total: null, - percent: null - }, - { - label: "Temple's Corollary", - position: 'lower-right', - dataValue: 'xHigh:yNormal', - count: null, - total: null, - percent: null - }, - { - label: 'Normal Range', - position: 'lower-left', - dataValue: 'xNormal:yNormal', - count: null, - total: null, - percent: null - } - ], - - //Standard webcharts settings - x: { - column: null, //set in onPreprocess/updateAxisSettings - label: null, // set in onPreprocess/updateAxisSettings, - type: 'linear', - behavior: 'raw', - format: '.2f' - //domain: [0, null] - }, - y: { - column: null, // set in onPreprocess/updateAxisSettings, - label: null, // set in onPreprocess/updateAxisSettings, - type: 'linear', - behavior: 'raw', - format: '.2f' - //domain: [0, null] - }, - marks: [ - { - per: [], // set in syncSettings() - type: 'circle', - summarizeY: 'mean', - summarizeX: 'mean', - attributes: { 'fill-opacity': 0 } - } - ], - gridlines: 'xy', - color_by: null, //set in syncSettings - max_width: 600, - aspect: 1, - legend: { location: 'top' }, - margin: { right: 25, top: 25, bottom: 75 } - }; - } - - //Replicate settings in multiple places in the settings object - function syncSettings(settings) { - settings.marks[0].per[0] = settings.id_col; - - //set grouping config - if (typeof settings.group_cols == 'string') { - settings.group_cols = [{ value_col: settings.group_cols, label: settings.group_cols }]; - } - - if (!(settings.group_cols instanceof Array && settings.group_cols.length)) { - settings.group_cols = [{ value_col: 'NONE', label: 'None' }]; - } else { - settings.group_cols = settings.group_cols.map(function(group) { - return { - value_col: group.value_col || group, - label: group.label || group.value_col || group - }; - }); - - var hasNone = - settings.group_cols - .map(function(m) { - return m.value_col; - }) - .indexOf('NONE') > -1; - if (!hasNone) { - settings.group_cols.unshift({ value_col: 'NONE', label: 'None' }); - } - } - - if (settings.group_cols.length > 1) { - settings.color_by = settings.group_cols[1].value_col - ? settings.group_cols[1].value_col - : settings.group_cols[1]; - } else { - settings.color_by = 'NONE'; - } - - //make sure filters is an Array - if (!(settings.filters instanceof Array)) { - settings.filters = typeof settings.filters == 'string' ? [settings.filters] : []; - } - - //Define default details. - var defaultDetails = [{ value_col: settings.id_col, label: 'Subject Identifier' }]; - if (settings.filters) { - settings.filters.forEach(function(filter) { - var obj = { - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter - }; - - if ( - defaultDetails.find(function(f) { - return f.value_col == obj.value_col; - }) == undefined - ) { - defaultDetails.push(obj); - } - }); - } - - if (settings.group_cols) { - settings.group_cols - .filter(function(f) { - return f.value_col != 'NONE'; - }) - .forEach(function(group) { - var obj = { - value_col: group.value_col ? group.value_col : filter, - label: group.label - ? group.label - : group.value_col - ? group.value_col - : filter - }; - if ( - defaultDetails.find(function(f) { - return f.value_col == obj.value_col; - }) == undefined - ) { - defaultDetails.push(obj); - } - }); - } - - //parse details to array if needed - if (!(settings.details instanceof Array)) { - settings.details = typeof settings.details == 'string' ? [settings.details] : []; - } - - //If [settings.details] is not specified: - if (!settings.details) settings.details = defaultDetails; - else { - //If [settings.details] is specified: - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - settings.details.forEach(function(detail) { - if ( - defaultDetails - .map(function(d) { - return d.value_col; - }) - .indexOf(detail.value_col ? detail.value_col : detail) === -1 - ) - defaultDetails.push({ - value_col: detail.value_col ? detail.value_col : detail, - label: detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail - }); - }); - settings.details = defaultDetails; - } - - // If settings.analysisFlag is null - if (!settings.analysisFlag) settings.analysisFlag = { value_col: null, values: [] }; - if (!settings.analysisFlag.value_col) settings.analysisFlag.value_col = null; - if (!(settings.analysisFlag.values instanceof Array)) { - settings.analysisFlag.values = - typeof settings.analysisFlag.values == 'string' - ? [settings.analysisFlag.values] - : []; - } - //if it is null, set settings.baseline.value_col to settings.studyday_col. - if (!settings.baseline) settings.baseline = { value_col: null, values: [] }; - if (!settings.baseline.value_col) settings.baseline.value_col = settings.studyday_col; - if (!(settings.baseline.values instanceof Array)) { - settings.baseline.values = - typeof settings.baseline.values == 'string' ? [settings.baseline.values] : []; - } - - //parse x_ and y_options to array if needed - if (!(settings.x_options instanceof Array)) { - settings.x_options = typeof settings.x_options == 'string' ? [settings.x_options] : []; - } - - if (!(settings.y_options instanceof Array)) { - settings.y_options = typeof settings.y_options == 'string' ? [settings.y_options] : []; - } - - // track initial Cutpoint (lets us detect when cutpoint should change) - settings.cuts.x = settings.x.column; - settings.cuts.y = settings.y.column; - settings.cuts.display = settings.display; - - //Attach measure columns to axis settings. - settings.x.column = settings.x_options[0]; - settings.y.column = settings.y_options[0]; - - return settings; - } - - function controlInputs() { - return [ - { - type: 'number', - label: 'R Ratio Range', - description: 'Filter points based on R ratio [(ALT/ULN) / (ALP/ULN)]', - option: 'r_ratio[0]' - }, - { - type: 'number', - label: null, //combined with r_ratio[0] control in formatRRatioControl() - description: null, - option: 'r_ratio[1]' - }, - { - type: 'dropdown', - label: 'Group', - description: 'Grouping variable', - options: ['color_by'], - start: null, // set in syncControlInputs() - values: ['NONE'], // set in syncControlInputs() - require: true - }, - { - type: 'dropdown', - label: 'Display Type', - description: 'Relative or absolute axes', - options: ['displayLabel'], - start: null, // set in syncControlInputs() - values: null, // set in syncControlInputs() - require: true - }, - { - type: 'dropdown', - label: 'X-axis Measure', - description: null, // set in syncControlInputs() - option: 'x.column', - start: null, // set in syncControlInputs() - values: null, //set in syncControlInptus() - require: true - }, - { - type: 'number', - label: null, // set in syncControlInputs - description: 'X-axis Reference Line', - option: null // set in syncControlInputs - }, - { - type: 'dropdown', - label: 'Y-axis Measure', - description: null, // set in syncControlInputs() - option: 'y.column', - start: null, // set in syncControlInputs() - values: null, //set in syncControlInptus() - require: true - }, - { - type: 'number', - label: null, // set in syncControlInputs - description: 'Y-axis Reference Line', - option: null // set in syncControlInputs - }, - { - type: 'dropdown', - label: 'Point Size', - description: 'Parameter to set point radius', - options: ['point_size'], - start: null, // set in syncControlInputs() - values: ['Uniform'], - require: true - }, - { - type: 'dropdown', - label: 'Axis Type', - description: 'Linear or Log Axes', - options: ['x.type', 'y.type'], - start: null, // set in syncControlInputs() - values: ['linear', 'log'], - require: true - }, - { - type: 'number', - label: 'Highlight Points Based on Timing', - description: 'Fill points with max values less than X days apart', - option: 'visit_window' - } - ]; - } - - //Map values from settings to control inputs - function syncControlInputs(controlInputs, settings) { - //////////////////////// - // Group control - /////////////////////// - - var groupControl = controlInputs.find(function(controlInput) { - return controlInput.label === 'Group'; - }); - - //sync start value - groupControl.start = settings.color_by; //sync start value - - //sync values - settings.group_cols - .filter(function(group) { - return group.value_col !== 'NONE'; - }) - .forEach(function(group) { - groupControl.values.push(group.value_col); - }); - - //drop the group control if NONE is the only option - if (settings.group_cols.length == 1) - controlInputs = controlInputs.filter(function(controlInput) { - return controlInput.label != 'Group'; - }); - - ////////////////////////// - // x-axis measure control - ////////////////////////// - - // drop the control if there's only one option - if (settings.x_options.length === 1) - controlInputs = controlInputs.filter(function(controlInput) { - return controlInput.option !== 'x.column'; - }); - else { - //otherwise sync the properties - var xAxisMeasureControl = controlInputs.find(function(controlInput) { - return controlInput.option === 'x.column'; - }); - - xAxisMeasureControl.description = settings.x_options.join(', '); - xAxisMeasureControl.start = settings.x_options[0]; - xAxisMeasureControl.values = settings.x_options; - } - - ////////////////////////////////// - // x-axis reference line control - ////////////////////////////////// - - var xRefControl = controlInputs.find(function(controlInput) { - return controlInput.description === 'X-axis Reference Line'; - }); - xRefControl.label = settings.x_options[0] + ' Cutpoint'; - xRefControl.option = 'settings.cuts.' + [settings.x.column] + '.' + [settings.display]; - - //////////////////////////// - // y-axis measure control - //////////////////////////// - - // drop the control if there's only one option - if (settings.y_options.length === 1) - controlInputs = controlInputs.filter(function(controlInput) { - return controlInput.option !== 'y.column'; - }); - else { - //otherwise sync the properties - var yAxisMeasureControl = controlInputs.find(function(controlInput) { - return controlInput.option === 'y.column'; - }); - yAxisMeasureControl.description = settings.y_options.join(', '); - yAxisMeasureControl.start = settings.y_options[0]; - yAxisMeasureControl.values = settings.y_options; - } - - ////////////////////////////////// - // y-axis reference line control - ////////////////////////////////// - - var yRefControl = controlInputs.find(function(controlInput) { - return controlInput.description === 'Y-axis Reference Line'; - }); - yRefControl.label = settings.y_options[0] + ' Cutpoint'; - - yRefControl.option = 'settings.cuts.' + [settings.y.column] + '.' + [settings.display]; - - ////////////////////////////////// - // R ratio filter control - ////////////////////////////////// - - //drop the R Ratio control if r_ratio_filter is false - if (!settings.r_ratio_filter) { - controlInputs = controlInputs.filter(function(controlInput) { - return ['r_ratio[0]', 'r_ratio[1]'].indexOf(controlInput.option) == -1; - }); - } - - ////////////////////////////////// - // Point size control - ////////////////////////////////// - - var pointSizeControl = controlInputs.find(function(ci) { - return ci.label === 'Point Size'; - }); - - pointSizeControl.start = settings.point_size || 'Uniform'; - - settings.point_size_options.forEach(function(d) { - pointSizeControl.values.push(d); - }); - - //drop the pointSize control if NONE is the only option - if (settings.point_size_options.length == 0) - controlInputs = controlInputs.filter(function(controlInput) { - return controlInput.label != 'Point Size'; - }); - - ////////////////////////////////// - // Display control - ////////////////////////////////// - - controlInputs.find(function(controlInput) { - return controlInput.label === 'Display Type'; - }).values = settings.display_options.map(function(m) { - return m.label; - }); - - ////////////////////////////////// - // Add filters to inputs - ////////////////////////////////// - if (settings.filters && settings.filters.length > 0) { - var otherFilters = settings.filters.map(function(filter) { - filter = { - type: 'subsetter', - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter, - multiple: settings.filters_multiselect - }; - return filter; - }); - return d3.merge([otherFilters, controlInputs]); - } else return controlInputs; - } - - var configuration = { - settings: settings, - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs - }; - - function checkMeasureDetails() { - var config = this.config; - var measures = d3 - .set( - this.raw_data.map(function(d) { - return d[config.measure_col]; - }) - ) - .values() - .sort(); - var specifiedMeasures = Object.keys(config.measure_values).map(function(e) { - return config.measure_values[e]; - }); - var missingMeasures = []; - Object.keys(config.measure_values).forEach(function(d) { - if (measures.indexOf(config.measure_values[d]) == -1) { - missingMeasures.push(config.measure_values[d]); - delete config.measure_values[d]; - } - }); - var nMeasuresRemoved = missingMeasures.length; - if (nMeasuresRemoved > 0) - console.warn( - 'The data are missing ' + - (nMeasuresRemoved === 1 ? 'this measure' : 'these measures') + - ': ' + - missingMeasures.join(', ') + - '.' - ); - - //check that x_options, y_options and size_options all have value keys/values in measure_values - var valid_options = Object.keys(config.measure_values); - var all_options = ['x_options', 'y_options', 'point_size_options']; - all_options.forEach(function(options) { - config[options].forEach(function(option) { - if (valid_options.indexOf(option) == -1) { - delete config[options][option]; - console.warn( - option + - " wasn't found in the measure_values index and has been removed from config." + - options + - '. This may cause problems with the chart.' - ); - } - }); - }); - } - - function iterateOverData() { - var _this = this; - - this.raw_data.forEach(function(d) { - d[_this.config.x.column] = null; // placeholder variable for x-axis - d[_this.config.y.column] = null; // placeholder variable for y-axis - d.NONE = 'All Participants'; // placeholder variable for non-grouped comparisons - - //Remove space characters from result variable. - if (typeof d[_this.config.value_col] == 'string') - d[_this.config.value_col] = d[_this.config.value_col].replace(/\s/g, ''); // remove space characters - }); - } - - function addRRatioFilter() { - if (this.config.r_ratio_filter) { - this.filters.push({ - col: 'rRatioFlag', - val: 'Y', - choices: ['Y', 'N'], - loose: undefined - }); - } - } - - function imputeColumn(data, measure_column, value_column, measure, llod, imputed_value, drop) { - //returns a data set with imputed values (or drops records) for records at or below a lower threshold for a given measure - //data = the data set for imputation - //measure_column = the column with the text measure names - //value_column = the column with the numeric values to be changed via imputation - //measure = the measure to be imputed - //llod = the lower limit of detection - values at or below the llod are imputed - //imputed_value = value for imputed records - //drop = boolean flag indicating whether values at or below the llod should be dropped (default = false) - - if (drop == undefined) drop = false; - if (drop) { - return data.filter(function(f) { - dropFlag = d[measure_column] == measure && +d[value_column] <= 0; - return !dropFlag; - }); - } else { - data.forEach(function(d) { - if ( - d[measure_column] == measure && - +d[value_column] < +llod && - d[value_column] >= 0 - ) { - d.impute_flag = true; - d[value_column + '_original'] = d[value_column]; - d[value_column] = imputed_value; - } - }); - - var impute_count = d3.sum( - data.filter(function(d) { - return d[measure_column] === measure; - }), - function(f) { - return f.impute_flag; - } - ); - - if (impute_count > 0) - console.warn( - '' + - impute_count + - ' value(s) less than ' + - llod + - ' were imputed to ' + - imputed_value + - ' for ' + - measure - ); - return data; - } - } - - function imputeData() { - var chart = this; - var config = this.config; - - Object.keys(config.measure_values).forEach(function(measureKey) { - var values = chart.imputed_data - .filter(function(f) { - return f[config.measure_col] == config.measure_values[measureKey]; - }) - .map(function(m) { - return +m[config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }), - minValue = d3.min( - values.filter(function(f) { - return f > 0; - }) - ), - //minimum value > 0 - llod = null, - imputed_value = null, - drop = null; - - if (config.imputation_methods[measureKey] == 'data-driven') { - llod = minValue; - imputed_value = minValue / 2; - drop = false; - } else if (config.imputation_methods[measureKey] == 'user-defined') { - llod = config.imputation_values[measureKey]; - imputed_value = config.imputation_values[measureKey] / 2; - drop = false; - } else if (config.imputation_methods[measureKey] == 'drop') { - llod = null; - imputed_value = null; - drop = true; - } - chart.imputed_data = imputeColumn( - chart.imputed_data, - config.measure_col, - config.value_col, - config.measure_values[measureKey], - llod, - imputed_value, - drop - ); - - var total_imputed = d3.sum(chart.raw_data, function(f) { - return f.impute_flag ? 1 : 0; - }); - }); - } - - function dropRows() { - var chart = this; - var config = this.config; - this.dropped_rows = []; - - ///////////////////////// - // Remove invalid rows - ///////////////////////// - var numerics = ['value_col', 'studyday_col', 'normal_col_high']; - chart.imputed_data = chart.initial_data.filter(function(f) { - return true; - }); - numerics.forEach(function(setting) { - chart.imputed_data = chart.imputed_data.filter(function(d) { - //Remove non-numeric value_col - var numericCol = /^-?(\d*\.?\d+|\d+\.?\d*)(E-?\d+)?$/.test(d[config[setting]]); - if (!numericCol) { - d.dropReason = setting + ' Column ("' + config[setting] + '") is not numeric.'; - chart.dropped_rows.push(d); - } - return numericCol; - }); - }); - } - - function deriveVariables() { - var config = this.config; - - //filter the lab data to only the required measures - var included_measures = Object.keys(config.measure_values).map(function(e) { - return config.measure_values[e]; - }); - - var sub = this.imputed_data.filter(function(f) { - return included_measures.indexOf(f[config.measure_col]) > -1; - }); - - var missingBaseline = 0; - - //coerce numeric values to number - this.imputed_data = this.imputed_data.map(function(d) { - var numerics = ['value_col', 'studyday_col', 'normal_col_low', 'normal_col_high']; - numerics.forEach(function(col) { - d[config[col]] = parseFloat(d[config[col]]); - }); - return d; - }); - - //create an object mapping baseline values for id/measure pairs - var baseline_records = sub.filter(function(f) { - var current = - typeof f[config.baseline.value_col] == 'string' - ? f[config.baseline.value_col].trim() - : parseFloat(f[config.baseline.value_col]); - return config.baseline.values.indexOf(current) > -1; - }); - - var baseline_values = d3 - .nest() - .key(function(d) { - return d[config.id_col]; - }) - .key(function(d) { - return d[config.measure_col]; - }) - .rollup(function(d) { - return d[0][config.value_col]; - }) - .map(baseline_records); - - this.imputed_data = this.imputed_data.map(function(d) { - //standardize key variables - d.key_measure = false; - if (included_measures.indexOf(d[config.measure_col]) > -1) { - d.key_measure = true; - - //map the raw value to a variable called 'absolute' - d.absolute = d[config.value_col]; - - //get the value relative to the ULN (% of the upper limit of normal) for the measure - d.uln = d[config.normal_col_high]; - d.relative_uln = d[config.value_col] / d[config.normal_col_high]; - - //get value relative to baseline - if (baseline_values[d[config.id_col]]) { - if (baseline_values[d[config.id_col]][d[config.measure_col]]) { - d.baseline_absolute = - baseline_values[d[config.id_col]][d[config.measure_col]]; - d.relative_baseline = d.absolute / d.baseline_absolute; - } else { - missingBaseline = missingBaseline + 1; - d.baseline_absolute = null; - d.relative_baseline = null; - } - } else { - missingBaseline = missingBaseline + 1; - d.baseline_absolute = null; - d.relative_baseline = null; - } - } - return d; - }); - - if (missingBaseline > 0) - console.warn( - 'No baseline value found for ' + missingBaseline + ' of ' + sub.length + ' records.' - ); - } - - function makeAnalysisFlag() { - var config = this.config; - this.imputed_data = this.imputed_data.map(function(d) { - var hasAnalysisSetting = - config.analysisFlag.value_col != null && config.analysisFlag.values.length > 0; - d.analysisFlag = hasAnalysisSetting - ? config.analysisFlag.values.indexOf(d[config.analysisFlag.value_col]) > -1 - : true; - return d; - }); - } - - function cleanData() { - var config = this.config; - - //drop rows with invalid data - this.imputedData = dropRows.call(this); - - this.imputed_data.forEach(function(d) { - d.impute_flag = false; - }); - - imputeData.call(this); - deriveVariables.call(this); - makeAnalysisFlag.call(this); - } - - function onInit() { - checkMeasureDetails.call(this); - iterateOverData.call(this); - addRRatioFilter.call(this); - cleanData.call(this); //clean visit-level data - imputation and variable derivations - } - - function formatRRatioControl() { - var chart = this; - var config = this.config; - if (this.config.r_ratio_filter) { - var min_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { - return d.option === 'r_ratio[0]'; - }); - var min_r_ratio_input = min_r_ratio.select('input'); - - var max_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { - return d.option === 'r_ratio[1]'; - }); - var max_r_ratio_input = max_r_ratio.select('input'); - - min_r_ratio_input.attr('id', 'r_ratio_min'); - max_r_ratio_input.attr('id', 'r_ratio_max'); - - //move the max r ratio control next to the min control - min_r_ratio.append('span').text(' - '); - min_r_ratio.append(function() { - return max_r_ratio_input.node(); - }); - - max_r_ratio.remove(); - - //add a reset button - min_r_ratio - .append('button') - .style('padding', '0.2em 0.5em 0.2em 0.4em') - .style('margin-left', '0.5em') - .style('border-radius', '0.4em') - .text('Reset') - .on('click', function() { - config.r_ratio[0] = 0; - min_r_ratio.select('input#r_ratio_min').property('value', config.r_ratio[0]); - config.r_ratio[1] = config.max_r_ratio; - min_r_ratio.select('input#r_ratio_max').property('value', config.r_ratio[1]); - chart.draw(); - }); - } - } - - function updateSummaryTable() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - var rows = quadrants.table.rows; - var cells = quadrants.table.cells; - - function updateCells(d) { - var cellData = cells.map(function(cell) { - cell.value = d[cell.value_col]; - return cell; - }); - var row_cells = d3 - .select(this) - .selectAll('td') - .data(cellData, function(d) { - return d.value_col; - }); - - row_cells - .enter() - .append('td') - .style('text-align', function(d, i) { - return d.label != 'Quadrant' ? 'center' : null; - }) - .style('font-size', '0.9em') - .style('padding', '0 0.5em 0 0.5em'); - - row_cells.html(function(d) { - return d.value; - }); - } - - //update the content of each row - rows.data(quadrants, function(d) { - return d.label; - }); - rows.each(updateCells); - } - - function initSummaryTable() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - quadrants.table = {}; - quadrants.table.wrap = this.wrap - .append('div') - .attr('class', 'quadrantTable') - .style('padding-top', '1em'); - quadrants.table.tab = quadrants.table.wrap - .append('table') - .style('border-collapse', 'collapse'); - - //table header - quadrants.table.cells = [ - { - value_col: 'label', - label: 'Quadrant' - }, - { - value_col: 'count', - label: '#' - }, - { - value_col: 'percent', - label: '%' - } - ]; - - if (config.populationProfileURL) { - quadrants.forEach(function(d) { - d.link = "🔗"; - }); - quadrants.table.cells.push({ - value_col: 'link', - label: 'Population Profile' - }); - } - quadrants.table.thead = quadrants.table.tab - .append('thead') - .style('border-top', '2px solid #999') - .style('border-bottom', '2px solid #999') - .append('tr') - .style('padding', '0.1em'); - - quadrants.table.thead - .selectAll('th') - .data(quadrants.table.cells) - .enter() - .append('th') - .html(function(d) { - return d.label; - }); - - //table contents - quadrants.table.tbody = quadrants.table.tab - .append('tbody') - .style('border-bottom', '2px solid #999'); - quadrants.table.rows = quadrants.table.tbody - .selectAll('tr') - .data(quadrants, function(d) { - return d.label; - }) - .enter() - .append('tr') - .style('padding', '0.1em'); - - //initial table update - updateSummaryTable.call(this); - } - - function init() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - var x_input = chart.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.description == 'X-axis Reference Line'; - }) - .select('input'); - - var y_input = chart.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.description == 'Y-axis Reference Line'; - }) - .select('input'); - - /////////////////////////////////////////////////////////// - // set initial values - ////////////////////////////////////////////////////////// - x_input.node().value = config.cuts[config.x.column][config.display]; - y_input.node().value = config.cuts[config.y.column][config.display]; - - /////////////////////////////////////////////////////////// - // set control step to 0.1 - ////////////////////////////////////////////////////////// - x_input.attr('step', 0.1); - y_input.attr('step', 0.1); - - /////////////////////////////////////////////////////////// - // initialize the summary table - ////////////////////////////////////////////////////////// - initSummaryTable.call(chart); - } - - function layoutQuadrantLabels() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - ////////////////////////////////////////////////////////// - //layout the quadrant labels - ///////////////////////////////////////////////////////// - - chart.quadrant_labels = {}; - chart.quadrant_labels.g = this.svg.append('g').attr('class', 'quadrant-labels'); - - chart.quadrant_labels.text = chart.quadrant_labels.g - .selectAll('text.quadrant-label') - .data(quadrants) - .enter() - .append('text') - .attr('class', function(d) { - return 'quadrant-label ' + d.position; - }) - .attr('dy', function(d) { - return d.position.search('lower') > -1 ? '-.2em' : '.5em'; - }) - .attr('dx', function(d) { - return d.position.search('right') > -1 ? '-.5em' : '.5em'; - }) - .attr('text-anchor', function(d) { - return d.position.search('right') > 0 ? 'end' : null; - }) - .attr('fill', '#bbb') - .text(function(d) { - return d.label; - }); - } - - function layoutCutLines() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - ////////////////////////////////////////////////////////// - //layout the cut lines - ///////////////////////////////////////////////////////// - chart.cut_lines = {}; - chart.cut_lines.wrap = this.svg.append('g').attr('class', 'cut-lines'); - var wrap = chart.cut_lines.wrap; - - //slight hack to make life easier on drag - var cutLineData = [{ dimension: 'x' }, { dimension: 'y' }]; - - cutLineData.forEach(function(d) { - d.chart = chart; - }); - - chart.cut_lines.g = wrap - .selectAll('g.cut') - .data(cutLineData) - .enter() - .append('g') - .attr('class', function(d) { - return 'cut ' + d.dimension; - }); - - chart.cut_lines.lines = chart.cut_lines.g - .append('line') - .attr('class', 'cut-line') - .attr('stroke-dasharray', '5,5') - .attr('stroke', '#bbb'); - - chart.cut_lines.backing = chart.cut_lines.g - .append('line') - .attr('class', 'cut-line-backing') - .attr('stroke', 'transparent') - .attr('stroke-width', '10') - .attr('cursor', 'move'); - } - - function initQuadrants() { - init.call(this); - layoutCutLines.call(this); - layoutQuadrantLabels.call(this); - } - - function initRugs() { - //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover - this.x_rug = this.svg.append('g').attr('class', 'rug x'); - this.y_rug = this.svg.append('g').attr('class', 'rug y'); - } - - function initVisitPath() { - //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover - this.visitPath = this.svg.append('g').attr('class', 'visit-path'); - } - - function initParticipantDetails() { - //layout participant details section - this.participantDetails = {}; - this.participantDetails.wrap = this.wrap.append('div').attr('class', 'participantDetails'); - - this.participantDetails.header = this.participantDetails.wrap - .append('div') - .attr('class', 'participantHeader'); - var splot = this.participantDetails.wrap.append('div').attr('class', 'spaghettiPlot'); - splot - .append('h3') - .attr('class', 'id') - .html('Standardized Lab Values by Visit') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - splot.append('div').attr('class', 'chart'); - - var mtable = this.participantDetails.wrap.append('div').attr('class', 'measureTable'); - mtable - .append('h3') - .attr('class', 'id') - .html('Raw Lab Values Summary Table') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - //initialize the measureTable - var settings = { - cols: ['key', 'n', 'min', 'median', 'max', 'spark'], - headers: ['Measure', 'N', 'Min', 'Median', 'Max', ''], - searchable: false, - sortable: false, - pagination: false, - exportable: false, - applyCSS: true - }; - this.measureTable = webcharts.createTable( - this.element + ' .participantDetails .measureTable', - settings - ); - this.measureTable.init([]); - - //hide the section until needed - this.participantDetails.wrap.selectAll('*').style('display', 'none'); - } - - function initResetButton() { - var chart = this; - - this.controls.reset = {}; - var reset = this.controls.reset; - reset.wrap = this.controls.wrap.append('div').attr('class', 'control-group'); - reset.label = reset.wrap - .append('span') - .attr('class', 'wc-control-label') - .text('Reset Chart'); - reset.button = reset.wrap - .append('button') - .text('Reset') - .on('click', function() { - var initial_container = chart.element; - var initial_settings = chart.initial_settings; - var initial_data = chart.initial_data; - chart.emptyChartWarning.remove(); - - chart.destroy(); - chart = null; - - var newChart = safetyedish(initial_container, initial_settings); - newChart.init(initial_data); - }); - } - - function initDisplayControl() { - var chart = this; - var config = this.config; - var displayControlWrap = this.controls.wrap.selectAll('div').filter(function(controlInput) { - return controlInput.label === 'Display Type'; - }); - - var displayControl = displayControlWrap.select('select'); - - //set the start value - var start_value = config.display_options.find(function(f) { - return f.value == config.display; - }).label; - displayControl.selectAll('option').attr('selected', function(d) { - return d == start_value ? 'selected' : null; - }); - - //annotation of baseline visit (only visible when mDish is selected) - displayControlWrap - .append('span') - .attr('class', 'displayControlAnnotation span-description') - .style('color', 'blue') - .text( - 'Note: Baseline defined as ' + - chart.config.baseline.value_col + - ' = ' + - chart.config.baseline.values.join(',') - ) - .style('display', config.display == 'relative_baseline' ? null : 'none'); - - displayControl.on('change', function(d) { - var currentLabel = this.value; - var currentValue = config.display_options.find(function(f) { - return f.label == currentLabel; - }).value; - config.display = currentValue; - - if (currentValue == 'relative_baseline') { - displayControlWrap.select('span.displayControlAnnotation').style('display', null); - } else { - displayControlWrap.select('span.displayControlAnnotation').style('display', 'none'); - } - - config.cuts.display_change = true; - - chart.draw(); - }); - } - - function layoutPanels() { - this.wrap.style('display', 'inline-block').style('width', '75%'); - - this.controls.wrap - .style('display', 'inline-block') - .style('width', '25%') - .style('vertical-align', 'top'); - - this.controls.wrap.selectAll('div.control-group').style('display', 'block'); - this.controls.wrap - .selectAll('div.control-group') - .select('select') - .style('width', '200px'); - } - - function initTitle() { - this.titleDiv = this.controls.wrap - .insert('div', '*') - .attr('class', 'title') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - - this.titleDiv - .append('span') - .text(this.config.title) - .style('font-size', '1.5em') - .style('font-weight', 'strong') - .style('display', 'block'); - } - - function add(messageText, type, label, messages, callback) { - var messageObj = { - id: messages.list.length + 1, - type: type, - message: messageText, - label: label, - hidden: false, - callback: callback - }; - messages.list.push(messageObj); - messages.update(messages); - } - - function remove(id, label, messages) { - // hide the the message(s) by id or label - if (id) { - var matches = messages.list.filter(function(f) { - return +f.id == +id; - }); - } else if (label.length) { - var matches = messages.list.filter(function(f) { - return label == 'all' ? true : f.label == label; - }); - } - matches.forEach(function(d) { - d.hidden = true; - }); - messages.update(messages); - } - - function update(messages) { - function jsUcfirst(string) { - return string.charAt(0).toUpperCase() + string.slice(1); - } - - var visibleMessages = messages.list.filter(function(f) { - return f.hidden == false; - }); - - //update title - messages.header.title.text('Messages (' + visibleMessages.length + ')'); - - // - var messageDivs = messages.wrap.selectAll('div.message').data(visibleMessages, function(d) { - return d.id; - }); - - var newMessages = messageDivs - .enter() - .append('div') - .attr('class', function(d) { - return d.type + ' message ' + d.label; - }) - .html(function(d) { - var messageText = '' + jsUcfirst(d.type) + ': ' + d.message; - return messageText.split('.')[0] + '.'; - }) - .style('border-radius', '.5em') - .style('margin-right', '1em') - .style('margin-bottom', '0.5em') - .style('padding', '0.2em') - .style('font-size', '0.9em'); - - newMessages - .append('div.expand') - .html('•••') - .style('background', 'white') - .style('display', 'inline-block') - .style('border', '1px solid #999') - .style('padding', '0 0.2em') - .style('margin-left', '0.3em') - .style('font-size', '0.4em') - .style('border-radius', '0.6em') - .style('cursor', 'pointer') - .on('click', function(d) { - d3.select(this.parentNode) - .html(function(d) { - return '' + jsUcfirst(d.type) + ': ' + d.message; - }) - .each(function(d) { - if (d.callback) { - d.callback.call(this.parentNode); - } - }); - }); - - messageDivs.each(function(d) { - var type = d.type; - var thisMessage = d3.select(this); - if (type == 'caution') { - thisMessage - .style('border', '1px solid #faebcc') - .style('color', '#8a6d3b') - .style('background-color', '#fcf8e3'); - } else if (type == 'warning') { - thisMessage - .style('border', '1px solid #ebccd1') - .style('color', '#a94442') - .style('background-color', '#f2dede'); - } else { - thisMessage - .style('border', '1px solid #999') - .style('color', '#999') - .style('background-color', null); - } - - if (d.callback) { - d.callback.call(this); - } - }); - - messageDivs.exit().remove(); - } - - function init$1() { - var chart = this; - this.messages = { - add: add, - remove: remove, - update: update - }; - // this.messages.add = addMessage; - // this.messages.remove = removeMessage; - this.messages.list = []; - this.messages.wrap = this.controls.wrap.insert('div', '*').style('margin', '0 1em 1em 0'); - this.messages.header = this.messages.wrap - .append('div') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('font-weight', 'strong') - .style('margin', '0 1em 1em 0'); - - this.messages.header.title = this.messages.header - .append('div') - .attr('class', 'title') - .style('display', 'inline-block') - .text('Messages (0)'); - - this.messages.header.clear = this.messages.header - .append('div') - .text('Clear') - .style('font-size', '0.8em') - .style('vertical-align', 'center') - .style('display', 'inline-block') - .style('float', 'right') - .style('color', 'blue') - .style('cursor', 'pointer') - .style('text-decoration', 'underline') - .on('click', function() { - chart.messages.remove(null, 'all', chart.messages); - }); - } - - function initCustomWarning() { - if (this.config.warningText) { - this.messages.add( - this.config.warningText, - 'caution', - 'validationCaution', - this.messages - ); - } - } - - function downloadCSV(data, cols, file) { - var CSVarray = []; - - //add headers to CSV array - var cols = cols ? cols : Object.keys(data[0]); - var headers = cols.map(function(header) { - return '"' + header.replace(/"/g, '""') + '"'; - }); - CSVarray.push(headers); - //add rows to CSV array - data.forEach(function(d, i) { - var row = cols.map(function(col) { - var value = d[col]; - - if (typeof value === 'string') value = value.replace(/"/g, '""'); - - return '"' + value + '"'; - }); - - CSVarray.push(row); - }); - - //transform blob array into a blob of characters - var blob = new Blob([CSVarray.join('\n')], { - type: 'text/csv;charset=utf-8;' - }); - var fileCore = file ? file : 'eDish'; - var fileName = fileCore + '_' + d3.time.format('%Y-%m-%dT%H-%M-%S')(new Date()) + '.csv'; - var link = d3.select(this); - - if (navigator.msSaveBlob) - //IE - navigator.msSaveBlob(blob, fileName); - else if (link.node().download !== undefined) { - //21st century browsers - var url = URL.createObjectURL(blob); - link.node().setAttribute('href', url); - link.node().setAttribute('download', fileName); - } - } - - function initDroppedRowsWarning() { - var chart = this; - if (this.dropped_rows.length > 0) { - var warningText = - this.dropped_rows.length + - ' rows were removed. This is probably because of non-numeric or missing data provided for key variables. Click here to download a csv with a brief explanation of why each row was removed.'; - - this.messages.add(warningText, 'caution', 'droppedRows', this.messages, function() { - //custom callback to activate the droppedRows download - d3.select(this) - .select('a.rowDownload') - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .datum(chart.dropped_rows) - .on('click', function(d) { - var systemVars = d3.merge([ - ['dropReason', 'NONE'], - Object.keys(chart.config.measure_values) - ]); - var cols = d3.merge([ - ['dropReason'], - Object.keys(d[0]).filter(function(f) { - return systemVars.indexOf(f) == -1; - }) - ]); - downloadCSV.call(this, d, cols, 'eDishDroppedRows'); - }); - }); - } - } - - function initControlLabels() { - var config = this.config; - - //Add settings label - var first_setting = this.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.type != 'subsetter'; - }) - .filter(function(f) { - return f.option != 'r_ratio[0]'; - }) - .filter(function(f, i) { - return i == 0; - }) - .attr('class', 'first-setting'); - - this.controls.setting_header = this.controls.wrap - .insert('div', '.first-setting') - .attr('class', 'subtitle') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - - this.controls.setting_header - .append('span') - .text('Settings') - .style('font-weight', 'strong') - .style('display', 'block'); - - //Add filter label if at least 1 filter exists - if (config.r_ratio_filter || config.filters.length > 0) { - //insert a header before the first filter - var control_wraps = this.controls.wrap - .selectAll('div') - .filter(function(controlInput) { - return ( - controlInput.label === 'R Ratio Range' || controlInput.type === 'subsetter' - ); - }) - .classed('subsetter', true); - - this.controls.filter_header = this.controls.wrap - .insert('div', 'div.subsetter') - .attr('class', 'subtitle') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - this.controls.filter_header - .append('span') - .text('Filters') - .style('font-weight', 'strong') - .style('display', 'block'); - var population = d3 - .set( - this.initial_data.map(function(m) { - return m[config.id_col]; - }) - ) - .values().length; - this.controls.filter_header - .append('span') - .attr('class', 'popCount') - .html( - '' + - population + - ' of ' + - population + - ' participants shown.' - ) - .style('font-size', '0.8em'); - - this.controls.filter_numerator = this.controls.filter_header - .select('span.popCount') - .select('span.numerator'); - this.controls.filter_denominator = this.controls.filter_header - .select('span.popCount') - .select('span.denominator'); - } - } - - function addFootnote() { - this.footnote = this.wrap - .append('div') - .attr('class', 'footnote') - .text('Use controls to update chart or click a point to see participant details.') - .style('font-size', '0.7em') - .style('padding-top', '0.1em'); - this.footnote.timing = this.footnote.append('p'); - } - - function addDownloadButton() { - var chart = this; - var config = this.config; - if (config.downloadLink) { - this.titleDiv - .select('span') - .append('a') - .attr('class', 'downloadRaw') - .html('↓ Raw Data') - .attr('title', 'Download Raw Data') - .style('font-size', '.5em') - .style('margin-left', '1em') - .style('border', '1px solid black') - .style('border-radius', '2px') - .style('padding', '2px 4px') - .style('text-align', 'center') - .style('display', 'inline-block') - .style('cursor', 'pointer') - .style('font-weight', 'bold') - .datum(chart.initial_data) - .on('click', function(d) { - var systemVars = [ - 'dropReason', - 'NONE', - 'ALT', - 'TB', - 'impute_flag', - 'key_measure', - 'analysisFlag' - ]; - var cols = Object.keys(d[0]).filter(function(f) { - return systemVars.indexOf(f) == -1; - }); - downloadCSV.call(this, d, cols, 'eDishRawData'); - }); - } - } - - function initEmptyChartWarning() { - console.log(this); - this.emptyChartWarning = d3 - .select(this.element) - .append('span') - .text('No data selected. Try updating your settings or resetting the chart. ') - .style('display', 'none') - .style('color', '#a94442') - .style('background-color', '#f2dede') - .style('border', '1px solid #ebccd1') - .style('padding', '0.5em') - .style('margin', '0 2% 12px 2%') - .style('border-radius', '0.2em'); - } - - function onLayout() { - layoutPanels.call(this); - - //init messages section - init$1.call(this); - initCustomWarning.call(this); - initDroppedRowsWarning.call(this); - - initTitle.call(this); - addDownloadButton.call(this); - - addFootnote.call(this); - formatRRatioControl.call(this); - initQuadrants.call(this); - initRugs.call(this); - initVisitPath.call(this); - initParticipantDetails.call(this); - initResetButton.call(this); - initDisplayControl.call(this); - initControlLabels.call(this); - initEmptyChartWarning.call(this); - } - - function updateAxisSettings() { - var config = this.config; - var unit = - config.display == 'relative_uln' - ? ' [xULN]' - : config.display == 'relative_baseline' - ? ' [xBaseline]' - : config.display == 'absolute' - ? ' [raw values]' - : null; - - //Update axis labels. - config.x.label = config.measure_values[config.x.column] + unit; - config.y.label = config.measure_values[config.y.column] + unit; - } - - function updateControlCutpointLabels() { - if ( - this.controls.config.inputs.find(function(input) { - return input.description === 'X-axis Reference Line'; - }) - ) - this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.description === 'X-axis Reference Line'; - }) - .select('.wc-control-label') - .text(this.config.x.column + ' Reference Line'); - if ( - this.controls.config.inputs.find(function(input) { - return input.description === 'Y-axis Reference Line'; - }) - ) - this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.description === 'Y-axis Reference Line'; - }) - .select('.wc-control-label') - .text(this.config.y.column + ' Reference Line'); - } - - function setMaxRRatio() { - var chart = this; - var config = this.config; - var r_ratio_wrap = chart.controls.wrap.selectAll('.control-group').filter(function(d) { - return d.option === 'r_ratio[0]'; - }); - - //if no max value is defined, use the max value from the data - if (this.config.r_ratio_filter) { - if (!config.r_ratio[1]) { - var raw_max_r_ratio = d3.max(this.raw_data, function(d) { - return d.rRatio; - }); - config.max_r_ratio = Math.ceil(raw_max_r_ratio * 10) / 10; //round up to the nearest 0.1 - config.r_ratio[1] = config.max_r_ratio; - chart.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'r_ratio[0]'; - }) - .select('input#r_ratio_max') - .property('value', config.max_r_ratio); - } - - //make sure r_ratio[0] <= r_ratio[1] - if (config.r_ratio[0] > config.r_ratio[1]) { - config.r_ratio = config.r_ratio.reverse(); - r_ratio_wrap.select('input#r_ratio_min').property('value', config.r_ratio[0]); - r_ratio_wrap.select('input#r_ratio_max').property('value', config.r_ratio[1]); - } - - //Define flag given r-ratio minimum. - this.raw_data.forEach(function(participant_obj) { - var aboveMin = participant_obj.rRatio >= config.r_ratio[0]; - var belowMax = participant_obj.rRatio <= config.r_ratio[1]; - participant_obj.rRatioFlag = aboveMin & belowMax ? 'Y' : 'N'; - }); - } - } - - function addParticipantLevelMetadata(d, participant_obj) { - var varList = []; - if (this.config.filters) { - var filterVars = this.config.filters.map(function(d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, filterVars]); - } - if (this.config.group_cols) { - var groupVars = this.config.group_cols.map(function(d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, groupVars]); - } - if (this.config.details) { - var detailVars = this.config.details.map(function(d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, detailVars]); - } - - varList.forEach(function(v) { - participant_obj[v] = d[0][v]; - }); - } - - function calculateRRatios(d, participant_obj) { - if (this.config.r_ratio_filter) { - //R-ratio should be the ratio of ALT to ALP, i.e. the x-axis to the z-axis. - participant_obj.rRatio = - participant_obj['ALT_relative_uln'] / participant_obj['ALP_relative_uln']; - } - } - - //Converts a one record per measure data object to a one record per participant objects - function flattenData() { - var chart = this; - var config = this.config; - - //make a data set with one row per ID - - //get list of columns to flatten - var colList = []; - var measureCols = [ - 'measure_col', - 'value_col', - 'studyday_col', - 'normal_col_low', - 'normal_col_high' - ]; - - measureCols.forEach(function(d) { - if (Array.isArray(d)) { - d.forEach(function(di) { - colList.push( - di.hasOwnProperty('value_col') ? config[di.value_col] : config[di] - ); - }); - } else { - colList.push(d.hasOwnProperty('value_col') ? config[d.value_col] : config[d]); - } - }); - - //merge in the absolute and relative values - colList = d3.merge([ - colList, - ['absolute', 'relative_uln', 'relative_baseline', 'baseline_absolute', 'analysisFlag'] - ]); - - //get maximum values for each measure type - var flat_data = d3 - .nest() - .key(function(f) { - return f[config.id_col]; - }) - .rollup(function(d) { - var participant_obj = {}; - participant_obj.days_x = null; - participant_obj.days_y = null; - Object.keys(config.measure_values).forEach(function(mKey) { - //get all raw data for the current measure - var matches = d - .filter(function(f) { - return config.measure_values[mKey] == f[config.measure_col]; - }) //get matching measures - .filter(function(f) { - return f.analysisFlag; - }); - - if (matches.length == 0) { - if (config.debug) { - console.warn( - 'No analysis records found for ' + - d[0][config.id_col] + - ' for ' + - mKey - ); - } - - participant_obj.drop_participant = true; - participant_obj.drop_reason = - 'No analysis results found for 1+ key measure, including ' + mKey + '.'; - return participant_obj; - } else { - participant_obj.drop_participant = false; - } - - //get record with maximum value for the current display type - participant_obj[mKey] = d3.max(matches, function(d) { - return +d[config.display]; - }); - - var maxRecord = matches.find(function(d) { - return participant_obj[mKey] == +d[config.display]; - }); - //map all measure specific values - colList.forEach(function(col) { - participant_obj[mKey + '_' + col] = maxRecord[col]; - }); - - //determine whether the value is above the specified threshold - if (config.cuts[mKey][config.display]) { - config.show_quadrants = true; - participant_obj[mKey + '_cut'] = config.cuts[mKey][config.display]; - participant_obj[mKey + '_flagged'] = - participant_obj[mKey] >= participant_obj[mKey + '_cut']; - } else { - config.show_quadrants = false; - participant_obj[mKey + '_cut'] = null; - participant_obj[mKey + '_flagged'] = null; - } - - //save study days for each axis; - if (mKey == config.x.column) - participant_obj.days_x = maxRecord[config.studyday_col]; - if (mKey == config.y.column) - participant_obj.days_y = maxRecord[config.studyday_col]; - }); - - //Add participant level metadata - addParticipantLevelMetadata.call(chart, d, participant_obj); - - //Calculate ratios between measures. - calculateRRatios.call(chart, d, participant_obj); - - //calculate the day difference between x and y - participant_obj.day_diff = Math.abs( - participant_obj.days_x - participant_obj.days_y - ); - - return participant_obj; - }) - .entries( - this.imputed_data.filter(function(f) { - return f.key_measure; - }) - ); - - chart.dropped_participants = flat_data - .filter(function(f) { - return f.values.drop_participant; - }) - .map(function(d) { - return { - id: d.key, - drop_reason: d.values.drop_reason, - allrecords: chart.initial_data.filter(function(f) { - return f[config.id_col] == d.key; - }) - }; - }); - var flat_data = flat_data - .filter(function(f) { - return !f.values.drop_participant; - }) - .map(function(m) { - m.values[config.id_col] = m.key; - - //link the raw data to the flattened object - var allMatches = chart.imputed_data.filter(function(f) { - return f[config.id_col] == m.key; - }); - m.values.raw = allMatches; - - return m.values; - }); - return flat_data; - } - - function setLegendLabel() { - //change the legend label to match the group variable - //or hide legend if group = NONE - this.config.legend.label = - this.config.color_by !== 'NONE' - ? this.config.group_cols[ - this.config.group_cols - .map(function(group) { - return group.value_col; - }) - .indexOf(this.config.color_by) - ].label - : ''; - } - - function showMissingDataWarning() { - var chart = this; - var config = chart.config; - - if (config.debug) { - //confirm participants are only dropped once (?!) - var unique_dropped_participants = d3 - .set( - this.dropped_participants.map(function(m) { - return m.id; - }) - ) - .values().length; - console.log( - 'Of ' + - this.dropped_participants.length + - ' dropped participants, ' + - unique_dropped_participants + - ' are unique.' - ); - console.log(this.dropped_participants); - } - - chart.messages.remove(null, 'droppedPts', chart.messages); //remove message from previous render - if (this.dropped_participants.length > 0) { - var warningText = - this.dropped_participants.length + - ' participants are not plotted. They likely have invalid or missing data for key variables in the current chart. Click here to download a csv with a brief explanation of why each participant was not plotted.'; - - this.messages.add(warningText, 'caution', 'droppedPts', this.messages, function() { - //custom callback to activate the droppedRows download - d3.select(this) - .select('a.ptDownload') - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .datum(chart.dropped_participants) - .on('click', function(d) { - var cols = ['id', 'drop_reason']; - downloadCSV.call(this, d, cols, 'eDishDroppedParticipants'); - }); - }); - } - } - - function dropMissingValues() { - var chart = this; - var config = this.config; - //drop records with missing or invalid (negative) values - var missing_count = d3.sum(this.raw_data, function(f) { - return f[config.x.column] <= 0 || f[config.y.column] <= 0; - }); - - if (missing_count > 0) { - this.raw_data = this.raw_data.map(function(d) { - d.nonPositiveFlag = d[config.x.column] <= 0 || d[config.y.column] <= 0; - var type = config.display == 'relative_uln' ? 'eDish' : 'mDish'; - // generate an informative reason the participant was dropped - var dropText = - type + - ' values could not be generated for ' + - config.x.column + - ' or ' + - config.y.column + - '. '; - - // x type is mdish and baseline is missing - if ((type == 'mDish') & !d[config.x.column + '_baseline_absolute']) { - dropText = dropText + 'Baseline for ' + config.x.column + ' is missing. '; - } - - // y type is mdish and baseline is missing - if ((type == 'mDish') & !d[config.y.column + '_baseline_absolute']) { - dropText = dropText + 'Baseline for ' + config.y.column + ' is missing. '; - } - - d.drop_reason = d.nonPositiveFlag ? dropText : ''; - return d; - }); - - this.dropped_participants = d3.merge([ - this.dropped_participants, - this.raw_data - .filter(function(f) { - return f.nonPositiveFlag; - }) - .map(function(m) { - return { id: m[config.id_col], drop_reason: m.drop_reason }; - }) - ]); - - this.dropped_participants.map(function(m) { - m.raw = chart.initial_data.filter(function(f) { - return f[config.id_col] == m.id; - }); - }); - } - - this.raw_data = this.raw_data.filter(function(f) { - return !f.nonPositiveFlag; - }); - showMissingDataWarning.call(this); - } - - function onPreprocess() { - updateAxisSettings.call(this); //update axis label based on display type - updateControlCutpointLabels.call(this); //update cutpoint control labels given x- and y-axis variables - this.raw_data = flattenData.call(this); //convert from visit-level data to participant-level data - setMaxRRatio.call(this); - setLegendLabel.call(this); //update legend label based on group variable - dropMissingValues.call(this); - } - - function onDataTransform() {} - - function updateQuadrantData() { - var chart = this; - var config = this.config; - - //add "eDISH_quadrant" column to raw_data - var x_var = this.config.x.column; - var y_var = this.config.y.column; - - var x_cut = this.config.cuts[x_var][config.display]; - var y_cut = this.config.cuts[y_var][config.display]; - - this.filtered_data.forEach(function(d) { - var x_cat = d[x_var] >= x_cut ? 'xHigh' : 'xNormal'; - var y_cat = d[y_var] >= y_cut ? 'yHigh' : 'yNormal'; - d['eDISH_quadrant'] = x_cat + ':' + y_cat; - }); - - //update Quadrant data - config.quadrants.forEach(function(quad) { - quad.count = chart.filtered_data.filter(function(d) { - return d.eDISH_quadrant == quad.dataValue; - }).length; - quad.total = chart.filtered_data.length; - quad.percent = d3.format('0.1%')(quad.count / quad.total); - }); - } - - function setDomain(dimension) { - var config = this.config; - var domain = this[dimension].domain(); - var measure = config[dimension].column; - var cut = config.cuts[measure][config.display]; - - //make sure the domain contains the cut point - if (cut * 1.01 >= domain[1]) { - domain[1] = cut * 1.01; - } - - // make sure the domain lower limit captures all of the raw Values - if (this.config[dimension].type == 'linear') { - // just use the lower limit of 0 for continuous - domain[0] = 0; - } else if (this.config[dimension].type == 'log') { - // use the smallest raw value for a log axis - var measure = config.measure_values[config[dimension].column]; - var values = this.imputed_data - .filter(function(f) { - return f[config.measure_col] == measure; - }) - .map(function(m) { - return +m[config.display]; - }) - .filter(function(m) { - return m > 0; - }) - .sort(function(a, b) { - return a - b; - }); - var minValue = d3.min(values); - - if (minValue < domain[0]) { - domain[0] = minValue; - } - - //throw a warning if the domain is > 0 if using log scale - if (this[dimension].type == 'log' && domain[0] <= 0) { - console.warn( - "Can't draw a log " + dimension + '-axis because there are values <= 0.' - ); - } - } - this[dimension + '_dom'] = domain; - } - - function clearVisitPath() { - this.visitPath.selectAll('*').remove(); - } - - function clearParticipantHeader() { - this.participantDetails.header.selectAll('*').remove(); //clear participant header - } - - function hideMeasureTable() { - this.measureTable.draw([]); - this.measureTable.wrap.selectAll('*').style('display', 'none'); - } - - function clearRugs(axis) { - this[axis + '_rug'].selectAll('*').remove(); - } - - function formatPoints() { - var chart = this; - var config = this.config; - var points = this.svg.selectAll('g.point').select('circle'); - - points - .attr('stroke', function(d) { - var disabled = d3.select(this).classed('disabled'); - var raw = d.values.raw[0], - pointColor = chart.colorScale(raw[config.color_by]); - return disabled ? '#ccc' : pointColor; - }) - .attr('fill', function(d) { - var disabled = d3.select(this).classed('disabled'); - var raw = d.values.raw[0], - pointColor = chart.colorScale(raw[config.color_by]); - return disabled ? 'white' : pointColor; - }) - .attr('stroke-width', 1) - .style('clip-path', null); - } - - function clearParticipantDetails() { - var config = this.config; - var points = this.svg.selectAll('g.point').select('circle'); - - points.classed('disabled', false); - this.config.quadrants.table.wrap.style('display', null); - clearVisitPath.call(this); //remove path - clearParticipantHeader.call(this); - clearRugs.call(this, 'x'); //clear rugs - clearRugs.call(this, 'y'); - hideMeasureTable.call(this); //remove the detail table - formatPoints.call(this); - this.participantDetails.wrap.selectAll('*').style('display', 'none'); - } - - function updateFilterLabel() { - if (this.controls.filter_numerator) { - this.controls.filter_numerator.text(this.filtered_data.length); - } - } - - function setCutpointMinimums() { - var chart = this; - var config = this.config; - var lower_limits = { - x: chart['x_dom'][0], - y: chart['y_dom'][0] - }; - - //Make sure cutpoint isn't below lower domain - Comes in to play when changing from log to linear axes - Object.keys(lower_limits).forEach(function(dimension) { - var measure = config[dimension].column; - var current_cut = config.cuts[measure][config.display]; - var min = lower_limits[dimension]; - if (current_cut < min) { - config.cuts[measure][config.display] = min; - chart.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input') - .node().value = min; - } - }); - - //Update cut point controls - var controlWraps = this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return /.-axis Reference Line/i.test(d.description); - }) - .attr('min', function(d) { - return lower_limits[d.description.split('-')[0]]; - }); - - controlWraps.select('input').on('change', function(d) { - var dimension = d.description.split('-')[0].toLowerCase(); - var min = chart[dimension + '_dom'][0]; - var input = d3.select(this); - - //Prevent a cutpoint less than the lower domain. - if (input.property('value') < min) input.property('value', min); - - //Update chart setting. - var measure = config[dimension].column; - config.cuts[measure][config.display] = input.property('value'); - chart.draw(); - }); - } - - function syncCutpoints() { - var chart = this; - var config = this.config; - - //check to see if the cutpoint used is current - if ( - config.cuts.x != config.x.column || - config.cuts.y != config.y.column || - config.cuts.display != config.display - ) { - // if not, update it! - - // track the current cut point variables - config.cuts.x = config.x.column; - config.cuts.y = config.y.column; - config.cuts.display = config.display; - - // update the cutpoint shown in the control - config.cuts.display_change = false; //reset the change flag; - var dimensions = ['x', 'y']; - dimensions.forEach(function(dimension) { - //change the control to point at the correct cut point - var dimInput = chart.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input'); - - dimInput.node().value = config.cuts[config[dimension].column][config.display]; - - //don't think this actually changes functionality, but nice to have it accurate just in case - dimInput.option = - 'settings.cuts.' + [config[dimension].column] + '.' + [config.display]; - }); - } - } - - function hideEmptyChart() { - var emptyChart = this.filtered_data.length == 0; - this.wrap.style('display', emptyChart ? 'none' : 'inline-block'); - this.emptyChartWarning.style('display', emptyChart ? 'inline-block' : 'none'); - } - - function onDraw() { - //clear participant Details - clearParticipantDetails.call(this); - - //get correct cutpoint for the current view - syncCutpoints.call(this); - - //update domains to include cut lines - setDomain.call(this, 'x'); - setDomain.call(this, 'y'); - - //Set update cutpoint interactivity - setCutpointMinimums.call(this); - - //Classify participants in to eDISH quadrants - updateQuadrantData.call(this); - - //update the count in the filter label - updateFilterLabel.call(this); - hideEmptyChart.call(this); - } - - function drawQuadrants() { - var _this = this; - - var config = this.config; - var x_var = this.config.x.column; - var y_var = this.config.y.column; - - var x_cut = this.config.cuts[x_var][config.display]; - var y_cut = this.config.cuts[y_var][config.display]; - - //position for cut-point lines - this.cut_lines.lines - .filter(function(d) { - return d.dimension == 'x'; - }) - .attr('x1', this.x(x_cut)) - .attr('x2', this.x(x_cut)) - .attr('y1', this.plot_height) - .attr('y2', 0); - - this.cut_lines.lines - .filter(function(d) { - return d.dimension == 'y'; - }) - .attr('x1', 0) - .attr('x2', this.plot_width) - .attr('y1', function(d) { - return _this.y(y_cut); - }) - .attr('y2', function(d) { - return _this.y(y_cut); - }); - - this.cut_lines.backing - .filter(function(d) { - return d.dimension == 'x'; - }) - .attr('x1', this.x(x_cut)) - .attr('x2', this.x(x_cut)) - .attr('y1', this.plot_height) - .attr('y2', 0); - - this.cut_lines.backing - .filter(function(d) { - return d.dimension == 'y'; - }) - .attr('x1', 0) - .attr('x2', this.plot_width) - .attr('y1', function(d) { - return _this.y(y_cut); - }) - .attr('y2', function(d) { - return _this.y(y_cut); - }); - - //position labels - this.quadrant_labels.g - .select('text.upper-right') - .attr('x', this.plot_width) - .attr('y', 0); - - this.quadrant_labels.g - .select('text.upper-left') - .attr('x', 0) - .attr('y', 0); - - this.quadrant_labels.g - .select('text.lower-right') - .attr('x', this.plot_width) - .attr('y', this.plot_height); - - this.quadrant_labels.g - .select('text.lower-left') - .attr('x', 0) - .attr('y', this.plot_height); - - this.quadrant_labels.text.text(function(d) { - return d.label + ' (' + d.percent + ')'; - }); - } - - //draw marginal rug for visit-level measures - function drawRugs(d, axis) { - var chart = this; - var config = this.config; - - //get matching measures - var allMatches = d.values.raw[0].raw; - var measure = config.measure_values[config[axis].column]; - var matches = allMatches.filter(function(f) { - return f[config.measure_col] == measure; - }); - - //draw the rug - var min_value = axis == 'x' ? chart.y.domain()[0] : chart.x.domain()[0]; - chart[axis + '_rug'] - .selectAll('text') - .data(matches) - .enter() - .append('text') - .attr('class', 'rug-tick') - .attr('x', function(d) { - return axis == 'x' ? chart.x(d[config.display]) : chart.x(min_value); - }) - .attr('y', function(d) { - return axis == 'y' ? chart.y(d[config.display]) : chart.y(min_value); - }) - // .attr('dy', axis == 'x' ? '-0.2em' : null) - .attr('text-anchor', axis == 'y' ? 'end' : null) - .attr('alignment-baseline', axis == 'x' ? 'hanging' : null) - .attr('font-size', axis == 'x' ? '6px' : null) - .attr('stroke', function(d) { - return chart.colorScale(d[config.color_by]); - }) - .text(function(d) { - return axis == 'x' ? '|' : '–'; - }) - .append('svg:title') - .text(function(d) { - return ( - d[config.measure_col] + - '=' + - d3.format('.2f')(d.absolute) + - ' (' + - d3.format('.2f')(d.relative) + - ' xULN) @ ' + - d[config.visit_col] - ); - }); - } - - function addPointMouseover() { - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - //add event listener to all participant level points - points - .filter(function(d) { - var disabled = d3.select(this).classed('disabled'); - return !disabled; - }) - .on('mouseover', function(d) { - //disable mouseover when highlights (onClick) are visible - var disabled = d3.select(this).classed('disabled'); - if (!disabled) { - //clear previous mouseover if any - points.attr('stroke-width', 1); - clearRugs.call(chart, 'x'); - clearRugs.call(chart, 'y'); - - //draw the rugs - d3.select(this).attr('stroke-width', 3); - drawRugs.call(chart, d, 'x'); - drawRugs.call(chart, d, 'y'); - } - }); - } - - function drawVisitPath(d) { - var chart = this; - var config = chart.config; - - var allMatches = d.values.raw[0].raw; - var x_measure = config.measure_values[config.x.column]; - var y_measure = config.measure_values[config.y.column]; - var matches = allMatches.filter(function(f) { - return f[config.measure_col] == x_measure || f[config.measure_col] == y_measure; - }); - - //get coordinates by visit - var visits = d3 - .set( - matches.map(function(m) { - return m[config.studyday_col]; - }) - ) - .values(); - var visit_data = visits - .map(function(m) { - var visitObj = {}; - visitObj.studyday = +m; - visitObj.visit = config.visit_col - ? matches.filter(function(f) { - return f[config.studyday_col] == m; - })[0][config.visit_col] - : null; - visitObj.visitn = config.visitn_col - ? matches.filter(function(f) { - return f[config.studyday_col] == m; - })[0][config.visitn_col] - : null; - visitObj[config.color_by] = matches[0][config.color_by]; - - //get x coordinate - var x_match = matches - .filter(function(f) { - return f[config.studyday_col] == m; - }) - .filter(function(f) { - return f[config.measure_col] == x_measure; - }); - - if (x_match.length) { - visitObj.x = x_match[0][config.display]; - visitObj.xMatch = x_match[0]; - } else { - visitObj.x = null; - visitObj.xMatch = null; - } - - //get y coordinate - var y_match = matches - .filter(function(f) { - return f[config.studyday_col] == m; - }) - .filter(function(f) { - return f[config.measure_col] == y_measure; - }); - if (y_match.length) { - visitObj.y = y_match[0][config.display]; - visitObj.yMatch = y_match[0]; - } else { - visitObj.y = null; - visitObj.yMatch = null; - } - - return visitObj; - }) - .sort(function(a, b) { - return a.studyday - b.studyday; - }) - .filter(function(f) { - return (f.x > 0) & (f.y > 0); - }); - - //draw the path - var myLine = d3.svg - .line() - .x(function(d) { - return chart.x(d.x); - }) - .y(function(d) { - return chart.y(d.y); - }); - - chart.visitPath.selectAll('*').remove(); - chart.visitPath.moveToFront(); - - var path = chart.visitPath - .append('path') - .attr('class', 'participant-visits') - .datum(visit_data) - .attr('d', myLine) - .attr('stroke', function(d) { - return chart.colorScale(matches[0][config.color_by]); - }) - .attr('stroke-width', '2px') - .attr('fill', 'none'); - - //Little trick for animating line drawing - var totalLength = path.node().getTotalLength(); - path.attr('stroke-dasharray', totalLength + ' ' + totalLength) - .attr('stroke-dashoffset', totalLength) - .transition() - .duration(2000) - .ease('linear') - .attr('stroke-dashoffset', 0); - - //draw visit points - var visitPoints = chart.visitPath - .selectAll('g.visit-point') - .data(visit_data) - .enter() - .append('g') - .attr('class', 'visit-point'); - - visitPoints - .append('circle') - .attr('class', 'participant-visits') - .attr('r', 0) - .attr('stroke', function(d) { - return chart.colorScale(d[config.color_by]); - }) - .attr('stroke-width', 1) - .attr('cx', function(d) { - return chart.x(d.x); - }) - .attr('cy', function(d) { - return chart.y(d.y); - }) - .attr('fill', function(d) { - return chart.colorScale(d[config.color_by]); - }) - .attr('fill-opacity', 0.5) - .transition() - .delay(2000) - .duration(200) - .attr('r', 4); - - //custom titles for points on mouseover - visitPoints.append('title').text(function(d) { - var xvar = config.x.column; - var yvar = config.y.column; - var studyday_label = 'Study day: ' + d.studyday + '\n', - visitn_label = d.visitn ? 'Visit Number: ' + d.visitn + '\n' : '', - visit_label = d.visit ? 'Visit: ' + d.visit + '\n' : '', - x_label = config.x.label + ': ' + d3.format('0.3f')(d.x) + '\n', - y_label = config.y.label + ': ' + d3.format('0.3f')(d.y); - - return studyday_label + visit_label + visitn_label + x_label + y_label; - }); - } - - function makeNestedData(d) { - var chart = this; - var config = chart.config; - var allMatches = d.values.raw[0].raw; - - var ranges = d3 - .nest() - .key(function(d) { - return d[config.measure_col]; - }) - .rollup(function(d) { - var vals = d - .map(function(m) { - return m[config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }); - var lower_extent = d3.quantile(vals, config.measureBounds[0]), - upper_extent = d3.quantile(vals, config.measureBounds[1]); - return [lower_extent, upper_extent]; - }) - .entries(chart.initial_data); - - //make nest by measure - var nested = d3 - .nest() - .key(function(d) { - return d[config.measure_col]; - }) - .rollup(function(d) { - var measureObj = {}; - measureObj.eDish = chart; - measureObj.key = d[0][config.measure_col]; - measureObj.raw = d; - measureObj.values = d.map(function(d) { - return +d[config.value_col]; - }); - measureObj.max = +d3.format('0.2f')(d3.max(measureObj.values)); - measureObj.min = +d3.format('0.2f')(d3.min(measureObj.values)); - measureObj.median = +d3.format('0.2f')(d3.median(measureObj.values)); - measureObj.n = measureObj.values.length; - measureObj.spark = 'spark!'; - measureObj.population_extent = ranges.find(function(f) { - return measureObj.key == f.key; - }).values; - var hasColor = - chart.spaghetti.colorScale.domain().indexOf(d[0][config.measure_col]) > -1; - measureObj.color = hasColor - ? chart.spaghetti.colorScale(d[0][config.measure_col]) - : 'black'; - measureObj.spark_data = d.map(function(m) { - var obj = { - id: m[config.id_col], - lab: m[config.measure_col], - visit: config.visit_col ? m[config.visit_col] : null, - visitn: config.visitn_col ? +m[config.visitn_col] : null, - studyday: +m[config.studyday_col], - value: +m[config.value_col], - lln: config.normal_col_low ? +m[config.normal_col_low] : null, - uln: +m[config.normal_col_high], - population_extent: measureObj.population_extent, - outlier_low: config.normal_col_low - ? +m[config.value_col] < +m[config.normal_col_low] - : null, - outlier_high: +m[config.value_col] > +m[config.normal_col_high] - }; - obj.outlier = obj.outlier_low || obj.outlier_high; - return obj; - }); - return measureObj; - }) - .entries(allMatches); - - var nested = nested - .map(function(m) { - return m.values; - }) - .sort(function(a, b) { - var a_order = Object.keys(config.measure_values) - .map(function(e) { - return config.measure_values[e]; - }) - .indexOf(a.key); - var b_order = Object.keys(config.measure_values) - .map(function(e) { - return config.measure_values[e]; - }) - .indexOf(b.key); - return b_order - a_order; - }); - return nested; - } - - function addSparkLines(d) { - if (this.data.raw.length > 0) { - //don't try to draw sparklines if the table is empty - this.tbody - .selectAll('tr') - .style('background', 'none') - .style('border-bottom', '.5px solid black') - .each(function(row_d) { - //Spark line cell - var cell = d3 - .select(this) - .select('td.spark') - .classed('minimized', true) - .text(''), - toggle = cell - .append('span') - .html('▽') - .style('cursor', 'pointer') - .style('color', '#999') - .style('vertical-align', 'middle'), - width = 100, - height = 25, - offset = 4, - overTime = row_d.spark_data.sort(function(a, b) { - return +a.studyday - +b.studyday; - }), - color = row_d.color; - - var x = d3.scale - .linear() - .domain( - d3.extent(overTime, function(m) { - return m.studyday; - }) - ) - .range([offset, width - offset]); - - //y-domain includes 99th population percentile + any participant outliers - var y_min = d3.min(d3.merge([row_d.values, row_d.population_extent])) * 0.99; - var y_max = d3.max(d3.merge([row_d.values, row_d.population_extent])) * 1.01; - var y = d3.scale - .linear() - .domain([y_min, y_max]) - .range([height - offset, offset]); - - //render the svg - var svg = cell - .append('svg') - .attr({ - width: width, - height: height - }) - .append('g'); - - //draw the normal range polygon ULN and LLN - var upper = overTime.map(function(m) { - return { studyday: m.studyday, value: m.uln }; - }); - var lower = overTime - .map(function(m) { - return { studyday: m.studyday, value: m.lln }; - }) - .reverse(); - var normal_data = d3.merge([upper, lower]).filter(function(m) { - return m.value; - }); - - var drawnormal = d3.svg - .line() - .x(function(d) { - return x(d.studyday); - }) - .y(function(d) { - return y(d.value); - }); - - var normalpath = svg - .append('path') - .datum(normal_data) - .attr({ - class: 'normalrange', - d: drawnormal, - fill: '#eee', - stroke: 'none' - }); - - //draw lines at the population guidelines - svg.selectAll('lines.guidelines') - .data(row_d.population_extent) - .enter() - .append('line') - .attr('class', 'guidelines') - .attr('x1', 0) - .attr('x2', width) - .attr('y1', function(d) { - return y(d); - }) - .attr('y2', function(d) { - return y(d); - }) - .attr('stroke', '#ccc') - .attr('stroke-dasharray', '2 2'); - - //draw the sparkline - var draw_sparkline = d3.svg - .line() - .interpolate('cardinal') - .x(function(d) { - return x(d.studyday); - }) - .y(function(d) { - return y(d.value); - }); - var sparkline = svg - .append('path') - .datum(overTime) - .attr({ - class: 'sparkLine', - d: draw_sparkline, - fill: 'none', - stroke: color - }); - - //draw outliers - var outliers = overTime.filter(function(f) { - return f.outlier; - }); - var outlier_circles = svg - .selectAll('circle.outlier') - .data(outliers) - .enter() - .append('circle') - .attr('class', 'circle outlier') - .attr('cx', function(d) { - return x(d.studyday); - }) - .attr('cy', function(d) { - return y(d.value); - }) - .attr('r', '2px') - .attr('stroke', color) - .attr('fill', color); - }); - } - } - - function insertAfter(newNode, referenceNode) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); - } - - var defaultSettings = { - max_width: 800, - aspect: 4, - x: { - column: 'studyday', - type: 'linear', - label: 'Study Day' - }, - y: { - column: 'value', - type: 'linear', - label: '', - format: '.1f' - }, - marks: [ - { - type: 'line', - per: ['lab'] - }, - { - type: 'circle', - radius: 4, - per: ['lab', 'studyday'] //, - // values: { outlier: [true] }, - // attributes: { - // 'fill-opacity': 1 - // } - } - ], - margin: { top: 20 }, - gridlines: 'x', - colors: [] - }; - - function setDomain$1(d) { - //y-domain includes 99th population percentile + any participant outliers - var raw_values = this.raw_data.map(function(m) { - return m.value; - }); - var population_extent = this.raw_data[0].population_extent; - var y_min = d3.min(d3.merge([raw_values, population_extent])) * 0.99; - var y_max = d3.max(d3.merge([raw_values, population_extent])) * 1.01; - this.y.domain([y_min, y_max]); - this.y_dom = [y_min, y_max]; - } - - function drawPopulationExtent() { - var lineChart = this; - this.svg - .selectAll('line.guidelines') - .data(lineChart.raw_data[0].population_extent) - .enter() - .append('line') - .attr('class', 'guidelines') - .attr('x1', 0) - .attr('x2', lineChart.plot_width) - .attr('y1', function(d) { - return lineChart.y(d); - }) - .attr('y2', function(d) { - return lineChart.y(d); - }) - .attr('stroke', '#ccc') - .attr('stroke-dasharray', '2 2'); - } - - function drawNormalRange() { - var lineChart = this; - var upper = this.raw_data.map(function(m) { - return { studyday: m.studyday, value: m.uln }; - }); - var lower = this.raw_data - .map(function(m) { - return { studyday: m.studyday, value: m.lln }; - }) - .reverse(); - var normal_data = d3.merge([upper, lower]).filter(function(f) { - return f.value || f.value == 0; - }); - var drawnormal = d3.svg - .line() - .x(function(d) { - return lineChart.x(d.studyday); - }) - .y(function(d) { - return lineChart.y(d.value); - }); - var normalpath = this.svg - .append('path') - .datum(normal_data) - .attr({ - class: 'normalrange', - d: drawnormal, - fill: '#eee', - stroke: 'none' - }); - normalpath.moveToBack(); - } - - function addPointTitles() { - var config = this.edish.config; - var points = this.marks[1].circles; - points.select('title').remove(); - points.append('title').text(function(d) { - var raw = d.values.raw[0]; - var xvar = config.x.column; - var yvar = config.y.column; - var studyday_label = 'Study day: ' + raw.studyday + '\n', - visitn_label = raw.visitn ? 'Visit Number: ' + raw.visitn + '\n' : '', - visit_label = raw.visit ? 'Visit: ' + raw.visit + '\n' : '', - lab_label = raw.lab + ': ' + d3.format('0.3f')(raw.value); - return studyday_label + visit_label + visitn_label + lab_label; - }); - } - - function updatePointFill() { - var points = this.marks[1].circles; - points.attr('fill-opacity', function(d) { - var outlier = d.values.raw[0].outlier; - return outlier ? 1 : 0; - }); - } - - function init$2(d, edish) { - //layout the new cells on the DOM (slightly easier than using D3) - var summaryRow_node = this.parentNode; - var chartRow_node = document.createElement('tr'); - var chartCell_node = document.createElement('td'); - insertAfter(chartRow_node, summaryRow_node); - chartRow_node.appendChild(chartCell_node); - - //update the row styles - d3.select(chartRow_node) - .style('background', 'none') - .style('border-bottom', '0.5px solid black'); - - //layout the svg with D3 - var cellCount = d3.select(summaryRow_node).selectAll('td')[0].length; - var chartCell = d3.select(chartCell_node).attr('colspan', cellCount); - - //draw the chart - defaultSettings.colors = [d.color]; - var lineChart = webcharts.createChart(chartCell_node, defaultSettings); - lineChart.on('draw', function() { - setDomain$1.call(this); - }); - lineChart.edish = edish; - lineChart.on('resize', function() { - drawPopulationExtent.call(this); - drawNormalRange.call(this); - addPointTitles.call(this); - updatePointFill.call(this); - }); - lineChart.init(d.spark_data); - lineChart.row = chartRow_node; - return lineChart; - } - - function addSparkClick() { - var edish = this.edish; - if (this.data.raw.length > 0) { - this.tbody - .selectAll('tr') - .select('td.spark') - .on('click', function(d) { - if (d3.select(this).classed('minimized')) { - d3.select(this).classed('minimized', false); - d3.select(this.parentNode).style('border-bottom', 'none'); - - this.lineChart = init$2.call(this, d, edish); - d3.select(this) - .select('svg') - .style('display', 'none'); - - d3.select(this) - .select('span') - .html('△ Minimize Chart'); - } else { - d3.select(this).classed('minimized', true); - - d3.select(this.parentNode).style('border-bottom', '0.5px solid black'); - - d3.select(this) - .select('span') - .html('▽'); - - d3.select(this) - .select('svg') - .style('display', null); - - d3.select(this.lineChart.row).remove(); - this.lineChart.destroy(); - } - }); - } - } - - function addFootnote$1() { - var footnoteText = [ - 'The y-axis for each chart is set to the ' + - this.edish.config.measureBounds - .map(function(bound) { - var percentile = '' + Math.round(bound * 100); - var lastDigit = +percentile.substring(percentile.length - 1); - var text = - percentile + - ([0, 4, 5, 6, 7, 8, 9].indexOf(lastDigit) > -1 - ? 'th' - : lastDigit === 3 - ? 'rd' - : lastDigit === 2 - ? 'nd' - : 'st'); - return text; - }) - .join(' and ') + - " percentiles of the entire population's results for that measure. " + - 'Values outside the normal range are plotted as individual points. ' + - 'Click a sparkline to view a more detailed version of the chart.' - ]; - var footnotes = this.wrap.selectAll('span.footnote').data(footnoteText, function(d) { - return d; - }); - - footnotes - .enter() - .append('span') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('padding-top', '0.1em') - .text(function(d) { - return d; - }); - - footnotes.exit().remove(); - } - - function addExtraMeasureToggle() { - var measureTable = this; - var chart = this.edish; - var config = chart.config; - - measureTable.wrap.selectAll('div.wc-controls').remove(); - - //check to see if there are extra measures in the MeasureTable - var specifiedMeasures = Object.keys(config.measure_values).map(function(e) { - return config.measure_values[e]; - }); - var tableMeasures = measureTable.data.raw.map(function(f) { - return f.key; - }); - - //if extra measure exist... - if (tableMeasures.length > specifiedMeasures.length) { - var extraRows = measureTable.table - .select('tbody') - .selectAll('tr') - .filter(function(f) { - return specifiedMeasures.indexOf(f.key) == -1; - }); - - //hide extra rows by default - extraRows.style('display', 'none'); - - //add a toggle - var toggleDiv = measureTable.wrap - .insert('div', '*') - .attr('class', 'wc-controls') - .append('div') - .attr('class', 'control-group'); - var extraCount = tableMeasures.length - specifiedMeasures.length; - toggleDiv - .append('span') - .attr('class', 'wc-control-label') - .style('display', 'inline-block') - .style('padding-right', '.3em') - .text( - 'Show ' + - extraCount + - ' additional measure' + - (extraCount == 1 ? '' : 's') + - ':' - ); - var toggle = toggleDiv.append('input').property('type', 'checkbox'); - toggle.on('change', function() { - var showRows = this.checked; - extraRows.style('display', showRows ? null : 'none'); - }); - } - } - - function drawMeasureTable(d) { - var nested = makeNestedData.call(this, d); - - //draw the measure table - this.measureTable.edish = this; - this.measureTable.on('draw', function() { - addSparkLines.call(this); - addSparkClick.call(this); - addExtraMeasureToggle.call(this); - addFootnote$1.call(this); - }); - this.measureTable.draw(nested); - } - - function makeParticipantHeader(d) { - var chart = this; - var wrap = this.participantDetails.header; - var raw = d.values.raw[0]; - - var title = this.participantDetails.header - .append('h3') - .attr('class', 'id') - .html('Participant Details') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - if (chart.config.participantProfileURL) { - title - .append('a') - .html('Full Participant Profile') - .attr('href', chart.config.participantProfileURL) - .style('font-size', '0.8em') - .style('padding-left', '1em'); - } - - title - .append('Button') - .text('Clear') - .style('margin-left', '1em') - .style('float', 'right') - .on('click', function() { - clearParticipantDetails.call(chart); - }); - - //show detail variables in a ul - var ul = this.participantDetails.header - .append('ul') - .style('list-style', 'none') - .style('padding', '0'); - - var lis = ul - .selectAll('li') - .data(chart.config.details) - .enter() - .append('li') - .style('', 'block') - .style('display', 'inline-block') - .style('text-align', 'center') - .style('padding', '0.5em'); - - lis.append('div') - .text(function(d) { - return d.label; - }) - .attr('div', 'label') - .style('font-size', '0.8em'); - - lis.append('div') - .text(function(d) { - return raw[d.value_col]; - }) - .attr('div', 'value'); - } - - var defaultSettings$1 = { - max_width: 600, - x: { - column: null, - type: 'linear', - label: 'Study Day' - }, - y: defineProperty( - { - column: 'relative_uln', - type: 'linear', - label: null, // set in ../callbacks/onPreprocess - domain: null, - format: '.1f' - }, - 'domain', - [0, null] - ), - marks: [ - { - type: 'line', - per: [] - }, - { - type: 'circle', - radius: 4, - per: [] - } - ], - margin: { top: 20, bottom: 70 }, // bottom margin provides space for exposure plot - gridlines: 'xy', - color_by: null, - colors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628'], - aspect: 2 - }; - - var controlInputs$1 = [ - { - type: 'subsetter', - label: 'Select Labs', - value_col: null, - multiple: true - }, - { - type: 'dropdown', - label: 'Y-axis Display Type', - description: null, - option: 'displayLabel', - start: null, - values: null, - require: true - } - ]; - - function onLayout$1() { - var spaghetti = this; - var eDish = this.edish; - - //customize the display control - var displayControlWrap = spaghetti.controls.wrap - .selectAll('div') - .filter(function(controlInput) { - return controlInput.label === 'Y-axis Display Type'; - }); - - var displayControl = displayControlWrap.select('select'); - - //set the start value - var start_value = eDish.config.display_options.find(function(f) { - return f.value == eDish.config.display; - }).label; - - displayControl.selectAll('option').attr('selected', function(d) { - return d == start_value ? 'selected' : null; - }); - - displayControl.on('change', function(d) { - var currentLabel = this.value; - var currentValue = eDish.config.display_options.find(function(f) { - return f.label == currentLabel; - }).value; - spaghetti.config.y.column = currentValue; - spaghetti.draw(); - }); - } - - function onPreprocess$1() { - var config = this.config; - var unit = this.config.y.column == 'relative_uln' ? '[xULN]' : '[xBaseline]'; - config.y.label = 'Standardized Result ' + unit; - } - - function drawCutLine(d) { - //bit of a hack to make this work with paths and circles - var spaghetti = this; - var config = this.config; - var raw = d.values.raw ? d.values.raw[0] : d.values[0].values.raw[0]; - var cut = raw[config.y.column + '_cut']; - var param = raw[config.color_by]; - spaghetti.cutLine = spaghetti.svg - .append('line') - .attr('y1', spaghetti.y(cut)) - .attr('y2', spaghetti.y(cut)) - .attr('x1', 0) - .attr('x2', spaghetti.plot_width) - .attr('stroke', spaghetti.colorScale(param)) - .attr('stroke-dasharray', '3 3'); - spaghetti.cutLabel = spaghetti.svg - .append('text') - .attr('y', spaghetti.y(cut)) - .attr('dy', '-0.2em') - .attr('x', spaghetti.plot_width) - .attr('text-anchor', 'end') - .attr('alignment-baseline', 'baseline') - .attr('fill', spaghetti.colorScale(param)) - .text(d3.format('0.1f')(cut)); - } - - function addPointTitles$1() { - var spaghetti = this; - var config = this.edish.config; - var points = this.marks[1].circles; - points.select('title').remove(); - points.append('title').text(function(d) { - var raw = d.values.raw[0]; - var ylabel = spaghetti.config.displayLabel; - var yvar = spaghetti.config.y.column; - var studyday_label = 'Study day: ' + raw[config.studyday_col] + '\n', - visitn_label = config.visitn_col - ? 'Visit Number: ' + raw[config.visitn_col] + '\n' - : '', - visit_label = config.visit_col ? 'Visit: ' + raw[config.visit_col] + '\n' : '', - raw_label = - 'Raw ' + - raw[config.measure_col] + - ': ' + - d3.format('0.3f')(raw[config.value_col]) + - '\n', - adj_label = - 'Adjusted ' + raw[config.measure_col] + ': ' + d3.format('0.3f')(raw[yvar]); - return studyday_label + visit_label + visitn_label + raw_label + adj_label; - }); - } - - function addExposure() { - var context = this; - this.svg.select('.se-exposure-supergroup').remove(); - - //If exposure data exists, annotate exposures beneath x-axis. - if (this.edish.exposure.include) { - var supergroup = this.svg - .insert('g', '.supergroup') - .classed('se-exposure-supergroup', true); - var dy = 20; // offset from chart - var strokeWidth = 5; // width/diameter of marks - this.svg.selectAll('.x.axis .tick text').attr('dy', dy + strokeWidth * 3 + 'px'); // offset x-axis tick labels - - //top boundary line - supergroup.append('line').attr({ - x1: -this.margin.left, - y1: this.plot_height + dy - strokeWidth * 2, - x2: this.plot_width, - y2: this.plot_height + dy - strokeWidth * 2, - stroke: 'black', - 'stroke-opacity': 0.1 - }); - - //Exposure text - supergroup - .append('text') - .attr({ - x: -3, - y: this.plot_height + dy + strokeWidth, - 'text-anchor': 'end', - textLength: this.margin.left - 3 - }) - .text('Exposure'); - - //bottom boundary line - supergroup.append('line').attr({ - x1: -this.margin.left, - y1: this.plot_height + dy + strokeWidth * 2, - x2: this.plot_width, - y2: this.plot_height + dy + strokeWidth * 2, - stroke: 'black', - 'stroke-opacity': 0.1 - }); - - //Exposures - var groups = supergroup - .selectAll('g.se-exposure-group') - .data(this.exposure_data) - .enter() - .append('g') - .classed('se-exposure-group', true); - groups.each(function(d) { - var group = d3.select(this); - - //draw a line if exposure start and end dates are unequal - if ( - d[context.edish.config.exposure_stdy_col] !== - d[context.edish.config.exposure_endy_col] - ) { - group - .append('line') - .classed('se-exposure-line', true) - .attr({ - x1: function x1(d) { - return context.x(+d[context.edish.config.exposure_stdy_col]); - }, - y1: context.plot_height + dy, - x2: function x2(d) { - return context.x(+d[context.edish.config.exposure_endy_col]); - }, - y2: context.plot_height + dy, - stroke: 'black', - 'stroke-width': strokeWidth, - 'stroke-opacity': 0.25 - }) - .on('mouseover', function(d) { - this.setAttribute('stroke-width', strokeWidth * 2); - - //annotate a rectangle in the chart - group - .append('rect') - .classed('se-exposure-highlight', true) - .attr({ - x: function x(d) { - return context.x( - +d[context.edish.config.exposure_stdy_col] - ); - }, - y: 0, - width: function width(d) { - return ( - context.x(+d[context.edish.config.exposure_endy_col]) - - context.x(+d[context.edish.config.exposure_stdy_col]) - ); - }, - height: context.plot_height, - fill: 'black', - 'fill-opacity': 0.25 - }); - }) - .on('mouseout', function(d) { - this.setAttribute('stroke-width', strokeWidth); - - //remove rectangle from the chart - group.select('.se-exposure-highlight').remove(); - }) - .append('title') - .text( - 'Study Day: ' + - d[context.edish.config.exposure_stdy_col] + - '-' + - d[context.edish.config.exposure_endy_col] + - ' (' + - (+d[context.edish.config.exposure_endy_col] - - +d[context.edish.config.exposure_stdy_col] + - (+d[context.edish.config.exposure_endy_col] >= - +d[context.edish.config.exposure_stdy_col])) + - ' days)\nTreatment: ' + - d[context.edish.config.exposure_trt_col] + - '\nDose: ' + - d[context.edish.config.exposure_dose_col] + - ' ' + - d[context.edish.config.exposure_dosu_col] - ); - } - //draw a circle if exposure start and end dates are equal - else { - group - .append('circle') - .classed('se-exposure-circle', true) - .attr({ - cx: function cx(d) { - return context.x(+d[context.edish.config.exposure_stdy_col]); - }, - cy: context.plot_height + dy, - r: strokeWidth / 2, - fill: 'black', - 'fill-opacity': 0.25, - stroke: 'black', - 'stroke-opacity': 1 - }) - .on('mouseover', function(d) { - this.setAttribute('r', strokeWidth); - - //annotate a vertical line in the chart - group - .append('line') - .classed('se-exposure-highlight', true) - .attr({ - x1: context.x(+d[context.edish.config.exposure_stdy_col]), - y1: 0, - x2: context.x(+d[context.edish.config.exposure_stdy_col]), - y2: context.plot_height, - stroke: 'black', - 'stroke-width': 1, - 'stroke-opacity': 0.5, - 'stroke-dasharray': '3 1' - }); - }) - .on('mouseout', function(d) { - this.setAttribute('r', strokeWidth / 2); - - //remove vertical line from the chart - group.select('.se-exposure-highlight').remove(); - }) - .append('title') - .text( - 'Study Day: ' + - d[context.edish.config.exposure_stdy_col] + - '\nTreatment: ' + - d[context.edish.config.exposure_trt_col] + - '\nDose: ' + - d[context.edish.config.exposure_dose_col] + - ' ' + - d[context.edish.config.exposure_dosu_col] - ); - } - }); - } - } - - function onResize() { - var spaghetti = this; - var config = this.config; - - addPointTitles$1.call(this); - - //fill circles above the cut point - var y_col = this.config.y.column; - this.marks[1].circles - .attr('fill-opacity', function(d) { - return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; - }) - .attr('fill-opacity', function(d) { - return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; - }); - - //Show cut lines on mouseover - this.marks[1].circles - .on('mouseover', function(d) { - drawCutLine.call(spaghetti, d); - }) - .on('mouseout', function() { - spaghetti.cutLine.remove(); - spaghetti.cutLabel.remove(); - }); - - this.marks[0].paths - .on('mouseover', function(d) { - drawCutLine.call(spaghetti, d); - }) - .on('mouseout', function() { - spaghetti.cutLine.remove(); - spaghetti.cutLabel.remove(); - }); - - //annotate treatment exposure - addExposure.call(this); - - //embiggen clip-path so points aren't clipped - var radius = this.config.marks.find(function(mark) { - return mark.type === 'circle'; - }).radius; - this.svg - .select('.plotting-area') - .attr('width', this.plot_width + radius * 2 + 2) // plot width + circle radius * 2 + circle stroke width * 2 - .attr('height', this.plot_height + radius * 2 + 2) // plot height + circle radius * 2 + circle stroke width * 2 - .attr( - 'transform', - 'translate(-' + - (radius + 1) + // translate left circle radius + circle stroke width - ',-' + - (radius + 1) + // translate up circle radius + circle stroke width - ')' - ); - } - - function onDraw$1() { - var _this = this; - - var spaghetti = this; - var eDish = this.edish; - - //make sure x-domain includes the extent of the exposure data - if (this.edish.exposure.include) { - this.exposure_data = this.edish.exposure.data.filter(function(d) { - return d[_this.edish.config.id_col] === _this.edish.clicked_id; - }); - var extent = [ - d3.min(this.exposure_data, function(d) { - return +d[_this.edish.config.exposure_stdy_col]; - }), - d3.max(this.exposure_data, function(d) { - return +d[_this.edish.config.exposure_endy_col]; - }) - ]; - if (extent[0] < this.x_dom[0]) this.x_dom[0] = extent[0]; - if (extent[1] > this.x_dom[1]) this.x_dom[1] = extent[1]; - } - - //make sure y domain includes the current cut point for all measures - var max_value = d3.max(spaghetti.filtered_data, function(f) { - return f[spaghetti.config.y.column]; - }); - var max_cut = d3.max(spaghetti.filtered_data, function(f) { - return f[spaghetti.config.y.column + '_cut']; - }); - var y_max = d3.max([max_value, max_cut]); - spaghetti.config.y.domain = [0, y_max]; - spaghetti.y_dom = spaghetti.config.y.domain; - - //initialize the measureTable - if (spaghetti.config.firstDraw) { - drawMeasureTable.call(eDish, this.participant_data); - spaghetti.config.firstDraw = false; - } - } - - function init$3(d) { - var chart = this; //the full eDish object - var config = this.config; //the eDish config - var matches = d.values.raw[0].raw.filter(function(f) { - return f.key_measure; - }); - - if ('spaghetti' in chart) { - chart.spaghetti.destroy(); - } - - //sync settings - defaultSettings$1.x.column = config.studyday_col; - defaultSettings$1.color_by = config.measure_col; - defaultSettings$1.marks[0].per = [config.id_col, config.measure_col]; - defaultSettings$1.marks[1].per = [config.id_col, config.studyday_col, config.measure_col]; - defaultSettings$1.firstDraw = true; //only initailize the measure table on first draw - - //flag variables above the cut-off - matches.forEach(function(d) { - var measure = d[config['measure_col']]; - var label = Object.keys(config.measure_values).find(function(key) { - return config.measure_values[key] == measure; - }); - - d.relative_uln_cut = config.cuts[label].relative_uln; - d.relative_baseline_cut = config.cuts[label].relative_baseline; - - d.relative_uln_flagged = d.relative_uln >= d.relative_uln_cut; - d.relative_baseline_flagged = d.relative_baseline >= d.relative_baseline_cut; - }); - - //update the controls - var spaghettiElement = this.element + ' .participantDetails .spaghettiPlot .chart'; - - //Add y axis type options - controlInputs$1.find(function(f) { - return f.label == 'Y-axis Display Type'; - }).values = config.display_options.map(function(m) { - return m.label; - }); - - //sync parameter filter - controlInputs$1.find(function(f) { - return f.label == 'Select Labs'; - }).value_col = config.measure_col; - - var spaghettiControls = webcharts.createControls(spaghettiElement, { - location: 'top', - inputs: controlInputs$1 - }); - - //draw that chart - if (!this.exposure.include) delete defaultSettings$1.margin.bottom; // use default bottom margin when not plotting exposure - chart.spaghetti = webcharts.createChart( - spaghettiElement, - defaultSettings$1, - spaghettiControls - ); - - chart.spaghetti.edish = chart; //link the full eDish object - chart.spaghetti.participant_data = d; //include the passed data (used to initialize the measure table) - chart.spaghetti.on('layout', onLayout$1); - chart.spaghetti.on('preprocess', onPreprocess$1); - chart.spaghetti.on('draw', onDraw$1); - chart.spaghetti.on('resize', onResize); - chart.spaghetti.init(matches); - - //add a footnote - chart.spaghetti.wrap - .append('div') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('padding-top', '0.1em') - .text( - 'Points are filled for values above the current reference value. Mouseover a line to see the reference line for that lab.' - ); - } - - function addPointClick() { - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - - //add event listener to all participant level points - points.on('click', function(d) { - chart.clicked_id = d.key; - clearParticipantDetails.call(chart, d); //clear the previous participant - chart.config.quadrants.table.wrap.style('display', 'none'); //hide the quadrant summary - - //format the eDish chart - points - .attr('stroke', '#ccc') //set all points to gray - .attr('fill', 'white') - .classed('disabled', true); //disable mouseover while viewing participant details - - d3.select(this) - .attr('stroke', function(d) { - return chart.colorScale(d.values.raw[0][config.color_by]); - }) //highlight selected point - .attr('stroke-width', 3); - - //Add elements to the eDish chart - drawVisitPath.call(chart, d); //draw the path showing participant's pattern over time - drawRugs.call(chart, d, 'x'); - drawRugs.call(chart, d, 'y'); - - //draw the "detail view" for the clicked participant - chart.participantDetails.wrap.selectAll('*').style('display', null); - makeParticipantHeader.call(chart, d); - init$3.call(chart, d); //NOTE: the measure table is initialized from within the spaghettiPlot - }); - } - - function addPointTitles$2() { - var config = this.config; - var points = this.marks[0].circles; - points.select('title').remove(); - points.append('title').text(function(d) { - var xvar = config.x.column; - var yvar = config.y.column; - var raw = d.values.raw[0], - xLabel = - config.x.label + - ': ' + - d3.format('0.2f')(raw[xvar]) + - ' @ Day ' + - raw[xvar + '_' + config.studyday_col], - yLabel = - config.y.label + - ': ' + - d3.format('0.2f')(raw[yvar]) + - ' @ Day ' + - raw[yvar + '_' + config.studyday_col], - dayDiff = raw['day_diff'] + ' days apart', - idLabel = 'Participant ID: ' + raw[config.id_col], - rRatioLabel = config.r_ratio_filter - ? '\n' + 'Overall R Ratio: ' + d3.format('0.2f')(raw.rRatio) - : ''; - return idLabel + rRatioLabel + '\n' + xLabel + '\n' + yLabel + '\n' + dayDiff; - }); - } - - function addAxisLabelTitles() { - var chart = this; - var config = this.config; - - var details = - config.display == 'relative_uln' - ? 'Values are plotted as multiples of the upper limit of normal for the measure.' - : config.display == 'relative_baseline' - ? "Values are plotted as multiples of the partipant's baseline value for the measure." - : config.display == 'absolute' - ? ' Values are plotted using the raw units for the measure.' - : null; - - var axisLabels = chart.svg - .selectAll('.axis') - .select('.axis-title') - .select('tspan') - .remove(); - - var axisLabels = chart.svg - .selectAll('.axis') - .select('.axis-title') - .append('tspan') - .html(function(d) { - //var current = d3.select(this).text(); - return ' ⓘ'; - }) - .attr('font-size', '0.8em') - .style('cursor', 'help') - .append('title') - .text(details); - } - - function toggleLegend() { - var hideLegend = this.config.color_by == 'NONE'; - this.wrap.select('.legend').style('display', hideLegend ? 'None' : 'block'); - } - - function dragStarted() { - var dimension = d3.select(this).classed('x') ? 'x' : 'y'; - var chart = d3.select(this).datum().chart; - - d3.select(this) - .select('line.cut-line') - .attr('stroke-width', '2') - .attr('stroke-dasharray', '2,2'); - - chart.quadrant_labels.g.style('display', 'none'); - } - - function dragged() { - var chart = d3.select(this).datum().chart; - - var x = d3.event.dx; - var y = d3.event.dy; - - var line = d3.select(this).select('line.cut-line'); - var lineBack = d3.select(this).select('line.cut-line-backing'); - - var dimension = d3.select(this).classed('x') ? 'x' : 'y'; - - // Update the line properties - var attributes = { - x1: Math.max(0, parseInt(line.attr('x1')) + (dimension == 'x' ? x : 0)), - x2: Math.max(0, parseInt(line.attr('x2')) + (dimension == 'x' ? x : 0)), - y1: Math.min(chart.plot_height, parseInt(line.attr('y1')) + (dimension == 'y' ? y : 0)), - y2: Math.min(chart.plot_height, parseInt(line.attr('y2')) + (dimension == 'y' ? y : 0)) - }; - - line.attr(attributes); - lineBack.attr(attributes); - - var rawCut = line.attr(dimension + '1'); - var current_cut = +d3.format('0.1f')(chart[dimension].invert(rawCut)); - - //update the cut control in real time - chart.controls.wrap - .selectAll('div.control-group') - .filter(function(f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input') - .node().value = current_cut; - var measure = chart.config[dimension].column; - chart.config.cuts[measure][chart.config.display] = current_cut; - } - - function dragEnded() { - var chart = d3.select(this).datum().chart; - - d3.select(this) - .select('line.cut-line') - .attr('stroke-width', '1') - .attr('stroke-dasharray', '5,5'); - chart.quadrant_labels.g.style('display', null); - - //redraw the chart (updates the needed cutpoint settings and quadrant annotations) - chart.draw(); - } - - // credit to https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19 - - function init$4() { - var drag = d3.behavior - .drag() - .origin(function(d) { - return d; - }) - .on('dragstart', dragStarted) - .on('drag', dragged) - .on('dragend', dragEnded); - - this.cut_lines.wrap.moveToFront(); - this.cut_lines.g.call(drag); - } - - function addBoxPlot( - svg, - results, - height, - width, - domain, - boxPlotWidth, - boxColor, - boxInsideColor, - fmt, - horizontal, - log - ) { - //set default orientation to "horizontal" - var horizontal = horizontal == undefined ? true : horizontal; - - //make the results numeric and sort - var results = results - .map(function(d) { - return +d; - }) - .sort(d3.ascending); - - //set up d3.scales - if (horizontal) { - var y = log ? d3.scale.log() : d3.scale.linear(); - y.range([height, 0]).domain(domain); - var x = d3.scale.linear().range([0, width]); - } else { - var x = log ? d3.scale.log() : d3.scale.linear(); - x.range([0, width]).domain(domain); - var y = d3.scale.linear().range([height, 0]); - } - - var probs = [0.05, 0.25, 0.5, 0.75, 0.95]; - for (var i = 0; i < probs.length; i++) { - probs[i] = d3.quantile(results, probs[i]); - } - - var boxplot = svg - .append('g') - .attr('class', 'boxplot') - .datum({ values: results, probs: probs }); - - //draw rectangle from q1 to q3 - var box_x = horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[1]); - var box_width = horizontal - ? x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2) - : x(probs[3]) - x(probs[1]); - var box_y = horizontal ? y(probs[3]) : y(0.5 + boxPlotWidth / 2); - var box_height = horizontal - ? -y(probs[3]) + y(probs[1]) - : y(0.5 - boxPlotWidth / 2) - y(0.5 + boxPlotWidth / 2); - - boxplot - .append('rect') - .attr('class', 'boxplot fill') - .attr('x', box_x) - .attr('width', box_width) - .attr('y', box_y) - .attr('height', box_height) - .style('fill', boxColor); - - //draw dividing lines at d3.median, 95% and 5% - var iS = [0, 2, 4]; - var iSclass = ['', 'd3.median', '']; - var iSColor = [boxColor, boxInsideColor, boxColor]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot ' + iSclass[i]) - .attr('x1', horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('x2', horizontal ? x(0.5 + boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('y1', horizontal ? y(probs[iS[i]]) : y(0.5 - boxPlotWidth / 2)) - .attr('y2', horizontal ? y(probs[iS[i]]) : y(0.5 + boxPlotWidth / 2)) - .style('fill', iSColor[i]) - .style('stroke', iSColor[i]); - } - - //draw lines from 5% to 25% and from 75% to 95% - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot') - .attr('x1', horizontal ? x(0.5) : x(probs[iS[i][0]])) - .attr('x2', horizontal ? x(0.5) : x(probs[iS[i][1]])) - .attr('y1', horizontal ? y(probs[iS[i][0]]) : y(0.5)) - .attr('y2', horizontal ? y(probs[iS[i][1]]) : y(0.5)) - .style('stroke', boxColor); - } - - boxplot - .append('circle') - .attr('class', 'boxplot d3.mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 3) : y(1 - boxPlotWidth / 3)) - .style('fill', boxInsideColor) - .style('stroke', boxColor); - - boxplot - .append('circle') - .attr('class', 'boxplot d3.mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 6) : y(1 - boxPlotWidth / 6)) - .style('fill', boxColor) - .style('stroke', 'None'); - - var formatx = fmt ? d3.format(fmt) : d3.format('.2f'); - - boxplot - .selectAll('.boxplot') - .append('title') - .text(function(d) { - return ( - 'N = ' + - d.values.length + - '\n' + - 'd3.min = ' + - d3.min(d.values) + - '\n' + - '5th % = ' + - formatx(d3.quantile(d.values, 0.05)) + - '\n' + - 'Q1 = ' + - formatx(d3.quantile(d.values, 0.25)) + - '\n' + - 'd3.median = ' + - formatx(d3.median(d.values)) + - '\n' + - 'Q3 = ' + - formatx(d3.quantile(d.values, 0.75)) + - '\n' + - '95th % = ' + - formatx(d3.quantile(d.values, 0.95)) + - '\n' + - 'd3.max = ' + - d3.max(d.values) + - '\n' + - 'd3.mean = ' + - formatx(d3.mean(d.values)) + - '\n' + - 'StDev = ' + - formatx(d3.deviation(d.values)) - ); - }); - } - - function init$5() { - // Draw box plots - this.svg.selectAll('g.boxplot').remove(); - - // Y-axis box plot - var yValues = this.current_data.map(function(d) { - return d.values.y; - }); - var ybox = this.svg.append('g').attr('class', 'yMargin'); - addBoxPlot( - ybox, - yValues, - this.plot_height, - 1, - this.y_dom, - 10, - '#bbb', - 'white', - '0.2f', - true, - this.config.y.type == 'log' - ); - ybox.select('g.boxplot').attr( - 'transform', - 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' - ); - - //X-axis box plot - var xValues = this.current_data.map(function(d) { - return d.values.x; - }); - var xbox = this.svg.append('g').attr('class', 'xMargin'); - addBoxPlot( - xbox, //svg element - xValues, //values - 1, //height - this.plot_width, //width - this.x_dom, //domain - 10, //box plot width - '#bbb', //box color - 'white', //detail color - '0.2f', //format - false, // horizontal? - this.config.y.type == 'log' // log? - ); - xbox.select('g.boxplot').attr( - 'transform', - 'translate(0,' + -(this.config.margin.top / 2) + ')' - ); - } - - function setPointSize() { - var _this = this; - - var chart = this; - var config = this.config; - var points = this.svg.selectAll('g.point').select('circle'); - if (config.point_size != 'Uniform') { - //create the scale - var sizeScale = d3.scale - .linear() - .range([2, 10]) - .domain( - d3.extent( - chart.raw_data.map(function(m) { - return m[config.point_size]; - }) - ) - ); - - //draw a legend (coming later?) - - //set the point radius - points - .transition() - .attr('r', function(d) { - var raw = d.values.raw[0]; - return sizeScale(raw[config.point_size]); - }) - .attr('cx', function(d) { - return _this.x(d.values.x); - }) - .attr('cy', function(d) { - return _this.y(d.values.y); - }); - } - } - - function setPointOpacity() { - var config = this.config; - var points = this.svg.selectAll('g.point').select('circle'); - points.attr('fill-opacity', function(d) { - return d.values.raw[0].day_diff <= config.visit_window ? 1 : 0; - }); //fill points in visit_window - } - - function adjustTicks() { - this.svg - .selectAll('.x.axis .tick text') - .attr({ - transform: 'rotate(-45)', - dx: -10, - dy: 10 - }) - .style('text-anchor', 'end'); - } - - // Reposition any exisiting participant marks when the chart is resized - function updateParticipantMarks() { - var chart = this; - var config = this.config; - - //reposition participant visit path - var myNewLine = d3.svg - .line() - .x(function(d) { - return chart.x(d.x); - }) - .y(function(d) { - return chart.y(d.y); - }); - - chart.visitPath - .select('path') - .transition() - .attr('d', myNewLine); - - //reposition participant visit circles and labels - chart.visitPath - .selectAll('g.visit-point') - .select('circle') - .transition() - .attr('cx', function(d) { - return chart.x(d.x); - }) - .attr('cy', function(d) { - return chart.y(d.y); - }); - - chart.visitPath - .selectAll('g.visit-point') - .select('text.participant-visits') - .transition() - .attr('x', function(d) { - return chart.x(d.x); - }) - .attr('y', function(d) { - return chart.y(d.y); - }); - - //reposition axis rugs - chart.x_rug - .selectAll('text') - .transition() - .attr('x', function(d) { - return chart.x(d[config.display]); - }) - .attr('y', function(d) { - return chart.y(chart.y.domain()[0]); - }); - - chart.y_rug - .selectAll('text') - .transition() - .attr('x', function(d) { - return chart.x(chart.x.domain()[0]); - }) - .attr('y', function(d) { - return chart.y(d[config.display]); - }); - } - - function updateTimingFootnote() { - var config = this.config; - var windowText = - config.visit_window == 0 - ? 'on the same day' - : config.visit_window == 1 - ? 'within 1 day' - : 'within ' + config.visit_window + ' days'; - var timingFootnote = - ' Points where maximum ' + - config.measure_values[config.x.column] + - ' and ' + - config.measure_values[config.y.column] + - ' values were collected ' + - windowText + - ' are filled, others are empty.'; - - this.footnote.timing.text(timingFootnote); - } - - function onResize$1() { - //add point interactivity, custom title and formatting - addPointMouseover.call(this); - addPointClick.call(this); - addPointTitles$2.call(this); - addAxisLabelTitles.call(this); - formatPoints.call(this); - setPointSize.call(this); - setPointOpacity.call(this); - updateParticipantMarks.call(this); - - //draw the quadrants and add drag interactivity - updateSummaryTable.call(this); - drawQuadrants.call(this); - init$4.call(this); - - // hide the legend if no group options are given - toggleLegend.call(this); - - // add boxplots - init$5.call(this); - - //axis formatting - adjustTicks.call(this); - - //add timing footnote - updateTimingFootnote.call(this); - } - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDataTransform: onDataTransform, - onDraw: onDraw, - onResize: onResize$1 - }; - - function init$6() { - var lb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - var ex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; - - //const data = mergeData(lb,ex); - this.data = { - lb: lb, - ex: ex - }; - this.chart.exposure = { - include: Array.isArray(ex) && ex.length, - data: ex - }; - this.chart.init(lb); - } - - function safetyedish(element, settings) { - var initial_settings = clone(settings); - var defaultSettings = configuration.settings(); - var controlInputs = configuration.controlInputs(); - var mergedSettings = Object.assign({}, defaultSettings, settings); - var syncedSettings = configuration.syncSettings(mergedSettings); - var syncedControlInputs = configuration.syncControlInputs(controlInputs, syncedSettings); - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - var chart = webcharts.createChart(element, syncedSettings, controls); - - chart.element = element; - chart.initial_settings = initial_settings; - - //Define callbacks. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } - var se = { - element: element, - settings: settings, - chart: chart, - init: init$6 - }; - - return se; - } - - return safetyedish; -}); From a97fd47ff1c4f65b99d5178c231aacf16066673c Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 5 Jun 2019 10:12:25 -0700 Subject: [PATCH 34/39] update hep-explorer --- .../lib/hep-explorer-1.0.1/hepexplorer.js | 4633 +++++++++++++++++ 1 file changed, 4633 insertions(+) create mode 100644 inst/htmlwidgets/lib/hep-explorer-1.0.1/hepexplorer.js diff --git a/inst/htmlwidgets/lib/hep-explorer-1.0.1/hepexplorer.js b/inst/htmlwidgets/lib/hep-explorer-1.0.1/hepexplorer.js new file mode 100644 index 00000000..7d48a228 --- /dev/null +++ b/inst/htmlwidgets/lib/hep-explorer-1.0.1/hepexplorer.js @@ -0,0 +1,4633 @@ +(function(global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? (module.exports = factory(require('webcharts'))) + : typeof define === 'function' && define.amd + ? define(['webcharts'], factory) + : (global.hepexplorer = factory(global.webCharts)); +})(this, function(webcharts) { + 'use strict'; + + if (typeof Object.assign != 'function') { + Object.defineProperty(Object, 'assign', { + value: function assign(target, varArgs) { + if (target == null) { + // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { + // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + + return to; + }, + writable: true, + configurable: true + }); + } + + if (!Array.prototype.find) { + Object.defineProperty(Array.prototype, 'find', { + value: function value(predicate) { + // 1. Let O be ? ToObject(this value). + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + var o = Object(this); + + // 2. Let len be ? ToLength(? Get(O, 'length')). + var len = o.length >>> 0; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + + // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. + var thisArg = arguments[1]; + + // 5. Let k be 0. + var k = 0; + + // 6. Repeat, while k < len + while (k < len) { + // a. Let Pk be ! ToString(k). + // b. Let kValue be ? Get(O, Pk). + // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). + // d. If testResult is true, return kValue. + var kValue = o[k]; + if (predicate.call(thisArg, kValue, k, o)) { + return kValue; + } + // e. Increase k by 1. + k++; + } + + // 7. Return undefined. + return undefined; + } + }); + } + + if (!Array.prototype.findIndex) { + Object.defineProperty(Array.prototype, 'findIndex', { + value: function value(predicate) { + // 1. Let O be ? ToObject(this value). + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + var o = Object(this); + + // 2. Let len be ? ToLength(? Get(O, "length")). + var len = o.length >>> 0; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + + // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. + var thisArg = arguments[1]; + + // 5. Let k be 0. + var k = 0; + + // 6. Repeat, while k < len + while (k < len) { + // a. Let Pk be ! ToString(k). + // b. Let kValue be ? Get(O, Pk). + // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). + // d. If testResult is true, return k. + var kValue = o[k]; + if (predicate.call(thisArg, kValue, k, o)) { + return k; + } + // e. Increase k by 1. + k++; + } + + // 7. Return -1. + return -1; + } + }); + } + + // https://github.com/wbkd/d3-extended + d3.selection.prototype.moveToFront = function() { + return this.each(function() { + this.parentNode.appendChild(this); + }); + }; + + d3.selection.prototype.moveToBack = function() { + return this.each(function() { + var firstChild = this.parentNode.firstChild; + if (firstChild) { + this.parentNode.insertBefore(this, firstChild); + } + }); + }; + + var _typeof = + typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' + ? function(obj) { + return typeof obj; + } + : function(obj) { + return obj && + typeof Symbol === 'function' && + obj.constructor === Symbol && + obj !== Symbol.prototype + ? 'symbol' + : typeof obj; + }; + + var defineProperty = function(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + }; + + /*------------------------------------------------------------------------------------------------\ + Clone a variable (http://stackoverflow.com/a/728694). + \------------------------------------------------------------------------------------------------*/ + + function clone(obj) { + var copy; + + //Handle the 3 simple types, and null or undefined + if (null == obj || 'object' != (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) + return obj; + + //Handle Date + if (obj instanceof Date) { + copy = new Date(); + copy.setTime(obj.getTime()); + return copy; + } + + //Handle Array + if (obj instanceof Array) { + copy = []; + for (var i = 0, len = obj.length; i < len; i++) { + copy[i] = clone(obj[i]); + } + return copy; + } + + //Handle Object + if (obj instanceof Object) { + copy = {}; + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); + } + return copy; + } + + throw new Error("Unable to copy obj! Its type isn't supported."); + } + + function settings() { + return { + //LB domain settings + id_col: 'USUBJID', + studyday_col: 'DY', + value_col: 'STRESN', + measure_col: 'TEST', + normal_col_low: null, + normal_col_high: 'STNRHI', + visit_col: null, + visitn_col: null, + + //DM domain settings + group_cols: null, + filters: null, + details: null, + + //EX domain settings + exposure_stdy_col: 'EXSTDY', + exposure_endy_col: 'EXENDY', + exposure_trt_col: 'EXTRT', + exposure_dose_col: 'EXDOSE', + exposure_dosu_col: 'EXDOSU', + + //analysis settings + analysisFlag: { + value_col: null, + values: [] + }, + baseline: { + value_col: null, //synced with studyday_col in syncsettings() + values: [0] + }, + measure_values: { + ALT: 'Aminotransferase, alanine (ALT)', + AST: 'Aminotransferase, aspartate (AST)', + TB: 'Total Bilirubin', + ALP: 'Alkaline phosphatase (ALP)' + }, + x_options: ['ALT', 'AST', 'ALP'], + y_options: ['TB'], + point_size: 'Uniform', + point_size_options: ['ALT', 'AST', 'ALP', 'TB'], + cuts: { + ALT: { + relative_baseline: 3.8, + relative_uln: 3 + }, + AST: { + relative_baseline: 3.8, + relative_uln: 3 + }, + TB: { + relative_baseline: 4.8, + relative_uln: 2 + }, + ALP: { + relative_baseline: 3.8, + relative_uln: 1 + }, + xMeasure: null, //set in syncSettings + yMeasure: null, //set in syncSettings + display: null //set in syncSettings + }, + imputation_methods: { + ALT: 'data-driven', + AST: 'data-driven', + TB: 'data-driven', + ALP: 'data-driven' + }, + imputation_values: null, + display: 'relative_uln', //or "relative_baseline" + display_options: [ + { label: 'Upper limit of normal adjusted (eDish)', value: 'relative_uln' }, + { label: 'Baseline adjusted (mDish)', value: 'relative_baseline' } + ], + measureBounds: [0.01, 0.99], + populationProfileURL: null, + participantProfileURL: null, + r_ratio_filter: true, + r_ratio: [0, null], + visit_window: 30, + title: 'Hepatic Safety Explorer', + downloadLink: true, + filters_multiselect: true, + warningText: + "This graphic has been thoroughly tested, but is not validated. Any clinical recommendations based on this tool should be confirmed using your organization's standard operating procedures.", + //all values set in onLayout/quadrants/*.js + quadrants: [ + { + label: "Possible Hy's Law Range", + position: 'upper-right', + dataValue: 'xHigh:yHigh', + count: null, + total: null, + percent: null + }, + { + label: 'Hyperbilirubinemia', + position: 'upper-left', + dataValue: 'xNormal:yHigh', + count: null, + total: null, + percent: null + }, + { + label: "Temple's Corollary", + position: 'lower-right', + dataValue: 'xHigh:yNormal', + count: null, + total: null, + percent: null + }, + { + label: 'Normal Range', + position: 'lower-left', + dataValue: 'xNormal:yNormal', + count: null, + total: null, + percent: null + } + ], + + //Standard webcharts settings + x: { + column: null, //set in onPreprocess/updateAxisSettings + label: null, // set in onPreprocess/updateAxisSettings, + type: 'linear', + behavior: 'raw', + format: '.2f' + //domain: [0, null] + }, + y: { + column: null, // set in onPreprocess/updateAxisSettings, + label: null, // set in onPreprocess/updateAxisSettings, + type: 'linear', + behavior: 'raw', + format: '.2f' + //domain: [0, null] + }, + marks: [ + { + per: [], // set in syncSettings() + type: 'circle', + summarizeY: 'mean', + summarizeX: 'mean', + attributes: { 'fill-opacity': 0 } + } + ], + gridlines: 'xy', + color_by: null, //set in syncSettings + max_width: 600, + aspect: 1, + legend: { location: 'top' }, + margin: { right: 25, top: 25, bottom: 75 } + }; + } + + //Replicate settings in multiple places in the settings object + function syncSettings(settings) { + settings.marks[0].per[0] = settings.id_col; + + //set grouping config + if (typeof settings.group_cols == 'string') { + settings.group_cols = [{ value_col: settings.group_cols, label: settings.group_cols }]; + } + + if (!(settings.group_cols instanceof Array && settings.group_cols.length)) { + settings.group_cols = [{ value_col: 'NONE', label: 'None' }]; + } else { + settings.group_cols = settings.group_cols.map(function(group) { + return { + value_col: group.value_col || group, + label: group.label || group.value_col || group + }; + }); + + var hasNone = + settings.group_cols + .map(function(m) { + return m.value_col; + }) + .indexOf('NONE') > -1; + if (!hasNone) { + settings.group_cols.unshift({ value_col: 'NONE', label: 'None' }); + } + } + + if (settings.group_cols.length > 1) { + settings.color_by = settings.group_cols[1].value_col + ? settings.group_cols[1].value_col + : settings.group_cols[1]; + } else { + settings.color_by = 'NONE'; + } + + //make sure filters is an Array + if (!(settings.filters instanceof Array)) { + settings.filters = typeof settings.filters == 'string' ? [settings.filters] : []; + } + + //Define default details. + var defaultDetails = [{ value_col: settings.id_col, label: 'Subject Identifier' }]; + if (settings.filters) { + settings.filters.forEach(function(filter) { + var obj = { + value_col: filter.value_col ? filter.value_col : filter, + label: filter.label + ? filter.label + : filter.value_col + ? filter.value_col + : filter + }; + + if ( + defaultDetails.find(function(f) { + return f.value_col == obj.value_col; + }) == undefined + ) { + defaultDetails.push(obj); + } + }); + } + + if (settings.group_cols) { + settings.group_cols + .filter(function(f) { + return f.value_col != 'NONE'; + }) + .forEach(function(group) { + var obj = { + value_col: group.value_col ? group.value_col : filter, + label: group.label + ? group.label + : group.value_col + ? group.value_col + : filter + }; + if ( + defaultDetails.find(function(f) { + return f.value_col == obj.value_col; + }) == undefined + ) { + defaultDetails.push(obj); + } + }); + } + + //parse details to array if needed + if (!(settings.details instanceof Array)) { + settings.details = typeof settings.details == 'string' ? [settings.details] : []; + } + + //If [settings.details] is not specified: + if (!settings.details) settings.details = defaultDetails; + else { + //If [settings.details] is specified: + //Allow user to specify an array of columns or an array of objects with a column property + //and optionally a column label. + settings.details.forEach(function(detail) { + if ( + defaultDetails + .map(function(d) { + return d.value_col; + }) + .indexOf(detail.value_col ? detail.value_col : detail) === -1 + ) + defaultDetails.push({ + value_col: detail.value_col ? detail.value_col : detail, + label: detail.label + ? detail.label + : detail.value_col + ? detail.value_col + : detail + }); + }); + settings.details = defaultDetails; + } + + // If settings.analysisFlag is null + if (!settings.analysisFlag) settings.analysisFlag = { value_col: null, values: [] }; + if (!settings.analysisFlag.value_col) settings.analysisFlag.value_col = null; + if (!(settings.analysisFlag.values instanceof Array)) { + settings.analysisFlag.values = + typeof settings.analysisFlag.values == 'string' + ? [settings.analysisFlag.values] + : []; + } + //if it is null, set settings.baseline.value_col to settings.studyday_col. + if (!settings.baseline) settings.baseline = { value_col: null, values: [] }; + if (!settings.baseline.value_col) settings.baseline.value_col = settings.studyday_col; + if (!(settings.baseline.values instanceof Array)) { + settings.baseline.values = + typeof settings.baseline.values == 'string' ? [settings.baseline.values] : []; + } + + //parse x_ and y_options to array if needed + if (!(settings.x_options instanceof Array)) { + settings.x_options = typeof settings.x_options == 'string' ? [settings.x_options] : []; + } + + if (!(settings.y_options instanceof Array)) { + settings.y_options = typeof settings.y_options == 'string' ? [settings.y_options] : []; + } + + // track initial Cutpoint (lets us detect when cutpoint should change) + settings.cuts.x = settings.x.column; + settings.cuts.y = settings.y.column; + settings.cuts.display = settings.display; + + //Attach measure columns to axis settings. + settings.x.column = settings.x_options[0]; + settings.y.column = settings.y_options[0]; + + return settings; + } + + function controlInputs() { + return [ + { + type: 'number', + label: 'R Ratio Range', + description: 'Filter points based on R ratio [(ALT/ULN) / (ALP/ULN)]', + option: 'r_ratio[0]' + }, + { + type: 'number', + label: null, //combined with r_ratio[0] control in formatRRatioControl() + description: null, + option: 'r_ratio[1]' + }, + { + type: 'dropdown', + label: 'Group', + description: 'Grouping variable', + options: ['color_by'], + start: null, // set in syncControlInputs() + values: ['NONE'], // set in syncControlInputs() + require: true + }, + { + type: 'dropdown', + label: 'Display Type', + description: 'Relative or absolute axes', + options: ['displayLabel'], + start: null, // set in syncControlInputs() + values: null, // set in syncControlInputs() + require: true + }, + { + type: 'dropdown', + label: 'X-axis Measure', + description: null, // set in syncControlInputs() + option: 'x.column', + start: null, // set in syncControlInputs() + values: null, //set in syncControlInptus() + require: true + }, + { + type: 'number', + label: null, // set in syncControlInputs + description: 'X-axis Reference Line', + option: null // set in syncControlInputs + }, + { + type: 'dropdown', + label: 'Y-axis Measure', + description: null, // set in syncControlInputs() + option: 'y.column', + start: null, // set in syncControlInputs() + values: null, //set in syncControlInptus() + require: true + }, + { + type: 'number', + label: null, // set in syncControlInputs + description: 'Y-axis Reference Line', + option: null // set in syncControlInputs + }, + { + type: 'dropdown', + label: 'Point Size', + description: 'Parameter to set point radius', + options: ['point_size'], + start: null, // set in syncControlInputs() + values: ['Uniform'], + require: true + }, + { + type: 'dropdown', + label: 'Axis Type', + description: 'Linear or Log Axes', + options: ['x.type', 'y.type'], + start: null, // set in syncControlInputs() + values: ['linear', 'log'], + require: true + }, + { + type: 'number', + label: 'Highlight Points Based on Timing', + description: 'Fill points with max values less than X days apart', + option: 'visit_window' + } + ]; + } + + //Map values from settings to control inputs + function syncControlInputs(controlInputs, settings) { + //////////////////////// + // Group control + /////////////////////// + + var groupControl = controlInputs.find(function(controlInput) { + return controlInput.label === 'Group'; + }); + + //sync start value + groupControl.start = settings.color_by; //sync start value + + //sync values + settings.group_cols + .filter(function(group) { + return group.value_col !== 'NONE'; + }) + .forEach(function(group) { + groupControl.values.push(group.value_col); + }); + + //drop the group control if NONE is the only option + if (settings.group_cols.length == 1) + controlInputs = controlInputs.filter(function(controlInput) { + return controlInput.label != 'Group'; + }); + + ////////////////////////// + // x-axis measure control + ////////////////////////// + + // drop the control if there's only one option + if (settings.x_options.length === 1) + controlInputs = controlInputs.filter(function(controlInput) { + return controlInput.option !== 'x.column'; + }); + else { + //otherwise sync the properties + var xAxisMeasureControl = controlInputs.find(function(controlInput) { + return controlInput.option === 'x.column'; + }); + + xAxisMeasureControl.description = settings.x_options.join(', '); + xAxisMeasureControl.start = settings.x_options[0]; + xAxisMeasureControl.values = settings.x_options; + } + + ////////////////////////////////// + // x-axis reference line control + ////////////////////////////////// + + var xRefControl = controlInputs.find(function(controlInput) { + return controlInput.description === 'X-axis Reference Line'; + }); + xRefControl.label = settings.x_options[0] + ' Cutpoint'; + xRefControl.option = 'settings.cuts.' + [settings.x.column] + '.' + [settings.display]; + + //////////////////////////// + // y-axis measure control + //////////////////////////// + + // drop the control if there's only one option + if (settings.y_options.length === 1) + controlInputs = controlInputs.filter(function(controlInput) { + return controlInput.option !== 'y.column'; + }); + else { + //otherwise sync the properties + var yAxisMeasureControl = controlInputs.find(function(controlInput) { + return controlInput.option === 'y.column'; + }); + yAxisMeasureControl.description = settings.y_options.join(', '); + yAxisMeasureControl.start = settings.y_options[0]; + yAxisMeasureControl.values = settings.y_options; + } + + ////////////////////////////////// + // y-axis reference line control + ////////////////////////////////// + + var yRefControl = controlInputs.find(function(controlInput) { + return controlInput.description === 'Y-axis Reference Line'; + }); + yRefControl.label = settings.y_options[0] + ' Cutpoint'; + + yRefControl.option = 'settings.cuts.' + [settings.y.column] + '.' + [settings.display]; + + ////////////////////////////////// + // R ratio filter control + ////////////////////////////////// + + //drop the R Ratio control if r_ratio_filter is false + if (!settings.r_ratio_filter) { + controlInputs = controlInputs.filter(function(controlInput) { + return ['r_ratio[0]', 'r_ratio[1]'].indexOf(controlInput.option) == -1; + }); + } + + ////////////////////////////////// + // Point size control + ////////////////////////////////// + + var pointSizeControl = controlInputs.find(function(ci) { + return ci.label === 'Point Size'; + }); + + pointSizeControl.start = settings.point_size || 'Uniform'; + + settings.point_size_options.forEach(function(d) { + pointSizeControl.values.push(d); + }); + + //drop the pointSize control if NONE is the only option + if (settings.point_size_options.length == 0) + controlInputs = controlInputs.filter(function(controlInput) { + return controlInput.label != 'Point Size'; + }); + + ////////////////////////////////// + // Display control + ////////////////////////////////// + + controlInputs.find(function(controlInput) { + return controlInput.label === 'Display Type'; + }).values = settings.display_options.map(function(m) { + return m.label; + }); + + ////////////////////////////////// + // Add filters to inputs + ////////////////////////////////// + if (settings.filters && settings.filters.length > 0) { + var otherFilters = settings.filters.map(function(filter) { + filter = { + type: 'subsetter', + value_col: filter.value_col ? filter.value_col : filter, + label: filter.label + ? filter.label + : filter.value_col + ? filter.value_col + : filter, + multiple: settings.filters_multiselect + }; + return filter; + }); + return d3.merge([otherFilters, controlInputs]); + } else return controlInputs; + } + + var configuration = { + settings: settings, + syncSettings: syncSettings, + controlInputs: controlInputs, + syncControlInputs: syncControlInputs + }; + + function checkMeasureDetails() { + var config = this.config; + var measures = d3 + .set( + this.raw_data.map(function(d) { + return d[config.measure_col]; + }) + ) + .values() + .sort(); + var specifiedMeasures = Object.keys(config.measure_values).map(function(e) { + return config.measure_values[e]; + }); + var missingMeasures = []; + Object.keys(config.measure_values).forEach(function(d) { + if (measures.indexOf(config.measure_values[d]) == -1) { + missingMeasures.push(config.measure_values[d]); + delete config.measure_values[d]; + } + }); + var nMeasuresRemoved = missingMeasures.length; + if (nMeasuresRemoved > 0) + console.warn( + 'The data are missing ' + + (nMeasuresRemoved === 1 ? 'this measure' : 'these measures') + + ': ' + + missingMeasures.join(', ') + + '.' + ); + + //check that x_options, y_options and size_options all have value keys/values in measure_values + var valid_options = Object.keys(config.measure_values); + var all_options = ['x_options', 'y_options', 'point_size_options']; + all_options.forEach(function(options) { + config[options].forEach(function(option) { + if (valid_options.indexOf(option) == -1) { + delete config[options][option]; + console.warn( + option + + " wasn't found in the measure_values index and has been removed from config." + + options + + '. This may cause problems with the chart.' + ); + } + }); + }); + } + + function iterateOverData() { + var _this = this; + + this.raw_data.forEach(function(d) { + d[_this.config.x.column] = null; // placeholder variable for x-axis + d[_this.config.y.column] = null; // placeholder variable for y-axis + d.NONE = 'All Participants'; // placeholder variable for non-grouped comparisons + + //Remove space characters from result variable. + if (typeof d[_this.config.value_col] == 'string') + d[_this.config.value_col] = d[_this.config.value_col].replace(/\s/g, ''); // remove space characters + }); + } + + function addRRatioFilter() { + if (this.config.r_ratio_filter) { + this.filters.push({ + col: 'rRatioFlag', + val: 'Y', + choices: ['Y', 'N'], + loose: undefined + }); + } + } + + function imputeColumn(data, measure_column, value_column, measure, llod, imputed_value, drop) { + //returns a data set with imputed values (or drops records) for records at or below a lower threshold for a given measure + //data = the data set for imputation + //measure_column = the column with the text measure names + //value_column = the column with the numeric values to be changed via imputation + //measure = the measure to be imputed + //llod = the lower limit of detection - values at or below the llod are imputed + //imputed_value = value for imputed records + //drop = boolean flag indicating whether values at or below the llod should be dropped (default = false) + + if (drop == undefined) drop = false; + if (drop) { + return data.filter(function(f) { + dropFlag = d[measure_column] == measure && +d[value_column] <= 0; + return !dropFlag; + }); + } else { + data.forEach(function(d) { + if ( + d[measure_column] == measure && + +d[value_column] < +llod && + d[value_column] >= 0 + ) { + d.impute_flag = true; + d[value_column + '_original'] = d[value_column]; + d[value_column] = imputed_value; + } + }); + + var impute_count = d3.sum( + data.filter(function(d) { + return d[measure_column] === measure; + }), + function(f) { + return f.impute_flag; + } + ); + + if (impute_count > 0) + console.warn( + '' + + impute_count + + ' value(s) less than ' + + llod + + ' were imputed to ' + + imputed_value + + ' for ' + + measure + ); + return data; + } + } + + function imputeData() { + var chart = this; + var config = this.config; + + Object.keys(config.measure_values).forEach(function(measureKey) { + var values = chart.imputed_data + .filter(function(f) { + return f[config.measure_col] == config.measure_values[measureKey]; + }) + .map(function(m) { + return +m[config.value_col]; + }) + .sort(function(a, b) { + return a - b; + }), + minValue = d3.min( + values.filter(function(f) { + return f > 0; + }) + ), + //minimum value > 0 + llod = null, + imputed_value = null, + drop = null; + + if (config.imputation_methods[measureKey] == 'data-driven') { + llod = minValue; + imputed_value = minValue / 2; + drop = false; + } else if (config.imputation_methods[measureKey] == 'user-defined') { + llod = config.imputation_values[measureKey]; + imputed_value = config.imputation_values[measureKey] / 2; + drop = false; + } else if (config.imputation_methods[measureKey] == 'drop') { + llod = null; + imputed_value = null; + drop = true; + } + chart.imputed_data = imputeColumn( + chart.imputed_data, + config.measure_col, + config.value_col, + config.measure_values[measureKey], + llod, + imputed_value, + drop + ); + + var total_imputed = d3.sum(chart.raw_data, function(f) { + return f.impute_flag ? 1 : 0; + }); + }); + } + + function dropRows() { + var chart = this; + var config = this.config; + this.dropped_rows = []; + + ///////////////////////// + // Remove invalid rows + ///////////////////////// + var numerics = ['value_col', 'studyday_col', 'normal_col_high']; + chart.imputed_data = chart.initial_data.filter(function(f) { + return true; + }); + numerics.forEach(function(setting) { + chart.imputed_data = chart.imputed_data.filter(function(d) { + //Remove non-numeric value_col + var numericCol = /^-?(\d*\.?\d+|\d+\.?\d*)(E-?\d+)?$/.test(d[config[setting]]); + if (!numericCol) { + d.dropReason = setting + ' Column ("' + config[setting] + '") is not numeric.'; + chart.dropped_rows.push(d); + } + return numericCol; + }); + }); + } + + function deriveVariables() { + var config = this.config; + + //filter the lab data to only the required measures + var included_measures = Object.keys(config.measure_values).map(function(e) { + return config.measure_values[e]; + }); + + var sub = this.imputed_data.filter(function(f) { + return included_measures.indexOf(f[config.measure_col]) > -1; + }); + + var missingBaseline = 0; + + //coerce numeric values to number + this.imputed_data = this.imputed_data.map(function(d) { + var numerics = ['value_col', 'studyday_col', 'normal_col_low', 'normal_col_high']; + numerics.forEach(function(col) { + d[config[col]] = parseFloat(d[config[col]]); + }); + return d; + }); + + //create an object mapping baseline values for id/measure pairs + var baseline_records = sub.filter(function(f) { + var current = + typeof f[config.baseline.value_col] == 'string' + ? f[config.baseline.value_col].trim() + : parseFloat(f[config.baseline.value_col]); + return config.baseline.values.indexOf(current) > -1; + }); + + var baseline_values = d3 + .nest() + .key(function(d) { + return d[config.id_col]; + }) + .key(function(d) { + return d[config.measure_col]; + }) + .rollup(function(d) { + return d[0][config.value_col]; + }) + .map(baseline_records); + + this.imputed_data = this.imputed_data.map(function(d) { + //standardize key variables + d.key_measure = false; + if (included_measures.indexOf(d[config.measure_col]) > -1) { + d.key_measure = true; + + //map the raw value to a variable called 'absolute' + d.absolute = d[config.value_col]; + + //get the value relative to the ULN (% of the upper limit of normal) for the measure + d.uln = d[config.normal_col_high]; + d.relative_uln = d[config.value_col] / d[config.normal_col_high]; + + //get value relative to baseline + if (baseline_values[d[config.id_col]]) { + if (baseline_values[d[config.id_col]][d[config.measure_col]]) { + d.baseline_absolute = + baseline_values[d[config.id_col]][d[config.measure_col]]; + d.relative_baseline = d.absolute / d.baseline_absolute; + } else { + missingBaseline = missingBaseline + 1; + d.baseline_absolute = null; + d.relative_baseline = null; + } + } else { + missingBaseline = missingBaseline + 1; + d.baseline_absolute = null; + d.relative_baseline = null; + } + } + return d; + }); + + if (missingBaseline > 0) + console.warn( + 'No baseline value found for ' + missingBaseline + ' of ' + sub.length + ' records.' + ); + } + + function makeAnalysisFlag() { + var config = this.config; + this.imputed_data = this.imputed_data.map(function(d) { + var hasAnalysisSetting = + config.analysisFlag.value_col != null && config.analysisFlag.values.length > 0; + d.analysisFlag = hasAnalysisSetting + ? config.analysisFlag.values.indexOf(d[config.analysisFlag.value_col]) > -1 + : true; + return d; + }); + } + + function cleanData() { + var config = this.config; + + //drop rows with invalid data + this.imputedData = dropRows.call(this); + + this.imputed_data.forEach(function(d) { + d.impute_flag = false; + }); + + imputeData.call(this); + deriveVariables.call(this); + makeAnalysisFlag.call(this); + } + + function onInit() { + checkMeasureDetails.call(this); + iterateOverData.call(this); + addRRatioFilter.call(this); + cleanData.call(this); //clean visit-level data - imputation and variable derivations + } + + function formatRRatioControl() { + var chart = this; + var config = this.config; + if (this.config.r_ratio_filter) { + var min_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[0]'; + }); + var min_r_ratio_input = min_r_ratio.select('input'); + + var max_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[1]'; + }); + var max_r_ratio_input = max_r_ratio.select('input'); + + min_r_ratio_input.attr('id', 'r_ratio_min'); + max_r_ratio_input.attr('id', 'r_ratio_max'); + + //move the max r ratio control next to the min control + min_r_ratio.append('span').text(' - '); + min_r_ratio.append(function() { + return max_r_ratio_input.node(); + }); + + max_r_ratio.remove(); + + //add a reset button + min_r_ratio + .append('button') + .style('padding', '0.2em 0.5em 0.2em 0.4em') + .style('margin-left', '0.5em') + .style('border-radius', '0.4em') + .text('Reset') + .on('click', function() { + config.r_ratio[0] = 0; + min_r_ratio.select('input#r_ratio_min').property('value', config.r_ratio[0]); + config.r_ratio[1] = config.max_r_ratio; + min_r_ratio.select('input#r_ratio_max').property('value', config.r_ratio[1]); + chart.draw(); + }); + } + } + + function updateSummaryTable() { + var chart = this; + var config = chart.config; + var quadrants = this.config.quadrants; + var rows = quadrants.table.rows; + var cells = quadrants.table.cells; + + function updateCells(d) { + var cellData = cells.map(function(cell) { + cell.value = d[cell.value_col]; + return cell; + }); + var row_cells = d3 + .select(this) + .selectAll('td') + .data(cellData, function(d) { + return d.value_col; + }); + + row_cells + .enter() + .append('td') + .style('text-align', function(d, i) { + return d.label != 'Quadrant' ? 'center' : null; + }) + .style('font-size', '0.9em') + .style('padding', '0 0.5em 0 0.5em'); + + row_cells.html(function(d) { + return d.value; + }); + } + + //update the content of each row + rows.data(quadrants, function(d) { + return d.label; + }); + rows.each(updateCells); + } + + function initSummaryTable() { + var chart = this; + var config = chart.config; + var quadrants = this.config.quadrants; + + quadrants.table = {}; + quadrants.table.wrap = this.wrap + .append('div') + .attr('class', 'quadrantTable') + .style('padding-top', '1em'); + quadrants.table.tab = quadrants.table.wrap + .append('table') + .style('border-collapse', 'collapse'); + + //table header + quadrants.table.cells = [ + { + value_col: 'label', + label: 'Quadrant' + }, + { + value_col: 'count', + label: '#' + }, + { + value_col: 'percent', + label: '%' + } + ]; + + if (config.populationProfileURL) { + quadrants.forEach(function(d) { + d.link = "🔗"; + }); + quadrants.table.cells.push({ + value_col: 'link', + label: 'Population Profile' + }); + } + quadrants.table.thead = quadrants.table.tab + .append('thead') + .style('border-top', '2px solid #999') + .style('border-bottom', '2px solid #999') + .append('tr') + .style('padding', '0.1em'); + + quadrants.table.thead + .selectAll('th') + .data(quadrants.table.cells) + .enter() + .append('th') + .html(function(d) { + return d.label; + }); + + //table contents + quadrants.table.tbody = quadrants.table.tab + .append('tbody') + .style('border-bottom', '2px solid #999'); + quadrants.table.rows = quadrants.table.tbody + .selectAll('tr') + .data(quadrants, function(d) { + return d.label; + }) + .enter() + .append('tr') + .style('padding', '0.1em'); + + //initial table update + updateSummaryTable.call(this); + } + + function init() { + var chart = this; + var config = chart.config; + var quadrants = this.config.quadrants; + + var x_input = chart.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.description == 'X-axis Reference Line'; + }) + .select('input'); + + var y_input = chart.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.description == 'Y-axis Reference Line'; + }) + .select('input'); + + /////////////////////////////////////////////////////////// + // set initial values + ////////////////////////////////////////////////////////// + x_input.node().value = config.cuts[config.x.column][config.display]; + y_input.node().value = config.cuts[config.y.column][config.display]; + + /////////////////////////////////////////////////////////// + // set control step to 0.1 + ////////////////////////////////////////////////////////// + x_input.attr('step', 0.1); + y_input.attr('step', 0.1); + + /////////////////////////////////////////////////////////// + // initialize the summary table + ////////////////////////////////////////////////////////// + initSummaryTable.call(chart); + } + + function layoutQuadrantLabels() { + var chart = this; + var config = chart.config; + var quadrants = this.config.quadrants; + + ////////////////////////////////////////////////////////// + //layout the quadrant labels + ///////////////////////////////////////////////////////// + + chart.quadrant_labels = {}; + chart.quadrant_labels.g = this.svg.append('g').attr('class', 'quadrant-labels'); + + chart.quadrant_labels.text = chart.quadrant_labels.g + .selectAll('text.quadrant-label') + .data(quadrants) + .enter() + .append('text') + .attr('class', function(d) { + return 'quadrant-label ' + d.position; + }) + .attr('dy', function(d) { + return d.position.search('lower') > -1 ? '-.2em' : '.5em'; + }) + .attr('dx', function(d) { + return d.position.search('right') > -1 ? '-.5em' : '.5em'; + }) + .attr('text-anchor', function(d) { + return d.position.search('right') > 0 ? 'end' : null; + }) + .attr('fill', '#bbb') + .text(function(d) { + return d.label; + }); + } + + function layoutCutLines() { + var chart = this; + var config = chart.config; + var quadrants = this.config.quadrants; + + ////////////////////////////////////////////////////////// + //layout the cut lines + ///////////////////////////////////////////////////////// + chart.cut_lines = {}; + chart.cut_lines.wrap = this.svg.append('g').attr('class', 'cut-lines'); + var wrap = chart.cut_lines.wrap; + + //slight hack to make life easier on drag + var cutLineData = [{ dimension: 'x' }, { dimension: 'y' }]; + + cutLineData.forEach(function(d) { + d.chart = chart; + }); + + chart.cut_lines.g = wrap + .selectAll('g.cut') + .data(cutLineData) + .enter() + .append('g') + .attr('class', function(d) { + return 'cut ' + d.dimension; + }); + + chart.cut_lines.lines = chart.cut_lines.g + .append('line') + .attr('class', 'cut-line') + .attr('stroke-dasharray', '5,5') + .attr('stroke', '#bbb'); + + chart.cut_lines.backing = chart.cut_lines.g + .append('line') + .attr('class', 'cut-line-backing') + .attr('stroke', 'transparent') + .attr('stroke-width', '10') + .attr('cursor', 'move'); + } + + function initQuadrants() { + init.call(this); + layoutCutLines.call(this); + layoutQuadrantLabels.call(this); + } + + function initRugs() { + //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover + this.x_rug = this.svg.append('g').attr('class', 'rug x'); + this.y_rug = this.svg.append('g').attr('class', 'rug y'); + } + + function initVisitPath() { + //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover + this.visitPath = this.svg.append('g').attr('class', 'visit-path'); + } + + function initParticipantDetails() { + //layout participant details section + this.participantDetails = {}; + this.participantDetails.wrap = this.wrap.append('div').attr('class', 'participantDetails'); + + this.participantDetails.header = this.participantDetails.wrap + .append('div') + .attr('class', 'participantHeader'); + var splot = this.participantDetails.wrap.append('div').attr('class', 'spaghettiPlot'); + splot + .append('h3') + .attr('class', 'id') + .html('Standardized Lab Values by Visit') + .style('border-top', '2px solid black') + .style('border-bottom', '2px solid black') + .style('padding', '.2em'); + + splot.append('div').attr('class', 'chart'); + + var mtable = this.participantDetails.wrap.append('div').attr('class', 'measureTable'); + mtable + .append('h3') + .attr('class', 'id') + .html('Raw Lab Values Summary Table') + .style('border-top', '2px solid black') + .style('border-bottom', '2px solid black') + .style('padding', '.2em'); + + //initialize the measureTable + var settings = { + cols: ['key', 'n', 'min', 'median', 'max', 'spark'], + headers: ['Measure', 'N', 'Min', 'Median', 'Max', ''], + searchable: false, + sortable: false, + pagination: false, + exportable: false, + applyCSS: true + }; + this.measureTable = webcharts.createTable( + this.element + ' .participantDetails .measureTable', + settings + ); + this.measureTable.init([]); + + //hide the section until needed + this.participantDetails.wrap.selectAll('*').style('display', 'none'); + } + + function initResetButton() { + var chart = this; + + this.controls.reset = {}; + var reset = this.controls.reset; + reset.wrap = this.controls.wrap.append('div').attr('class', 'control-group'); + reset.label = reset.wrap + .append('span') + .attr('class', 'wc-control-label') + .text('Reset Chart'); + reset.button = reset.wrap + .append('button') + .text('Reset') + .on('click', function() { + var initial_container = chart.element; + var initial_settings = chart.initial_settings; + var initial_data = chart.initial_data; + chart.emptyChartWarning.remove(); + + chart.destroy(); + chart = null; + + var newChart = safetyedish(initial_container, initial_settings); + newChart.init(initial_data); + }); + } + + function initDisplayControl() { + var chart = this; + var config = this.config; + var displayControlWrap = this.controls.wrap.selectAll('div').filter(function(controlInput) { + return controlInput.label === 'Display Type'; + }); + + var displayControl = displayControlWrap.select('select'); + + //set the start value + var start_value = config.display_options.find(function(f) { + return f.value == config.display; + }).label; + displayControl.selectAll('option').attr('selected', function(d) { + return d == start_value ? 'selected' : null; + }); + + //annotation of baseline visit (only visible when mDish is selected) + displayControlWrap + .append('span') + .attr('class', 'displayControlAnnotation span-description') + .style('color', 'blue') + .text( + 'Note: Baseline defined as ' + + chart.config.baseline.value_col + + ' = ' + + chart.config.baseline.values.join(',') + ) + .style('display', config.display == 'relative_baseline' ? null : 'none'); + + displayControl.on('change', function(d) { + var currentLabel = this.value; + var currentValue = config.display_options.find(function(f) { + return f.label == currentLabel; + }).value; + config.display = currentValue; + + if (currentValue == 'relative_baseline') { + displayControlWrap.select('span.displayControlAnnotation').style('display', null); + } else { + displayControlWrap.select('span.displayControlAnnotation').style('display', 'none'); + } + + config.cuts.display_change = true; + + chart.draw(); + }); + } + + function layoutPanels() { + this.wrap.style('display', 'inline-block').style('width', '75%'); + + this.controls.wrap + .style('display', 'inline-block') + .style('width', '25%') + .style('vertical-align', 'top'); + + this.controls.wrap.selectAll('div.control-group').style('display', 'block'); + this.controls.wrap + .selectAll('div.control-group') + .select('select') + .style('width', '200px'); + } + + function initTitle() { + this.titleDiv = this.controls.wrap + .insert('div', '*') + .attr('class', 'title') + .style('margin-right', '1em') + .style('margin-bottom', '1em'); + + this.titleDiv + .append('span') + .text(this.config.title) + .style('font-size', '1.5em') + .style('font-weight', 'strong') + .style('display', 'block'); + } + + function add(messageText, type, label, messages, callback) { + var messageObj = { + id: messages.list.length + 1, + type: type, + message: messageText, + label: label, + hidden: false, + callback: callback + }; + messages.list.push(messageObj); + messages.update(messages); + } + + function remove(id, label, messages) { + // hide the the message(s) by id or label + if (id) { + var matches = messages.list.filter(function(f) { + return +f.id == +id; + }); + } else if (label.length) { + var matches = messages.list.filter(function(f) { + return label == 'all' ? true : f.label == label; + }); + } + matches.forEach(function(d) { + d.hidden = true; + }); + messages.update(messages); + } + + function update(messages) { + function jsUcfirst(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + var visibleMessages = messages.list.filter(function(f) { + return f.hidden == false; + }); + + //update title + messages.header.title.text('Messages (' + visibleMessages.length + ')'); + + // + var messageDivs = messages.wrap.selectAll('div.message').data(visibleMessages, function(d) { + return d.id; + }); + + var newMessages = messageDivs + .enter() + .append('div') + .attr('class', function(d) { + return d.type + ' message ' + d.label; + }) + .html(function(d) { + var messageText = '' + jsUcfirst(d.type) + ': ' + d.message; + return messageText.split('.')[0] + '.'; + }) + .style('border-radius', '.5em') + .style('margin-right', '1em') + .style('margin-bottom', '0.5em') + .style('padding', '0.2em') + .style('font-size', '0.9em'); + + newMessages + .append('div.expand') + .html('•••') + .style('background', 'white') + .style('display', 'inline-block') + .style('border', '1px solid #999') + .style('padding', '0 0.2em') + .style('margin-left', '0.3em') + .style('font-size', '0.4em') + .style('border-radius', '0.6em') + .style('cursor', 'pointer') + .on('click', function(d) { + d3.select(this.parentNode) + .html(function(d) { + return '' + jsUcfirst(d.type) + ': ' + d.message; + }) + .each(function(d) { + if (d.callback) { + d.callback.call(this.parentNode); + } + }); + }); + + messageDivs.each(function(d) { + var type = d.type; + var thisMessage = d3.select(this); + if (type == 'caution') { + thisMessage + .style('border', '1px solid #faebcc') + .style('color', '#8a6d3b') + .style('background-color', '#fcf8e3'); + } else if (type == 'warning') { + thisMessage + .style('border', '1px solid #ebccd1') + .style('color', '#a94442') + .style('background-color', '#f2dede'); + } else { + thisMessage + .style('border', '1px solid #999') + .style('color', '#999') + .style('background-color', null); + } + + if (d.callback) { + d.callback.call(this); + } + }); + + messageDivs.exit().remove(); + } + + function init$1() { + var chart = this; + this.messages = { + add: add, + remove: remove, + update: update + }; + // this.messages.add = addMessage; + // this.messages.remove = removeMessage; + this.messages.list = []; + this.messages.wrap = this.controls.wrap.insert('div', '*').style('margin', '0 1em 1em 0'); + this.messages.header = this.messages.wrap + .append('div') + .style('border-top', '1px solid black') + .style('border-bottom', '1px solid black') + .style('font-weight', 'strong') + .style('margin', '0 1em 1em 0'); + + this.messages.header.title = this.messages.header + .append('div') + .attr('class', 'title') + .style('display', 'inline-block') + .text('Messages (0)'); + + this.messages.header.clear = this.messages.header + .append('div') + .text('Clear') + .style('font-size', '0.8em') + .style('vertical-align', 'center') + .style('display', 'inline-block') + .style('float', 'right') + .style('color', 'blue') + .style('cursor', 'pointer') + .style('text-decoration', 'underline') + .on('click', function() { + chart.messages.remove(null, 'all', chart.messages); + }); + } + + function initCustomWarning() { + if (this.config.warningText) { + this.messages.add( + this.config.warningText, + 'caution', + 'validationCaution', + this.messages + ); + } + } + + function downloadCSV(data, cols, file) { + var CSVarray = []; + + //add headers to CSV array + var cols = cols ? cols : Object.keys(data[0]); + var headers = cols.map(function(header) { + return '"' + header.replace(/"/g, '""') + '"'; + }); + CSVarray.push(headers); + //add rows to CSV array + data.forEach(function(d, i) { + var row = cols.map(function(col) { + var value = d[col]; + + if (typeof value === 'string') value = value.replace(/"/g, '""'); + + return '"' + value + '"'; + }); + + CSVarray.push(row); + }); + + //transform blob array into a blob of characters + var blob = new Blob([CSVarray.join('\n')], { + type: 'text/csv;charset=utf-8;' + }); + var fileCore = file ? file : 'eDish'; + var fileName = fileCore + '_' + d3.time.format('%Y-%m-%dT%H-%M-%S')(new Date()) + '.csv'; + var link = d3.select(this); + + if (navigator.msSaveBlob) + //IE + navigator.msSaveBlob(blob, fileName); + else if (link.node().download !== undefined) { + //21st century browsers + var url = URL.createObjectURL(blob); + link.node().setAttribute('href', url); + link.node().setAttribute('download', fileName); + } + } + + function initDroppedRowsWarning() { + var chart = this; + if (this.dropped_rows.length > 0) { + var warningText = + this.dropped_rows.length + + ' rows were removed. This is probably because of non-numeric or missing data provided for key variables. Click here to download a csv with a brief explanation of why each row was removed.'; + + this.messages.add(warningText, 'caution', 'droppedRows', this.messages, function() { + //custom callback to activate the droppedRows download + d3.select(this) + .select('a.rowDownload') + .style('color', 'blue') + .style('text-decoration', 'underline') + .style('cursor', 'pointer') + .datum(chart.dropped_rows) + .on('click', function(d) { + var systemVars = d3.merge([ + ['dropReason', 'NONE'], + Object.keys(chart.config.measure_values) + ]); + var cols = d3.merge([ + ['dropReason'], + Object.keys(d[0]).filter(function(f) { + return systemVars.indexOf(f) == -1; + }) + ]); + downloadCSV.call(this, d, cols, 'eDishDroppedRows'); + }); + }); + } + } + + function initControlLabels() { + var config = this.config; + + //Add settings label + var first_setting = this.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.type != 'subsetter'; + }) + .filter(function(f) { + return f.option != 'r_ratio[0]'; + }) + .filter(function(f, i) { + return i == 0; + }) + .attr('class', 'first-setting'); + + this.controls.setting_header = this.controls.wrap + .insert('div', '.first-setting') + .attr('class', 'subtitle') + .style('border-top', '1px solid black') + .style('border-bottom', '1px solid black') + .style('margin-right', '1em') + .style('margin-bottom', '1em'); + + this.controls.setting_header + .append('span') + .text('Settings') + .style('font-weight', 'strong') + .style('display', 'block'); + + //Add filter label if at least 1 filter exists + if (config.r_ratio_filter || config.filters.length > 0) { + //insert a header before the first filter + var control_wraps = this.controls.wrap + .selectAll('div') + .filter(function(controlInput) { + return ( + controlInput.label === 'R Ratio Range' || controlInput.type === 'subsetter' + ); + }) + .classed('subsetter', true); + + this.controls.filter_header = this.controls.wrap + .insert('div', 'div.subsetter') + .attr('class', 'subtitle') + .style('border-top', '1px solid black') + .style('border-bottom', '1px solid black') + .style('margin-right', '1em') + .style('margin-bottom', '1em'); + this.controls.filter_header + .append('span') + .text('Filters') + .style('font-weight', 'strong') + .style('display', 'block'); + var population = d3 + .set( + this.initial_data.map(function(m) { + return m[config.id_col]; + }) + ) + .values().length; + this.controls.filter_header + .append('span') + .attr('class', 'popCount') + .html( + '' + + population + + ' of ' + + population + + ' participants shown.' + ) + .style('font-size', '0.8em'); + + this.controls.filter_numerator = this.controls.filter_header + .select('span.popCount') + .select('span.numerator'); + this.controls.filter_denominator = this.controls.filter_header + .select('span.popCount') + .select('span.denominator'); + } + } + + function addFootnote() { + this.footnote = this.wrap + .append('div') + .attr('class', 'footnote') + .text('Use controls to update chart or click a point to see participant details.') + .style('font-size', '0.7em') + .style('padding-top', '0.1em'); + this.footnote.timing = this.footnote.append('p'); + } + + function addDownloadButton() { + var chart = this; + var config = this.config; + if (config.downloadLink) { + this.titleDiv + .select('span') + .append('a') + .attr('class', 'downloadRaw') + .html('↓ Raw Data') + .attr('title', 'Download Raw Data') + .style('font-size', '.5em') + .style('margin-left', '1em') + .style('border', '1px solid black') + .style('border-radius', '2px') + .style('padding', '2px 4px') + .style('text-align', 'center') + .style('display', 'inline-block') + .style('cursor', 'pointer') + .style('font-weight', 'bold') + .datum(chart.initial_data) + .on('click', function(d) { + var systemVars = [ + 'dropReason', + 'NONE', + 'ALT', + 'TB', + 'impute_flag', + 'key_measure', + 'analysisFlag' + ]; + var cols = Object.keys(d[0]).filter(function(f) { + return systemVars.indexOf(f) == -1; + }); + downloadCSV.call(this, d, cols, 'eDishRawData'); + }); + } + } + + function initEmptyChartWarning() { + console.log(this); + this.emptyChartWarning = d3 + .select(this.element) + .append('span') + .text('No data selected. Try updating your settings or resetting the chart. ') + .style('display', 'none') + .style('color', '#a94442') + .style('background-color', '#f2dede') + .style('border', '1px solid #ebccd1') + .style('padding', '0.5em') + .style('margin', '0 2% 12px 2%') + .style('border-radius', '0.2em'); + } + + function onLayout() { + layoutPanels.call(this); + + //init messages section + init$1.call(this); + initCustomWarning.call(this); + initDroppedRowsWarning.call(this); + + initTitle.call(this); + addDownloadButton.call(this); + + addFootnote.call(this); + formatRRatioControl.call(this); + initQuadrants.call(this); + initRugs.call(this); + initVisitPath.call(this); + initParticipantDetails.call(this); + initResetButton.call(this); + initDisplayControl.call(this); + initControlLabels.call(this); + initEmptyChartWarning.call(this); + } + + function updateAxisSettings() { + var config = this.config; + var unit = + config.display == 'relative_uln' + ? ' [xULN]' + : config.display == 'relative_baseline' + ? ' [xBaseline]' + : config.display == 'absolute' + ? ' [raw values]' + : null; + + //Update axis labels. + config.x.label = config.measure_values[config.x.column] + unit; + config.y.label = config.measure_values[config.y.column] + unit; + } + + function updateControlCutpointLabels() { + if ( + this.controls.config.inputs.find(function(input) { + return input.description === 'X-axis Reference Line'; + }) + ) + this.controls.wrap + .selectAll('.control-group') + .filter(function(d) { + return d.description === 'X-axis Reference Line'; + }) + .select('.wc-control-label') + .text(this.config.x.column + ' Reference Line'); + if ( + this.controls.config.inputs.find(function(input) { + return input.description === 'Y-axis Reference Line'; + }) + ) + this.controls.wrap + .selectAll('.control-group') + .filter(function(d) { + return d.description === 'Y-axis Reference Line'; + }) + .select('.wc-control-label') + .text(this.config.y.column + ' Reference Line'); + } + + function setMaxRRatio() { + var chart = this; + var config = this.config; + var r_ratio_wrap = chart.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.option === 'r_ratio[0]'; + }); + + //if no max value is defined, use the max value from the data + if (this.config.r_ratio_filter) { + if (!config.r_ratio[1]) { + var raw_max_r_ratio = d3.max(this.raw_data, function(d) { + return d.rRatio; + }); + config.max_r_ratio = Math.ceil(raw_max_r_ratio * 10) / 10; //round up to the nearest 0.1 + config.r_ratio[1] = config.max_r_ratio; + chart.controls.wrap + .selectAll('.control-group') + .filter(function(d) { + return d.option === 'r_ratio[0]'; + }) + .select('input#r_ratio_max') + .property('value', config.max_r_ratio); + } + + //make sure r_ratio[0] <= r_ratio[1] + if (config.r_ratio[0] > config.r_ratio[1]) { + config.r_ratio = config.r_ratio.reverse(); + r_ratio_wrap.select('input#r_ratio_min').property('value', config.r_ratio[0]); + r_ratio_wrap.select('input#r_ratio_max').property('value', config.r_ratio[1]); + } + + //Define flag given r-ratio minimum. + this.raw_data.forEach(function(participant_obj) { + var aboveMin = participant_obj.rRatio >= config.r_ratio[0]; + var belowMax = participant_obj.rRatio <= config.r_ratio[1]; + participant_obj.rRatioFlag = aboveMin & belowMax ? 'Y' : 'N'; + }); + } + } + + function addParticipantLevelMetadata(d, participant_obj) { + var varList = []; + if (this.config.filters) { + var filterVars = this.config.filters.map(function(d) { + return d.hasOwnProperty('value_col') ? d.value_col : d; + }); + varList = d3.merge([varList, filterVars]); + } + if (this.config.group_cols) { + var groupVars = this.config.group_cols.map(function(d) { + return d.hasOwnProperty('value_col') ? d.value_col : d; + }); + varList = d3.merge([varList, groupVars]); + } + if (this.config.details) { + var detailVars = this.config.details.map(function(d) { + return d.hasOwnProperty('value_col') ? d.value_col : d; + }); + varList = d3.merge([varList, detailVars]); + } + + varList.forEach(function(v) { + participant_obj[v] = '' + d[0][v]; + }); + } + + function calculateRRatios(d, participant_obj) { + if (this.config.r_ratio_filter) { + //R-ratio should be the ratio of ALT to ALP, i.e. the x-axis to the z-axis. + participant_obj.rRatio = + participant_obj['ALT_relative_uln'] / participant_obj['ALP_relative_uln']; + } + } + + //Converts a one record per measure data object to a one record per participant objects + function flattenData() { + var chart = this; + var config = this.config; + + //make a data set with one row per ID + + //get list of columns to flatten + var colList = []; + var measureCols = [ + 'measure_col', + 'value_col', + 'studyday_col', + 'normal_col_low', + 'normal_col_high' + ]; + + measureCols.forEach(function(d) { + if (Array.isArray(d)) { + d.forEach(function(di) { + colList.push( + di.hasOwnProperty('value_col') ? config[di.value_col] : config[di] + ); + }); + } else { + colList.push(d.hasOwnProperty('value_col') ? config[d.value_col] : config[d]); + } + }); + + //merge in the absolute and relative values + colList = d3.merge([ + colList, + ['absolute', 'relative_uln', 'relative_baseline', 'baseline_absolute', 'analysisFlag'] + ]); + + //get maximum values for each measure type + var flat_data = d3 + .nest() + .key(function(f) { + return f[config.id_col]; + }) + .rollup(function(d) { + var participant_obj = {}; + participant_obj.days_x = null; + participant_obj.days_y = null; + Object.keys(config.measure_values).forEach(function(mKey) { + //get all raw data for the current measure + var matches = d + .filter(function(f) { + return config.measure_values[mKey] == f[config.measure_col]; + }) //get matching measures + .filter(function(f) { + return f.analysisFlag; + }); + + if (matches.length == 0) { + if (config.debug) { + console.warn( + 'No analysis records found for ' + + d[0][config.id_col] + + ' for ' + + mKey + ); + } + + participant_obj.drop_participant = true; + participant_obj.drop_reason = + 'No analysis results found for 1+ key measure, including ' + mKey + '.'; + return participant_obj; + } else { + participant_obj.drop_participant = false; + } + + //get record with maximum value for the current display type + participant_obj[mKey] = d3.max(matches, function(d) { + return +d[config.display]; + }); + + var maxRecord = matches.find(function(d) { + return participant_obj[mKey] == +d[config.display]; + }); + //map all measure specific values + colList.forEach(function(col) { + participant_obj[mKey + '_' + col] = maxRecord[col]; + }); + + //determine whether the value is above the specified threshold + if (config.cuts[mKey][config.display]) { + config.show_quadrants = true; + participant_obj[mKey + '_cut'] = config.cuts[mKey][config.display]; + participant_obj[mKey + '_flagged'] = + participant_obj[mKey] >= participant_obj[mKey + '_cut']; + } else { + config.show_quadrants = false; + participant_obj[mKey + '_cut'] = null; + participant_obj[mKey + '_flagged'] = null; + } + + //save study days for each axis; + if (mKey == config.x.column) + participant_obj.days_x = maxRecord[config.studyday_col]; + if (mKey == config.y.column) + participant_obj.days_y = maxRecord[config.studyday_col]; + }); + + //Add participant level metadata + addParticipantLevelMetadata.call(chart, d, participant_obj); + + //Calculate ratios between measures. + calculateRRatios.call(chart, d, participant_obj); + + //calculate the day difference between x and y + participant_obj.day_diff = Math.abs( + participant_obj.days_x - participant_obj.days_y + ); + + return participant_obj; + }) + .entries( + this.imputed_data.filter(function(f) { + return f.key_measure; + }) + ); + + chart.dropped_participants = flat_data + .filter(function(f) { + return f.values.drop_participant; + }) + .map(function(d) { + return { + id: d.key, + drop_reason: d.values.drop_reason, + allrecords: chart.initial_data.filter(function(f) { + return f[config.id_col] == d.key; + }) + }; + }); + var flat_data = flat_data + .filter(function(f) { + return !f.values.drop_participant; + }) + .map(function(m) { + m.values[config.id_col] = m.key; + + //link the raw data to the flattened object + var allMatches = chart.imputed_data.filter(function(f) { + return f[config.id_col] == m.key; + }); + m.values.raw = allMatches; + + return m.values; + }); + return flat_data; + } + + function setLegendLabel() { + //change the legend label to match the group variable + //or hide legend if group = NONE + this.config.legend.label = + this.config.color_by !== 'NONE' + ? this.config.group_cols[ + this.config.group_cols + .map(function(group) { + return group.value_col; + }) + .indexOf(this.config.color_by) + ].label + : ''; + } + + function showMissingDataWarning() { + var chart = this; + var config = chart.config; + + if (config.debug) { + //confirm participants are only dropped once (?!) + var unique_dropped_participants = d3 + .set( + this.dropped_participants.map(function(m) { + return m.id; + }) + ) + .values().length; + console.log( + 'Of ' + + this.dropped_participants.length + + ' dropped participants, ' + + unique_dropped_participants + + ' are unique.' + ); + console.log(this.dropped_participants); + } + + chart.messages.remove(null, 'droppedPts', chart.messages); //remove message from previous render + if (this.dropped_participants.length > 0) { + var warningText = + this.dropped_participants.length + + ' participants are not plotted. They likely have invalid or missing data for key variables in the current chart. Click here to download a csv with a brief explanation of why each participant was not plotted.'; + + this.messages.add(warningText, 'caution', 'droppedPts', this.messages, function() { + //custom callback to activate the droppedRows download + d3.select(this) + .select('a.ptDownload') + .style('color', 'blue') + .style('text-decoration', 'underline') + .style('cursor', 'pointer') + .datum(chart.dropped_participants) + .on('click', function(d) { + var cols = ['id', 'drop_reason']; + downloadCSV.call(this, d, cols, 'eDishDroppedParticipants'); + }); + }); + } + } + + function dropMissingValues() { + var chart = this; + var config = this.config; + //drop records with missing or invalid (negative) values + var missing_count = d3.sum(this.raw_data, function(f) { + return f[config.x.column] <= 0 || f[config.y.column] <= 0; + }); + + if (missing_count > 0) { + this.raw_data = this.raw_data.map(function(d) { + d.nonPositiveFlag = d[config.x.column] <= 0 || d[config.y.column] <= 0; + var type = config.display == 'relative_uln' ? 'eDish' : 'mDish'; + // generate an informative reason the participant was dropped + var dropText = + type + + ' values could not be generated for ' + + config.x.column + + ' or ' + + config.y.column + + '. '; + + // x type is mdish and baseline is missing + if ((type == 'mDish') & !d[config.x.column + '_baseline_absolute']) { + dropText = dropText + 'Baseline for ' + config.x.column + ' is missing. '; + } + + // y type is mdish and baseline is missing + if ((type == 'mDish') & !d[config.y.column + '_baseline_absolute']) { + dropText = dropText + 'Baseline for ' + config.y.column + ' is missing. '; + } + + d.drop_reason = d.nonPositiveFlag ? dropText : ''; + return d; + }); + + this.dropped_participants = d3.merge([ + this.dropped_participants, + this.raw_data + .filter(function(f) { + return f.nonPositiveFlag; + }) + .map(function(m) { + return { id: m[config.id_col], drop_reason: m.drop_reason }; + }) + ]); + + this.dropped_participants.map(function(m) { + m.raw = chart.initial_data.filter(function(f) { + return f[config.id_col] == m.id; + }); + }); + } + + this.raw_data = this.raw_data.filter(function(f) { + return !f.nonPositiveFlag; + }); + showMissingDataWarning.call(this); + } + + function onPreprocess() { + updateAxisSettings.call(this); //update axis label based on display type + updateControlCutpointLabels.call(this); //update cutpoint control labels given x- and y-axis variables + this.raw_data = flattenData.call(this); //convert from visit-level data to participant-level data + setMaxRRatio.call(this); + setLegendLabel.call(this); //update legend label based on group variable + dropMissingValues.call(this); + } + + function onDataTransform() {} + + function updateQuadrantData() { + var chart = this; + var config = this.config; + + //add "eDISH_quadrant" column to raw_data + var x_var = this.config.x.column; + var y_var = this.config.y.column; + + var x_cut = this.config.cuts[x_var][config.display]; + var y_cut = this.config.cuts[y_var][config.display]; + + this.filtered_data.forEach(function(d) { + var x_cat = d[x_var] >= x_cut ? 'xHigh' : 'xNormal'; + var y_cat = d[y_var] >= y_cut ? 'yHigh' : 'yNormal'; + d['eDISH_quadrant'] = x_cat + ':' + y_cat; + }); + + //update Quadrant data + config.quadrants.forEach(function(quad) { + quad.count = chart.filtered_data.filter(function(d) { + return d.eDISH_quadrant == quad.dataValue; + }).length; + quad.total = chart.filtered_data.length; + quad.percent = d3.format('0.1%')(quad.count / quad.total); + }); + } + + function setDomain(dimension) { + var config = this.config; + var domain = this[dimension].domain(); + var measure = config[dimension].column; + var cut = config.cuts[measure][config.display]; + + //make sure the domain contains the cut point + if (cut * 1.01 >= domain[1]) { + domain[1] = cut * 1.01; + } + + // make sure the domain lower limit captures all of the raw Values + if (this.config[dimension].type == 'linear') { + // just use the lower limit of 0 for continuous + domain[0] = 0; + } else if (this.config[dimension].type == 'log') { + // use the smallest raw value for a log axis + var measure = config.measure_values[config[dimension].column]; + var values = this.imputed_data + .filter(function(f) { + return f[config.measure_col] == measure; + }) + .map(function(m) { + return +m[config.display]; + }) + .filter(function(m) { + return m > 0; + }) + .sort(function(a, b) { + return a - b; + }); + var minValue = d3.min(values); + + if (minValue < domain[0]) { + domain[0] = minValue; + } + + //throw a warning if the domain is > 0 if using log scale + if (this[dimension].type == 'log' && domain[0] <= 0) { + console.warn( + "Can't draw a log " + dimension + '-axis because there are values <= 0.' + ); + } + } + this[dimension + '_dom'] = domain; + } + + function clearVisitPath() { + this.visitPath.selectAll('*').remove(); + } + + function clearParticipantHeader() { + this.participantDetails.header.selectAll('*').remove(); //clear participant header + } + + function hideMeasureTable() { + this.measureTable.draw([]); + this.measureTable.wrap.selectAll('*').style('display', 'none'); + } + + function clearRugs(axis) { + this[axis + '_rug'].selectAll('*').remove(); + } + + function formatPoints() { + var chart = this; + var config = this.config; + var points = this.svg.selectAll('g.point').select('circle'); + + points + .attr('stroke', function(d) { + var disabled = d3.select(this).classed('disabled'); + var raw = d.values.raw[0], + pointColor = chart.colorScale(raw[config.color_by]); + return disabled ? '#ccc' : pointColor; + }) + .attr('fill', function(d) { + var disabled = d3.select(this).classed('disabled'); + var raw = d.values.raw[0], + pointColor = chart.colorScale(raw[config.color_by]); + return disabled ? 'white' : pointColor; + }) + .attr('stroke-width', 1) + .style('clip-path', null); + } + + function clearParticipantDetails() { + var config = this.config; + var points = this.svg.selectAll('g.point').select('circle'); + + points.classed('disabled', false); + this.config.quadrants.table.wrap.style('display', null); + clearVisitPath.call(this); //remove path + clearParticipantHeader.call(this); + clearRugs.call(this, 'x'); //clear rugs + clearRugs.call(this, 'y'); + hideMeasureTable.call(this); //remove the detail table + formatPoints.call(this); + this.participantDetails.wrap.selectAll('*').style('display', 'none'); + } + + function updateFilterLabel() { + if (this.controls.filter_numerator) { + this.controls.filter_numerator.text(this.filtered_data.length); + } + } + + function setCutpointMinimums() { + var chart = this; + var config = this.config; + var lower_limits = { + x: chart['x_dom'][0], + y: chart['y_dom'][0] + }; + + //Make sure cutpoint isn't below lower domain - Comes in to play when changing from log to linear axes + Object.keys(lower_limits).forEach(function(dimension) { + var measure = config[dimension].column; + var current_cut = config.cuts[measure][config.display]; + var min = lower_limits[dimension]; + if (current_cut < min) { + config.cuts[measure][config.display] = min; + chart.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.description + ? f.description.toLowerCase() == dimension + '-axis reference line' + : false; + }) + .select('input') + .node().value = min; + } + }); + + //Update cut point controls + var controlWraps = this.controls.wrap + .selectAll('.control-group') + .filter(function(d) { + return /.-axis Reference Line/i.test(d.description); + }) + .attr('min', function(d) { + return lower_limits[d.description.split('-')[0]]; + }); + + controlWraps.select('input').on('change', function(d) { + var dimension = d.description.split('-')[0].toLowerCase(); + var min = chart[dimension + '_dom'][0]; + var input = d3.select(this); + + //Prevent a cutpoint less than the lower domain. + if (input.property('value') < min) input.property('value', min); + + //Update chart setting. + var measure = config[dimension].column; + config.cuts[measure][config.display] = input.property('value'); + chart.draw(); + }); + } + + function syncCutpoints() { + var chart = this; + var config = this.config; + + //check to see if the cutpoint used is current + if ( + config.cuts.x != config.x.column || + config.cuts.y != config.y.column || + config.cuts.display != config.display + ) { + // if not, update it! + + // track the current cut point variables + config.cuts.x = config.x.column; + config.cuts.y = config.y.column; + config.cuts.display = config.display; + + // update the cutpoint shown in the control + config.cuts.display_change = false; //reset the change flag; + var dimensions = ['x', 'y']; + dimensions.forEach(function(dimension) { + //change the control to point at the correct cut point + var dimInput = chart.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.description + ? f.description.toLowerCase() == dimension + '-axis reference line' + : false; + }) + .select('input'); + + dimInput.node().value = config.cuts[config[dimension].column][config.display]; + + //don't think this actually changes functionality, but nice to have it accurate just in case + dimInput.option = + 'settings.cuts.' + [config[dimension].column] + '.' + [config.display]; + }); + } + } + + function hideEmptyChart() { + var emptyChart = this.filtered_data.length == 0; + this.wrap.style('display', emptyChart ? 'none' : 'inline-block'); + this.emptyChartWarning.style('display', emptyChart ? 'inline-block' : 'none'); + } + + function onDraw() { + //clear participant Details + clearParticipantDetails.call(this); + + //get correct cutpoint for the current view + syncCutpoints.call(this); + + //update domains to include cut lines + setDomain.call(this, 'x'); + setDomain.call(this, 'y'); + + //Set update cutpoint interactivity + setCutpointMinimums.call(this); + + //Classify participants in to eDISH quadrants + updateQuadrantData.call(this); + + //update the count in the filter label + updateFilterLabel.call(this); + hideEmptyChart.call(this); + } + + function drawQuadrants() { + var _this = this; + + var config = this.config; + var x_var = this.config.x.column; + var y_var = this.config.y.column; + + var x_cut = this.config.cuts[x_var][config.display]; + var y_cut = this.config.cuts[y_var][config.display]; + + //position for cut-point lines + this.cut_lines.lines + .filter(function(d) { + return d.dimension == 'x'; + }) + .attr('x1', this.x(x_cut)) + .attr('x2', this.x(x_cut)) + .attr('y1', this.plot_height) + .attr('y2', 0); + + this.cut_lines.lines + .filter(function(d) { + return d.dimension == 'y'; + }) + .attr('x1', 0) + .attr('x2', this.plot_width) + .attr('y1', function(d) { + return _this.y(y_cut); + }) + .attr('y2', function(d) { + return _this.y(y_cut); + }); + + this.cut_lines.backing + .filter(function(d) { + return d.dimension == 'x'; + }) + .attr('x1', this.x(x_cut)) + .attr('x2', this.x(x_cut)) + .attr('y1', this.plot_height) + .attr('y2', 0); + + this.cut_lines.backing + .filter(function(d) { + return d.dimension == 'y'; + }) + .attr('x1', 0) + .attr('x2', this.plot_width) + .attr('y1', function(d) { + return _this.y(y_cut); + }) + .attr('y2', function(d) { + return _this.y(y_cut); + }); + + //position labels + this.quadrant_labels.g + .select('text.upper-right') + .attr('x', this.plot_width) + .attr('y', 0); + + this.quadrant_labels.g + .select('text.upper-left') + .attr('x', 0) + .attr('y', 0); + + this.quadrant_labels.g + .select('text.lower-right') + .attr('x', this.plot_width) + .attr('y', this.plot_height); + + this.quadrant_labels.g + .select('text.lower-left') + .attr('x', 0) + .attr('y', this.plot_height); + + this.quadrant_labels.text.text(function(d) { + return d.label + ' (' + d.percent + ')'; + }); + } + + //draw marginal rug for visit-level measures + function drawRugs(d, axis) { + var chart = this; + var config = this.config; + + //get matching measures + var allMatches = d.values.raw[0].raw; + var measure = config.measure_values[config[axis].column]; + var matches = allMatches.filter(function(f) { + return f[config.measure_col] == measure; + }); + + //draw the rug + var min_value = axis == 'x' ? chart.y.domain()[0] : chart.x.domain()[0]; + chart[axis + '_rug'] + .selectAll('text') + .data(matches) + .enter() + .append('text') + .attr('class', 'rug-tick') + .attr('x', function(d) { + return axis == 'x' ? chart.x(d[config.display]) : chart.x(min_value); + }) + .attr('y', function(d) { + return axis == 'y' ? chart.y(d[config.display]) : chart.y(min_value); + }) + // .attr('dy', axis == 'x' ? '-0.2em' : null) + .attr('text-anchor', axis == 'y' ? 'end' : null) + .attr('alignment-baseline', axis == 'x' ? 'hanging' : null) + .attr('font-size', axis == 'x' ? '6px' : null) + .attr('stroke', function(d) { + return chart.colorScale(d[config.color_by]); + }) + .text(function(d) { + return axis == 'x' ? '|' : '–'; + }) + .append('svg:title') + .text(function(d) { + return ( + d[config.measure_col] + + '=' + + d3.format('.2f')(d.absolute) + + ' (' + + d3.format('.2f')(d.relative) + + ' xULN) @ ' + + d[config.visit_col] + ); + }); + } + + function addPointMouseover() { + var chart = this; + var config = this.config; + var points = this.marks[0].circles; + //add event listener to all participant level points + points + .filter(function(d) { + var disabled = d3.select(this).classed('disabled'); + return !disabled; + }) + .on('mouseover', function(d) { + //disable mouseover when highlights (onClick) are visible + var disabled = d3.select(this).classed('disabled'); + if (!disabled) { + //clear previous mouseover if any + points.attr('stroke-width', 1); + clearRugs.call(chart, 'x'); + clearRugs.call(chart, 'y'); + + //draw the rugs + d3.select(this).attr('stroke-width', 3); + drawRugs.call(chart, d, 'x'); + drawRugs.call(chart, d, 'y'); + } + }); + } + + function drawVisitPath(d) { + var chart = this; + var config = chart.config; + + var allMatches = d.values.raw[0].raw; + var x_measure = config.measure_values[config.x.column]; + var y_measure = config.measure_values[config.y.column]; + var matches = allMatches.filter(function(f) { + return f[config.measure_col] == x_measure || f[config.measure_col] == y_measure; + }); + + //get coordinates by visit + var visits = d3 + .set( + matches.map(function(m) { + return m[config.studyday_col]; + }) + ) + .values(); + var visit_data = visits + .map(function(m) { + var visitObj = {}; + visitObj.studyday = +m; + visitObj.visit = config.visit_col + ? matches.filter(function(f) { + return f[config.studyday_col] == m; + })[0][config.visit_col] + : null; + visitObj.visitn = config.visitn_col + ? matches.filter(function(f) { + return f[config.studyday_col] == m; + })[0][config.visitn_col] + : null; + visitObj[config.color_by] = matches[0][config.color_by]; + + //get x coordinate + var x_match = matches + .filter(function(f) { + return f[config.studyday_col] == m; + }) + .filter(function(f) { + return f[config.measure_col] == x_measure; + }); + + if (x_match.length) { + visitObj.x = x_match[0][config.display]; + visitObj.xMatch = x_match[0]; + } else { + visitObj.x = null; + visitObj.xMatch = null; + } + + //get y coordinate + var y_match = matches + .filter(function(f) { + return f[config.studyday_col] == m; + }) + .filter(function(f) { + return f[config.measure_col] == y_measure; + }); + if (y_match.length) { + visitObj.y = y_match[0][config.display]; + visitObj.yMatch = y_match[0]; + } else { + visitObj.y = null; + visitObj.yMatch = null; + } + + return visitObj; + }) + .sort(function(a, b) { + return a.studyday - b.studyday; + }) + .filter(function(f) { + return (f.x > 0) & (f.y > 0); + }); + + //draw the path + var myLine = d3.svg + .line() + .x(function(d) { + return chart.x(d.x); + }) + .y(function(d) { + return chart.y(d.y); + }); + + chart.visitPath.selectAll('*').remove(); + chart.visitPath.moveToFront(); + + var path = chart.visitPath + .append('path') + .attr('class', 'participant-visits') + .datum(visit_data) + .attr('d', myLine) + .attr('stroke', function(d) { + return chart.colorScale(matches[0][config.color_by]); + }) + .attr('stroke-width', '2px') + .attr('fill', 'none'); + + //Little trick for animating line drawing + var totalLength = path.node().getTotalLength(); + path.attr('stroke-dasharray', totalLength + ' ' + totalLength) + .attr('stroke-dashoffset', totalLength) + .transition() + .duration(2000) + .ease('linear') + .attr('stroke-dashoffset', 0); + + //draw visit points + var visitPoints = chart.visitPath + .selectAll('g.visit-point') + .data(visit_data) + .enter() + .append('g') + .attr('class', 'visit-point'); + + visitPoints + .append('circle') + .attr('class', 'participant-visits') + .attr('r', 0) + .attr('stroke', function(d) { + return chart.colorScale(d[config.color_by]); + }) + .attr('stroke-width', 1) + .attr('cx', function(d) { + return chart.x(d.x); + }) + .attr('cy', function(d) { + return chart.y(d.y); + }) + .attr('fill', function(d) { + return chart.colorScale(d[config.color_by]); + }) + .attr('fill-opacity', 0.5) + .transition() + .delay(2000) + .duration(200) + .attr('r', 4); + + //custom titles for points on mouseover + visitPoints.append('title').text(function(d) { + var xvar = config.x.column; + var yvar = config.y.column; + var studyday_label = 'Study day: ' + d.studyday + '\n', + visitn_label = d.visitn ? 'Visit Number: ' + d.visitn + '\n' : '', + visit_label = d.visit ? 'Visit: ' + d.visit + '\n' : '', + x_label = config.x.label + ': ' + d3.format('0.3f')(d.x) + '\n', + y_label = config.y.label + ': ' + d3.format('0.3f')(d.y); + + return studyday_label + visit_label + visitn_label + x_label + y_label; + }); + } + + function makeNestedData(d) { + var chart = this; + var config = chart.config; + var allMatches = d.values.raw[0].raw; + + var ranges = d3 + .nest() + .key(function(d) { + return d[config.measure_col]; + }) + .rollup(function(d) { + var vals = d + .map(function(m) { + return m[config.value_col]; + }) + .sort(function(a, b) { + return a - b; + }); + var lower_extent = d3.quantile(vals, config.measureBounds[0]), + upper_extent = d3.quantile(vals, config.measureBounds[1]); + return [lower_extent, upper_extent]; + }) + .entries(chart.initial_data); + + //make nest by measure + var nested = d3 + .nest() + .key(function(d) { + return d[config.measure_col]; + }) + .rollup(function(d) { + var measureObj = {}; + measureObj.eDish = chart; + measureObj.key = d[0][config.measure_col]; + measureObj.raw = d; + measureObj.values = d.map(function(d) { + return +d[config.value_col]; + }); + measureObj.max = +d3.format('0.2f')(d3.max(measureObj.values)); + measureObj.min = +d3.format('0.2f')(d3.min(measureObj.values)); + measureObj.median = +d3.format('0.2f')(d3.median(measureObj.values)); + measureObj.n = measureObj.values.length; + measureObj.spark = 'spark!'; + measureObj.population_extent = ranges.find(function(f) { + return measureObj.key == f.key; + }).values; + var hasColor = + chart.spaghetti.colorScale.domain().indexOf(d[0][config.measure_col]) > -1; + measureObj.color = hasColor + ? chart.spaghetti.colorScale(d[0][config.measure_col]) + : 'black'; + measureObj.spark_data = d.map(function(m) { + var obj = { + id: m[config.id_col], + lab: m[config.measure_col], + visit: config.visit_col ? m[config.visit_col] : null, + visitn: config.visitn_col ? +m[config.visitn_col] : null, + studyday: +m[config.studyday_col], + value: +m[config.value_col], + lln: config.normal_col_low ? +m[config.normal_col_low] : null, + uln: +m[config.normal_col_high], + population_extent: measureObj.population_extent, + outlier_low: config.normal_col_low + ? +m[config.value_col] < +m[config.normal_col_low] + : null, + outlier_high: +m[config.value_col] > +m[config.normal_col_high] + }; + obj.outlier = obj.outlier_low || obj.outlier_high; + return obj; + }); + return measureObj; + }) + .entries(allMatches); + + var nested = nested + .map(function(m) { + return m.values; + }) + .sort(function(a, b) { + var a_order = Object.keys(config.measure_values) + .map(function(e) { + return config.measure_values[e]; + }) + .indexOf(a.key); + var b_order = Object.keys(config.measure_values) + .map(function(e) { + return config.measure_values[e]; + }) + .indexOf(b.key); + return b_order - a_order; + }); + return nested; + } + + function addSparkLines(d) { + if (this.data.raw.length > 0) { + //don't try to draw sparklines if the table is empty + this.tbody + .selectAll('tr') + .style('background', 'none') + .style('border-bottom', '.5px solid black') + .each(function(row_d) { + //Spark line cell + var cell = d3 + .select(this) + .select('td.spark') + .classed('minimized', true) + .text(''), + toggle = cell + .append('span') + .html('▽') + .style('cursor', 'pointer') + .style('color', '#999') + .style('vertical-align', 'middle'), + width = 100, + height = 25, + offset = 4, + overTime = row_d.spark_data.sort(function(a, b) { + return +a.studyday - +b.studyday; + }), + color = row_d.color; + + var x = d3.scale + .linear() + .domain( + d3.extent(overTime, function(m) { + return m.studyday; + }) + ) + .range([offset, width - offset]); + + //y-domain includes 99th population percentile + any participant outliers + var y_min = d3.min(d3.merge([row_d.values, row_d.population_extent])) * 0.99; + var y_max = d3.max(d3.merge([row_d.values, row_d.population_extent])) * 1.01; + var y = d3.scale + .linear() + .domain([y_min, y_max]) + .range([height - offset, offset]); + + //render the svg + var svg = cell + .append('svg') + .attr({ + width: width, + height: height + }) + .append('g'); + + //draw the normal range polygon ULN and LLN + var upper = overTime.map(function(m) { + return { studyday: m.studyday, value: m.uln }; + }); + var lower = overTime + .map(function(m) { + return { studyday: m.studyday, value: m.lln }; + }) + .reverse(); + var normal_data = d3.merge([upper, lower]).filter(function(m) { + return m.value; + }); + + var drawnormal = d3.svg + .line() + .x(function(d) { + return x(d.studyday); + }) + .y(function(d) { + return y(d.value); + }); + + var normalpath = svg + .append('path') + .datum(normal_data) + .attr({ + class: 'normalrange', + d: drawnormal, + fill: '#eee', + stroke: 'none' + }); + + //draw lines at the population guidelines + svg.selectAll('lines.guidelines') + .data(row_d.population_extent) + .enter() + .append('line') + .attr('class', 'guidelines') + .attr('x1', 0) + .attr('x2', width) + .attr('y1', function(d) { + return y(d); + }) + .attr('y2', function(d) { + return y(d); + }) + .attr('stroke', '#ccc') + .attr('stroke-dasharray', '2 2'); + + //draw the sparkline + var draw_sparkline = d3.svg + .line() + .interpolate('cardinal') + .x(function(d) { + return x(d.studyday); + }) + .y(function(d) { + return y(d.value); + }); + var sparkline = svg + .append('path') + .datum(overTime) + .attr({ + class: 'sparkLine', + d: draw_sparkline, + fill: 'none', + stroke: color + }); + + //draw outliers + var outliers = overTime.filter(function(f) { + return f.outlier; + }); + var outlier_circles = svg + .selectAll('circle.outlier') + .data(outliers) + .enter() + .append('circle') + .attr('class', 'circle outlier') + .attr('cx', function(d) { + return x(d.studyday); + }) + .attr('cy', function(d) { + return y(d.value); + }) + .attr('r', '2px') + .attr('stroke', color) + .attr('fill', color); + }); + } + } + + function insertAfter(newNode, referenceNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); + } + + var defaultSettings = { + max_width: 800, + aspect: 4, + x: { + column: 'studyday', + type: 'linear', + label: 'Study Day' + }, + y: { + column: 'value', + type: 'linear', + label: '', + format: '.1f' + }, + marks: [ + { + type: 'line', + per: ['lab'] + }, + { + type: 'circle', + radius: 4, + per: ['lab', 'studyday'] //, + // values: { outlier: [true] }, + // attributes: { + // 'fill-opacity': 1 + // } + } + ], + margin: { top: 20 }, + gridlines: 'x', + colors: [] + }; + + function setDomain$1(d) { + //y-domain includes 99th population percentile + any participant outliers + var raw_values = this.raw_data.map(function(m) { + return m.value; + }); + var population_extent = this.raw_data[0].population_extent; + var y_min = d3.min(d3.merge([raw_values, population_extent])) * 0.99; + var y_max = d3.max(d3.merge([raw_values, population_extent])) * 1.01; + this.y.domain([y_min, y_max]); + this.y_dom = [y_min, y_max]; + } + + function drawPopulationExtent() { + var lineChart = this; + this.svg + .selectAll('line.guidelines') + .data(lineChart.raw_data[0].population_extent) + .enter() + .append('line') + .attr('class', 'guidelines') + .attr('x1', 0) + .attr('x2', lineChart.plot_width) + .attr('y1', function(d) { + return lineChart.y(d); + }) + .attr('y2', function(d) { + return lineChart.y(d); + }) + .attr('stroke', '#ccc') + .attr('stroke-dasharray', '2 2'); + } + + function drawNormalRange() { + var lineChart = this; + var upper = this.raw_data.map(function(m) { + return { studyday: m.studyday, value: m.uln }; + }); + var lower = this.raw_data + .map(function(m) { + return { studyday: m.studyday, value: m.lln }; + }) + .reverse(); + var normal_data = d3.merge([upper, lower]).filter(function(f) { + return f.value || f.value == 0; + }); + var drawnormal = d3.svg + .line() + .x(function(d) { + return lineChart.x(d.studyday); + }) + .y(function(d) { + return lineChart.y(d.value); + }); + var normalpath = this.svg + .append('path') + .datum(normal_data) + .attr({ + class: 'normalrange', + d: drawnormal, + fill: '#eee', + stroke: 'none' + }); + normalpath.moveToBack(); + } + + function addPointTitles() { + var config = this.edish.config; + var points = this.marks[1].circles; + points.select('title').remove(); + points.append('title').text(function(d) { + var raw = d.values.raw[0]; + var xvar = config.x.column; + var yvar = config.y.column; + var studyday_label = 'Study day: ' + raw.studyday + '\n', + visitn_label = raw.visitn ? 'Visit Number: ' + raw.visitn + '\n' : '', + visit_label = raw.visit ? 'Visit: ' + raw.visit + '\n' : '', + lab_label = raw.lab + ': ' + d3.format('0.3f')(raw.value); + return studyday_label + visit_label + visitn_label + lab_label; + }); + } + + function updatePointFill() { + var points = this.marks[1].circles; + points.attr('fill-opacity', function(d) { + var outlier = d.values.raw[0].outlier; + return outlier ? 1 : 0; + }); + } + + function init$2(d, edish) { + //layout the new cells on the DOM (slightly easier than using D3) + var summaryRow_node = this.parentNode; + var chartRow_node = document.createElement('tr'); + var chartCell_node = document.createElement('td'); + insertAfter(chartRow_node, summaryRow_node); + chartRow_node.appendChild(chartCell_node); + + //update the row styles + d3.select(chartRow_node) + .style('background', 'none') + .style('border-bottom', '0.5px solid black'); + + //layout the svg with D3 + var cellCount = d3.select(summaryRow_node).selectAll('td')[0].length; + var chartCell = d3.select(chartCell_node).attr('colspan', cellCount); + + //draw the chart + defaultSettings.colors = [d.color]; + var lineChart = webcharts.createChart(chartCell_node, defaultSettings); + lineChart.on('draw', function() { + setDomain$1.call(this); + }); + lineChart.edish = edish; + lineChart.on('resize', function() { + drawPopulationExtent.call(this); + drawNormalRange.call(this); + addPointTitles.call(this); + updatePointFill.call(this); + }); + lineChart.init(d.spark_data); + lineChart.row = chartRow_node; + return lineChart; + } + + function addSparkClick() { + var edish = this.edish; + if (this.data.raw.length > 0) { + this.tbody + .selectAll('tr') + .select('td.spark') + .on('click', function(d) { + if (d3.select(this).classed('minimized')) { + d3.select(this).classed('minimized', false); + d3.select(this.parentNode).style('border-bottom', 'none'); + + this.lineChart = init$2.call(this, d, edish); + d3.select(this) + .select('svg') + .style('display', 'none'); + + d3.select(this) + .select('span') + .html('△ Minimize Chart'); + } else { + d3.select(this).classed('minimized', true); + + d3.select(this.parentNode).style('border-bottom', '0.5px solid black'); + + d3.select(this) + .select('span') + .html('▽'); + + d3.select(this) + .select('svg') + .style('display', null); + + d3.select(this.lineChart.row).remove(); + this.lineChart.destroy(); + } + }); + } + } + + function addFootnote$1() { + var footnoteText = [ + 'The y-axis for each chart is set to the ' + + this.edish.config.measureBounds + .map(function(bound) { + var percentile = '' + Math.round(bound * 100); + var lastDigit = +percentile.substring(percentile.length - 1); + var text = + percentile + + ([0, 4, 5, 6, 7, 8, 9].indexOf(lastDigit) > -1 + ? 'th' + : lastDigit === 3 + ? 'rd' + : lastDigit === 2 + ? 'nd' + : 'st'); + return text; + }) + .join(' and ') + + " percentiles of the entire population's results for that measure. " + + 'Values outside the normal range are plotted as individual points. ' + + 'Click a sparkline to view a more detailed version of the chart.' + ]; + var footnotes = this.wrap.selectAll('span.footnote').data(footnoteText, function(d) { + return d; + }); + + footnotes + .enter() + .append('span') + .attr('class', 'footnote') + .style('font-size', '0.7em') + .style('padding-top', '0.1em') + .text(function(d) { + return d; + }); + + footnotes.exit().remove(); + } + + function addExtraMeasureToggle() { + var measureTable = this; + var chart = this.edish; + var config = chart.config; + + measureTable.wrap.selectAll('div.wc-controls').remove(); + + //check to see if there are extra measures in the MeasureTable + var specifiedMeasures = Object.keys(config.measure_values).map(function(e) { + return config.measure_values[e]; + }); + var tableMeasures = measureTable.data.raw.map(function(f) { + return f.key; + }); + + //if extra measure exist... + if (tableMeasures.length > specifiedMeasures.length) { + var extraRows = measureTable.table + .select('tbody') + .selectAll('tr') + .filter(function(f) { + return specifiedMeasures.indexOf(f.key) == -1; + }); + + //hide extra rows by default + extraRows.style('display', 'none'); + + //add a toggle + var toggleDiv = measureTable.wrap + .insert('div', '*') + .attr('class', 'wc-controls') + .append('div') + .attr('class', 'control-group'); + var extraCount = tableMeasures.length - specifiedMeasures.length; + toggleDiv + .append('span') + .attr('class', 'wc-control-label') + .style('display', 'inline-block') + .style('padding-right', '.3em') + .text( + 'Show ' + + extraCount + + ' additional measure' + + (extraCount == 1 ? '' : 's') + + ':' + ); + var toggle = toggleDiv.append('input').property('type', 'checkbox'); + toggle.on('change', function() { + var showRows = this.checked; + extraRows.style('display', showRows ? null : 'none'); + }); + } + } + + function drawMeasureTable(d) { + var nested = makeNestedData.call(this, d); + + //draw the measure table + this.measureTable.edish = this; + this.measureTable.on('draw', function() { + addSparkLines.call(this); + addSparkClick.call(this); + addExtraMeasureToggle.call(this); + addFootnote$1.call(this); + }); + this.measureTable.draw(nested); + } + + function makeParticipantHeader(d) { + var chart = this; + var wrap = this.participantDetails.header; + var raw = d.values.raw[0]; + + var title = this.participantDetails.header + .append('h3') + .attr('class', 'id') + .html('Participant Details') + .style('border-top', '2px solid black') + .style('border-bottom', '2px solid black') + .style('padding', '.2em'); + + if (chart.config.participantProfileURL) { + title + .append('a') + .html('Full Participant Profile') + .attr('href', chart.config.participantProfileURL) + .style('font-size', '0.8em') + .style('padding-left', '1em'); + } + + title + .append('Button') + .text('Clear') + .style('margin-left', '1em') + .style('float', 'right') + .on('click', function() { + clearParticipantDetails.call(chart); + }); + + //show detail variables in a ul + var ul = this.participantDetails.header + .append('ul') + .style('list-style', 'none') + .style('padding', '0'); + + var lis = ul + .selectAll('li') + .data(chart.config.details) + .enter() + .append('li') + .style('', 'block') + .style('display', 'inline-block') + .style('text-align', 'center') + .style('padding', '0.5em'); + + lis.append('div') + .text(function(d) { + return d.label; + }) + .attr('div', 'label') + .style('font-size', '0.8em'); + + lis.append('div') + .text(function(d) { + return raw[d.value_col]; + }) + .attr('div', 'value'); + } + + var defaultSettings$1 = { + max_width: 600, + x: { + column: null, + type: 'linear', + label: 'Study Day' + }, + y: defineProperty( + { + column: 'relative_uln', + type: 'linear', + label: null, // set in ../callbacks/onPreprocess + domain: null, + format: '.1f' + }, + 'domain', + [0, null] + ), + marks: [ + { + type: 'line', + per: [] + }, + { + type: 'circle', + radius: 4, + per: [] + } + ], + margin: { top: 20, bottom: 70 }, // bottom margin provides space for exposure plot + gridlines: 'xy', + color_by: null, + colors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628'], + aspect: 2 + }; + + var controlInputs$1 = [ + { + type: 'subsetter', + label: 'Select Labs', + value_col: null, + multiple: true + }, + { + type: 'dropdown', + label: 'Y-axis Display Type', + description: null, + option: 'displayLabel', + start: null, + values: null, + require: true + } + ]; + + function onLayout$1() { + var spaghetti = this; + var eDish = this.edish; + + //customize the display control + var displayControlWrap = spaghetti.controls.wrap + .selectAll('div') + .filter(function(controlInput) { + return controlInput.label === 'Y-axis Display Type'; + }); + + var displayControl = displayControlWrap.select('select'); + + //set the start value + var start_value = eDish.config.display_options.find(function(f) { + return f.value == eDish.config.display; + }).label; + + displayControl.selectAll('option').attr('selected', function(d) { + return d == start_value ? 'selected' : null; + }); + + displayControl.on('change', function(d) { + var currentLabel = this.value; + var currentValue = eDish.config.display_options.find(function(f) { + return f.label == currentLabel; + }).value; + spaghetti.config.y.column = currentValue; + spaghetti.draw(); + }); + } + + function onPreprocess$1() { + var config = this.config; + var unit = this.config.y.column == 'relative_uln' ? '[xULN]' : '[xBaseline]'; + config.y.label = 'Standardized Result ' + unit; + } + + function drawCutLine(d) { + //bit of a hack to make this work with paths and circles + var spaghetti = this; + var config = this.config; + var raw = d.values.raw ? d.values.raw[0] : d.values[0].values.raw[0]; + var cut = raw[config.y.column + '_cut']; + var param = raw[config.color_by]; + spaghetti.cutLine = spaghetti.svg + .append('line') + .attr('y1', spaghetti.y(cut)) + .attr('y2', spaghetti.y(cut)) + .attr('x1', 0) + .attr('x2', spaghetti.plot_width) + .attr('stroke', spaghetti.colorScale(param)) + .attr('stroke-dasharray', '3 3'); + spaghetti.cutLabel = spaghetti.svg + .append('text') + .attr('y', spaghetti.y(cut)) + .attr('dy', '-0.2em') + .attr('x', spaghetti.plot_width) + .attr('text-anchor', 'end') + .attr('alignment-baseline', 'baseline') + .attr('fill', spaghetti.colorScale(param)) + .text(d3.format('0.1f')(cut)); + } + + function addPointTitles$1() { + var spaghetti = this; + var config = this.edish.config; + var points = this.marks[1].circles; + points.select('title').remove(); + points.append('title').text(function(d) { + var raw = d.values.raw[0]; + var ylabel = spaghetti.config.displayLabel; + var yvar = spaghetti.config.y.column; + var studyday_label = 'Study day: ' + raw[config.studyday_col] + '\n', + visitn_label = config.visitn_col + ? 'Visit Number: ' + raw[config.visitn_col] + '\n' + : '', + visit_label = config.visit_col ? 'Visit: ' + raw[config.visit_col] + '\n' : '', + raw_label = + 'Raw ' + + raw[config.measure_col] + + ': ' + + d3.format('0.3f')(raw[config.value_col]) + + '\n', + adj_label = + 'Adjusted ' + raw[config.measure_col] + ': ' + d3.format('0.3f')(raw[yvar]); + return studyday_label + visit_label + visitn_label + raw_label + adj_label; + }); + } + + function addExposure() { + var context = this; + this.svg.select('.se-exposure-supergroup').remove(); + + //If exposure data exists, annotate exposures beneath x-axis. + if (this.edish.exposure.include) { + var supergroup = this.svg + .insert('g', '.supergroup') + .classed('se-exposure-supergroup', true); + var dy = 20; // offset from chart + var strokeWidth = 5; // width/diameter of marks + this.svg.selectAll('.x.axis .tick text').attr('dy', dy + strokeWidth * 3 + 'px'); // offset x-axis tick labels + + //top boundary line + supergroup.append('line').attr({ + x1: -this.margin.left, + y1: this.plot_height + dy - strokeWidth * 2, + x2: this.plot_width, + y2: this.plot_height + dy - strokeWidth * 2, + stroke: 'black', + 'stroke-opacity': 0.1 + }); + + //Exposure text + supergroup + .append('text') + .attr({ + x: -3, + y: this.plot_height + dy + strokeWidth, + 'text-anchor': 'end', + textLength: this.margin.left - 3 + }) + .text('Exposure'); + + //bottom boundary line + supergroup.append('line').attr({ + x1: -this.margin.left, + y1: this.plot_height + dy + strokeWidth * 2, + x2: this.plot_width, + y2: this.plot_height + dy + strokeWidth * 2, + stroke: 'black', + 'stroke-opacity': 0.1 + }); + + //Exposures + var groups = supergroup + .selectAll('g.se-exposure-group') + .data(this.exposure_data) + .enter() + .append('g') + .classed('se-exposure-group', true); + groups.each(function(d) { + var group = d3.select(this); + + //draw a line if exposure start and end dates are unequal + if ( + d[context.edish.config.exposure_stdy_col] !== + d[context.edish.config.exposure_endy_col] + ) { + group + .append('line') + .classed('se-exposure-line', true) + .attr({ + x1: function x1(d) { + return context.x(+d[context.edish.config.exposure_stdy_col]); + }, + y1: context.plot_height + dy, + x2: function x2(d) { + return context.x(+d[context.edish.config.exposure_endy_col]); + }, + y2: context.plot_height + dy, + stroke: 'black', + 'stroke-width': strokeWidth, + 'stroke-opacity': 0.25 + }) + .on('mouseover', function(d) { + this.setAttribute('stroke-width', strokeWidth * 2); + + //annotate a rectangle in the chart + group + .append('rect') + .classed('se-exposure-highlight', true) + .attr({ + x: function x(d) { + return context.x( + +d[context.edish.config.exposure_stdy_col] + ); + }, + y: 0, + width: function width(d) { + return ( + context.x(+d[context.edish.config.exposure_endy_col]) - + context.x(+d[context.edish.config.exposure_stdy_col]) + ); + }, + height: context.plot_height, + fill: 'black', + 'fill-opacity': 0.25 + }); + }) + .on('mouseout', function(d) { + this.setAttribute('stroke-width', strokeWidth); + + //remove rectangle from the chart + group.select('.se-exposure-highlight').remove(); + }) + .append('title') + .text( + 'Study Day: ' + + d[context.edish.config.exposure_stdy_col] + + '-' + + d[context.edish.config.exposure_endy_col] + + ' (' + + (+d[context.edish.config.exposure_endy_col] - + +d[context.edish.config.exposure_stdy_col] + + (+d[context.edish.config.exposure_endy_col] >= + +d[context.edish.config.exposure_stdy_col])) + + ' days)\nTreatment: ' + + d[context.edish.config.exposure_trt_col] + + '\nDose: ' + + d[context.edish.config.exposure_dose_col] + + ' ' + + d[context.edish.config.exposure_dosu_col] + ); + } + //draw a circle if exposure start and end dates are equal + else { + group + .append('circle') + .classed('se-exposure-circle', true) + .attr({ + cx: function cx(d) { + return context.x(+d[context.edish.config.exposure_stdy_col]); + }, + cy: context.plot_height + dy, + r: strokeWidth / 2, + fill: 'black', + 'fill-opacity': 0.25, + stroke: 'black', + 'stroke-opacity': 1 + }) + .on('mouseover', function(d) { + this.setAttribute('r', strokeWidth); + + //annotate a vertical line in the chart + group + .append('line') + .classed('se-exposure-highlight', true) + .attr({ + x1: context.x(+d[context.edish.config.exposure_stdy_col]), + y1: 0, + x2: context.x(+d[context.edish.config.exposure_stdy_col]), + y2: context.plot_height, + stroke: 'black', + 'stroke-width': 1, + 'stroke-opacity': 0.5, + 'stroke-dasharray': '3 1' + }); + }) + .on('mouseout', function(d) { + this.setAttribute('r', strokeWidth / 2); + + //remove vertical line from the chart + group.select('.se-exposure-highlight').remove(); + }) + .append('title') + .text( + 'Study Day: ' + + d[context.edish.config.exposure_stdy_col] + + '\nTreatment: ' + + d[context.edish.config.exposure_trt_col] + + '\nDose: ' + + d[context.edish.config.exposure_dose_col] + + ' ' + + d[context.edish.config.exposure_dosu_col] + ); + } + }); + } + } + + function onResize() { + var spaghetti = this; + var config = this.config; + + addPointTitles$1.call(this); + + //fill circles above the cut point + var y_col = this.config.y.column; + this.marks[1].circles + .attr('fill-opacity', function(d) { + return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; + }) + .attr('fill-opacity', function(d) { + return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; + }); + + //Show cut lines on mouseover + this.marks[1].circles + .on('mouseover', function(d) { + drawCutLine.call(spaghetti, d); + }) + .on('mouseout', function() { + spaghetti.cutLine.remove(); + spaghetti.cutLabel.remove(); + }); + + this.marks[0].paths + .on('mouseover', function(d) { + drawCutLine.call(spaghetti, d); + }) + .on('mouseout', function() { + spaghetti.cutLine.remove(); + spaghetti.cutLabel.remove(); + }); + + //annotate treatment exposure + addExposure.call(this); + + //embiggen clip-path so points aren't clipped + var radius = this.config.marks.find(function(mark) { + return mark.type === 'circle'; + }).radius; + this.svg + .select('.plotting-area') + .attr('width', this.plot_width + radius * 2 + 2) // plot width + circle radius * 2 + circle stroke width * 2 + .attr('height', this.plot_height + radius * 2 + 2) // plot height + circle radius * 2 + circle stroke width * 2 + .attr( + 'transform', + 'translate(-' + + (radius + 1) + // translate left circle radius + circle stroke width + ',-' + + (radius + 1) + // translate up circle radius + circle stroke width + ')' + ); + } + + function onDraw$1() { + var _this = this; + + var spaghetti = this; + var eDish = this.edish; + + //make sure x-domain includes the extent of the exposure data + if (this.edish.exposure.include) { + this.exposure_data = this.edish.exposure.data.filter(function(d) { + return d[_this.edish.config.id_col] === _this.edish.clicked_id; + }); + var extent = [ + d3.min(this.exposure_data, function(d) { + return +d[_this.edish.config.exposure_stdy_col]; + }), + d3.max(this.exposure_data, function(d) { + return +d[_this.edish.config.exposure_endy_col]; + }) + ]; + if (extent[0] < this.x_dom[0]) this.x_dom[0] = extent[0]; + if (extent[1] > this.x_dom[1]) this.x_dom[1] = extent[1]; + } + + //make sure y domain includes the current cut point for all measures + var max_value = d3.max(spaghetti.filtered_data, function(f) { + return f[spaghetti.config.y.column]; + }); + var max_cut = d3.max(spaghetti.filtered_data, function(f) { + return f[spaghetti.config.y.column + '_cut']; + }); + var y_max = d3.max([max_value, max_cut]); + spaghetti.config.y.domain = [0, y_max]; + spaghetti.y_dom = spaghetti.config.y.domain; + + //initialize the measureTable + if (spaghetti.config.firstDraw) { + drawMeasureTable.call(eDish, this.participant_data); + spaghetti.config.firstDraw = false; + } + } + + function init$3(d) { + var chart = this; //the full eDish object + var config = this.config; //the eDish config + var matches = d.values.raw[0].raw.filter(function(f) { + return f.key_measure; + }); + + if ('spaghetti' in chart) { + chart.spaghetti.destroy(); + } + + //sync settings + defaultSettings$1.x.column = config.studyday_col; + defaultSettings$1.color_by = config.measure_col; + defaultSettings$1.marks[0].per = [config.id_col, config.measure_col]; + defaultSettings$1.marks[1].per = [config.id_col, config.studyday_col, config.measure_col]; + defaultSettings$1.firstDraw = true; //only initailize the measure table on first draw + + //flag variables above the cut-off + matches.forEach(function(d) { + var measure = d[config['measure_col']]; + var label = Object.keys(config.measure_values).find(function(key) { + return config.measure_values[key] == measure; + }); + + d.relative_uln_cut = config.cuts[label].relative_uln; + d.relative_baseline_cut = config.cuts[label].relative_baseline; + + d.relative_uln_flagged = d.relative_uln >= d.relative_uln_cut; + d.relative_baseline_flagged = d.relative_baseline >= d.relative_baseline_cut; + }); + + //update the controls + var spaghettiElement = this.element + ' .participantDetails .spaghettiPlot .chart'; + + //Add y axis type options + controlInputs$1.find(function(f) { + return f.label == 'Y-axis Display Type'; + }).values = config.display_options.map(function(m) { + return m.label; + }); + + //sync parameter filter + controlInputs$1.find(function(f) { + return f.label == 'Select Labs'; + }).value_col = config.measure_col; + + var spaghettiControls = webcharts.createControls(spaghettiElement, { + location: 'top', + inputs: controlInputs$1 + }); + + //draw that chart + if (!this.exposure.include) delete defaultSettings$1.margin.bottom; // use default bottom margin when not plotting exposure + chart.spaghetti = webcharts.createChart( + spaghettiElement, + defaultSettings$1, + spaghettiControls + ); + + chart.spaghetti.edish = chart; //link the full eDish object + chart.spaghetti.participant_data = d; //include the passed data (used to initialize the measure table) + chart.spaghetti.on('layout', onLayout$1); + chart.spaghetti.on('preprocess', onPreprocess$1); + chart.spaghetti.on('draw', onDraw$1); + chart.spaghetti.on('resize', onResize); + chart.spaghetti.init(matches); + + //add a footnote + chart.spaghetti.wrap + .append('div') + .attr('class', 'footnote') + .style('font-size', '0.7em') + .style('padding-top', '0.1em') + .text( + 'Points are filled for values above the current reference value. Mouseover a line to see the reference line for that lab.' + ); + } + + function addPointClick() { + var chart = this; + var config = this.config; + var points = this.marks[0].circles; + + //add event listener to all participant level points + points.on('click', function(d) { + chart.clicked_id = d.key; + clearParticipantDetails.call(chart, d); //clear the previous participant + chart.config.quadrants.table.wrap.style('display', 'none'); //hide the quadrant summary + + //format the eDish chart + points + .attr('stroke', '#ccc') //set all points to gray + .attr('fill', 'white') + .classed('disabled', true); //disable mouseover while viewing participant details + + d3.select(this) + .attr('stroke', function(d) { + return chart.colorScale(d.values.raw[0][config.color_by]); + }) //highlight selected point + .attr('stroke-width', 3); + + //Add elements to the eDish chart + drawVisitPath.call(chart, d); //draw the path showing participant's pattern over time + drawRugs.call(chart, d, 'x'); + drawRugs.call(chart, d, 'y'); + + //draw the "detail view" for the clicked participant + chart.participantDetails.wrap.selectAll('*').style('display', null); + makeParticipantHeader.call(chart, d); + init$3.call(chart, d); //NOTE: the measure table is initialized from within the spaghettiPlot + }); + } + + function addPointTitles$2() { + var config = this.config; + var points = this.marks[0].circles; + points.select('title').remove(); + points.append('title').text(function(d) { + var xvar = config.x.column; + var yvar = config.y.column; + var raw = d.values.raw[0], + xLabel = + config.x.label + + ': ' + + d3.format('0.2f')(raw[xvar]) + + ' @ Day ' + + raw[xvar + '_' + config.studyday_col], + yLabel = + config.y.label + + ': ' + + d3.format('0.2f')(raw[yvar]) + + ' @ Day ' + + raw[yvar + '_' + config.studyday_col], + dayDiff = raw['day_diff'] + ' days apart', + idLabel = 'Participant ID: ' + raw[config.id_col], + rRatioLabel = config.r_ratio_filter + ? '\n' + 'Overall R Ratio: ' + d3.format('0.2f')(raw.rRatio) + : ''; + return idLabel + rRatioLabel + '\n' + xLabel + '\n' + yLabel + '\n' + dayDiff; + }); + } + + function addAxisLabelTitles() { + var chart = this; + var config = this.config; + + var details = + config.display == 'relative_uln' + ? 'Values are plotted as multiples of the upper limit of normal for the measure.' + : config.display == 'relative_baseline' + ? "Values are plotted as multiples of the partipant's baseline value for the measure." + : config.display == 'absolute' + ? ' Values are plotted using the raw units for the measure.' + : null; + + var axisLabels = chart.svg + .selectAll('.axis') + .select('.axis-title') + .select('tspan') + .remove(); + + var axisLabels = chart.svg + .selectAll('.axis') + .select('.axis-title') + .append('tspan') + .html(function(d) { + //var current = d3.select(this).text(); + return ' ⓘ'; + }) + .attr('font-size', '0.8em') + .style('cursor', 'help') + .append('title') + .text(details); + } + + function toggleLegend() { + var hideLegend = this.config.color_by == 'NONE'; + this.wrap.select('.legend').style('display', hideLegend ? 'None' : 'block'); + } + + function dragStarted() { + var dimension = d3.select(this).classed('x') ? 'x' : 'y'; + var chart = d3.select(this).datum().chart; + + d3.select(this) + .select('line.cut-line') + .attr('stroke-width', '2') + .attr('stroke-dasharray', '2,2'); + + chart.quadrant_labels.g.style('display', 'none'); + } + + function dragged() { + var chart = d3.select(this).datum().chart; + + var x = d3.event.dx; + var y = d3.event.dy; + + var line = d3.select(this).select('line.cut-line'); + var lineBack = d3.select(this).select('line.cut-line-backing'); + + var dimension = d3.select(this).classed('x') ? 'x' : 'y'; + + // Update the line properties + var attributes = { + x1: Math.max(0, parseInt(line.attr('x1')) + (dimension == 'x' ? x : 0)), + x2: Math.max(0, parseInt(line.attr('x2')) + (dimension == 'x' ? x : 0)), + y1: Math.min(chart.plot_height, parseInt(line.attr('y1')) + (dimension == 'y' ? y : 0)), + y2: Math.min(chart.plot_height, parseInt(line.attr('y2')) + (dimension == 'y' ? y : 0)) + }; + + line.attr(attributes); + lineBack.attr(attributes); + + var rawCut = line.attr(dimension + '1'); + var current_cut = +d3.format('0.1f')(chart[dimension].invert(rawCut)); + + //update the cut control in real time + chart.controls.wrap + .selectAll('div.control-group') + .filter(function(f) { + return f.description + ? f.description.toLowerCase() == dimension + '-axis reference line' + : false; + }) + .select('input') + .node().value = current_cut; + var measure = chart.config[dimension].column; + chart.config.cuts[measure][chart.config.display] = current_cut; + } + + function dragEnded() { + var chart = d3.select(this).datum().chart; + + d3.select(this) + .select('line.cut-line') + .attr('stroke-width', '1') + .attr('stroke-dasharray', '5,5'); + chart.quadrant_labels.g.style('display', null); + + //redraw the chart (updates the needed cutpoint settings and quadrant annotations) + chart.draw(); + } + + // credit to https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19 + + function init$4() { + var drag = d3.behavior + .drag() + .origin(function(d) { + return d; + }) + .on('dragstart', dragStarted) + .on('drag', dragged) + .on('dragend', dragEnded); + + this.cut_lines.wrap.moveToFront(); + this.cut_lines.g.call(drag); + } + + function addBoxPlot( + svg, + results, + height, + width, + domain, + boxPlotWidth, + boxColor, + boxInsideColor, + fmt, + horizontal, + log + ) { + //set default orientation to "horizontal" + var horizontal = horizontal == undefined ? true : horizontal; + + //make the results numeric and sort + var results = results + .map(function(d) { + return +d; + }) + .sort(d3.ascending); + + //set up d3.scales + if (horizontal) { + var y = log ? d3.scale.log() : d3.scale.linear(); + y.range([height, 0]).domain(domain); + var x = d3.scale.linear().range([0, width]); + } else { + var x = log ? d3.scale.log() : d3.scale.linear(); + x.range([0, width]).domain(domain); + var y = d3.scale.linear().range([height, 0]); + } + + var probs = [0.05, 0.25, 0.5, 0.75, 0.95]; + for (var i = 0; i < probs.length; i++) { + probs[i] = d3.quantile(results, probs[i]); + } + + var boxplot = svg + .append('g') + .attr('class', 'boxplot') + .datum({ values: results, probs: probs }); + + //draw rectangle from q1 to q3 + var box_x = horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[1]); + var box_width = horizontal + ? x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2) + : x(probs[3]) - x(probs[1]); + var box_y = horizontal ? y(probs[3]) : y(0.5 + boxPlotWidth / 2); + var box_height = horizontal + ? -y(probs[3]) + y(probs[1]) + : y(0.5 - boxPlotWidth / 2) - y(0.5 + boxPlotWidth / 2); + + boxplot + .append('rect') + .attr('class', 'boxplot fill') + .attr('x', box_x) + .attr('width', box_width) + .attr('y', box_y) + .attr('height', box_height) + .style('fill', boxColor); + + //draw dividing lines at d3.median, 95% and 5% + var iS = [0, 2, 4]; + var iSclass = ['', 'd3.median', '']; + var iSColor = [boxColor, boxInsideColor, boxColor]; + for (var i = 0; i < iS.length; i++) { + boxplot + .append('line') + .attr('class', 'boxplot ' + iSclass[i]) + .attr('x1', horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[iS[i]])) + .attr('x2', horizontal ? x(0.5 + boxPlotWidth / 2) : x(probs[iS[i]])) + .attr('y1', horizontal ? y(probs[iS[i]]) : y(0.5 - boxPlotWidth / 2)) + .attr('y2', horizontal ? y(probs[iS[i]]) : y(0.5 + boxPlotWidth / 2)) + .style('fill', iSColor[i]) + .style('stroke', iSColor[i]); + } + + //draw lines from 5% to 25% and from 75% to 95% + var iS = [[0, 1], [3, 4]]; + for (var i = 0; i < iS.length; i++) { + boxplot + .append('line') + .attr('class', 'boxplot') + .attr('x1', horizontal ? x(0.5) : x(probs[iS[i][0]])) + .attr('x2', horizontal ? x(0.5) : x(probs[iS[i][1]])) + .attr('y1', horizontal ? y(probs[iS[i][0]]) : y(0.5)) + .attr('y2', horizontal ? y(probs[iS[i][1]]) : y(0.5)) + .style('stroke', boxColor); + } + + boxplot + .append('circle') + .attr('class', 'boxplot d3.mean') + .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) + .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) + .attr('r', horizontal ? x(boxPlotWidth / 3) : y(1 - boxPlotWidth / 3)) + .style('fill', boxInsideColor) + .style('stroke', boxColor); + + boxplot + .append('circle') + .attr('class', 'boxplot d3.mean') + .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) + .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) + .attr('r', horizontal ? x(boxPlotWidth / 6) : y(1 - boxPlotWidth / 6)) + .style('fill', boxColor) + .style('stroke', 'None'); + + var formatx = fmt ? d3.format(fmt) : d3.format('.2f'); + + boxplot + .selectAll('.boxplot') + .append('title') + .text(function(d) { + return ( + 'N = ' + + d.values.length + + '\n' + + 'd3.min = ' + + d3.min(d.values) + + '\n' + + '5th % = ' + + formatx(d3.quantile(d.values, 0.05)) + + '\n' + + 'Q1 = ' + + formatx(d3.quantile(d.values, 0.25)) + + '\n' + + 'd3.median = ' + + formatx(d3.median(d.values)) + + '\n' + + 'Q3 = ' + + formatx(d3.quantile(d.values, 0.75)) + + '\n' + + '95th % = ' + + formatx(d3.quantile(d.values, 0.95)) + + '\n' + + 'd3.max = ' + + d3.max(d.values) + + '\n' + + 'd3.mean = ' + + formatx(d3.mean(d.values)) + + '\n' + + 'StDev = ' + + formatx(d3.deviation(d.values)) + ); + }); + } + + function init$5() { + // Draw box plots + this.svg.selectAll('g.boxplot').remove(); + + // Y-axis box plot + var yValues = this.current_data.map(function(d) { + return d.values.y; + }); + var ybox = this.svg.append('g').attr('class', 'yMargin'); + addBoxPlot( + ybox, + yValues, + this.plot_height, + 1, + this.y_dom, + 10, + '#bbb', + 'white', + '0.2f', + true, + this.config.y.type == 'log' + ); + ybox.select('g.boxplot').attr( + 'transform', + 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' + ); + + //X-axis box plot + var xValues = this.current_data.map(function(d) { + return d.values.x; + }); + var xbox = this.svg.append('g').attr('class', 'xMargin'); + addBoxPlot( + xbox, //svg element + xValues, //values + 1, //height + this.plot_width, //width + this.x_dom, //domain + 10, //box plot width + '#bbb', //box color + 'white', //detail color + '0.2f', //format + false, // horizontal? + this.config.y.type == 'log' // log? + ); + xbox.select('g.boxplot').attr( + 'transform', + 'translate(0,' + -(this.config.margin.top / 2) + ')' + ); + } + + function setPointSize() { + var _this = this; + + var chart = this; + var config = this.config; + var points = this.svg.selectAll('g.point').select('circle'); + if (config.point_size != 'Uniform') { + //create the scale + var sizeScale = d3.scale + .linear() + .range([2, 10]) + .domain( + d3.extent( + chart.raw_data.map(function(m) { + return m[config.point_size]; + }) + ) + ); + + //draw a legend (coming later?) + + //set the point radius + points + .transition() + .attr('r', function(d) { + var raw = d.values.raw[0]; + return sizeScale(raw[config.point_size]); + }) + .attr('cx', function(d) { + return _this.x(d.values.x); + }) + .attr('cy', function(d) { + return _this.y(d.values.y); + }); + } + } + + function setPointOpacity() { + var config = this.config; + var points = this.svg.selectAll('g.point').select('circle'); + points.attr('fill-opacity', function(d) { + return d.values.raw[0].day_diff <= config.visit_window ? 1 : 0; + }); //fill points in visit_window + } + + function adjustTicks() { + this.svg + .selectAll('.x.axis .tick text') + .attr({ + transform: 'rotate(-45)', + dx: -10, + dy: 10 + }) + .style('text-anchor', 'end'); + } + + // Reposition any exisiting participant marks when the chart is resized + function updateParticipantMarks() { + var chart = this; + var config = this.config; + + //reposition participant visit path + var myNewLine = d3.svg + .line() + .x(function(d) { + return chart.x(d.x); + }) + .y(function(d) { + return chart.y(d.y); + }); + + chart.visitPath + .select('path') + .transition() + .attr('d', myNewLine); + + //reposition participant visit circles and labels + chart.visitPath + .selectAll('g.visit-point') + .select('circle') + .transition() + .attr('cx', function(d) { + return chart.x(d.x); + }) + .attr('cy', function(d) { + return chart.y(d.y); + }); + + chart.visitPath + .selectAll('g.visit-point') + .select('text.participant-visits') + .transition() + .attr('x', function(d) { + return chart.x(d.x); + }) + .attr('y', function(d) { + return chart.y(d.y); + }); + + //reposition axis rugs + chart.x_rug + .selectAll('text') + .transition() + .attr('x', function(d) { + return chart.x(d[config.display]); + }) + .attr('y', function(d) { + return chart.y(chart.y.domain()[0]); + }); + + chart.y_rug + .selectAll('text') + .transition() + .attr('x', function(d) { + return chart.x(chart.x.domain()[0]); + }) + .attr('y', function(d) { + return chart.y(d[config.display]); + }); + } + + function updateTimingFootnote() { + var config = this.config; + var windowText = + config.visit_window == 0 + ? 'on the same day' + : config.visit_window == 1 + ? 'within 1 day' + : 'within ' + config.visit_window + ' days'; + var timingFootnote = + ' Points where maximum ' + + config.measure_values[config.x.column] + + ' and ' + + config.measure_values[config.y.column] + + ' values were collected ' + + windowText + + ' are filled, others are empty.'; + + this.footnote.timing.text(timingFootnote); + } + + function onResize$1() { + //add point interactivity, custom title and formatting + addPointMouseover.call(this); + addPointClick.call(this); + addPointTitles$2.call(this); + addAxisLabelTitles.call(this); + formatPoints.call(this); + setPointSize.call(this); + setPointOpacity.call(this); + updateParticipantMarks.call(this); + + //draw the quadrants and add drag interactivity + updateSummaryTable.call(this); + drawQuadrants.call(this); + init$4.call(this); + + // hide the legend if no group options are given + toggleLegend.call(this); + + // add boxplots + init$5.call(this); + + //axis formatting + adjustTicks.call(this); + + //add timing footnote + updateTimingFootnote.call(this); + } + + var callbacks = { + onInit: onInit, + onLayout: onLayout, + onPreprocess: onPreprocess, + onDataTransform: onDataTransform, + onDraw: onDraw, + onResize: onResize$1 + }; + + function init$6() { + var lb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + var ex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + + //const data = mergeData(lb,ex); + this.data = { + lb: lb, + ex: ex + }; + this.chart.exposure = { + include: Array.isArray(ex) && ex.length, + data: ex + }; + this.chart.init(lb); + } + + function safetyedish(element, settings) { + var initial_settings = clone(settings); + var defaultSettings = configuration.settings(); + var controlInputs = configuration.controlInputs(); + var mergedSettings = Object.assign({}, defaultSettings, settings); + var syncedSettings = configuration.syncSettings(mergedSettings); + var syncedControlInputs = configuration.syncControlInputs(controlInputs, syncedSettings); + var controls = webcharts.createControls(element, { + location: 'top', + inputs: syncedControlInputs + }); + var chart = webcharts.createChart(element, syncedSettings, controls); + + chart.element = element; + chart.initial_settings = initial_settings; + + //Define callbacks. + for (var callback in callbacks) { + chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); + } + var se = { + element: element, + settings: settings, + chart: chart, + init: init$6 + }; + + return se; + } + + return safetyedish; +}); From c8e26e39d80ffff77438b4c9eb918dcd1c4bc5f9 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Wed, 5 Jun 2019 13:17:28 -0400 Subject: [PATCH 35/39] add link to vignette --- inst/safetyGraphics_app/ui.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index 61ee113a..71bf60f0 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -26,8 +26,12 @@ tagList( column(width=4, imageOutput(outputId = "hex")) ) ), + tabPanel("Shiny App User Guide", + tags$iframe(style="height:800px; width:100%; scrolling=yes;", `data-type`="iframe", + src = "https://cran.r-project.org/web/packages/safetyGraphics/vignettes/shinyUserGuide.html") + ), tabPanel("Clinical workflow", - tags$iframe(style="height:400px; width:100%; scrolling=yes;", `data-type`="iframe", + tags$iframe(style="height:800px; width:100%; scrolling=yes;", `data-type`="iframe", src = "https://cdn.jsdelivr.net/gh/SafetyGraphics/SafetyGraphics.github.io/ISG%20Hepatic%20Safety%20Explorer%20User's%20Manual%20%26%20Workflow%20v1.0.pdf") ) ) From f353eb99b93c233f8a1178f933c18795716bf453 Mon Sep 17 00:00:00 2001 From: bzkrouse Date: Wed, 5 Jun 2019 13:43:41 -0400 Subject: [PATCH 36/39] update name for workflow --- inst/safetyGraphics_app/ui.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/safetyGraphics_app/ui.R b/inst/safetyGraphics_app/ui.R index 71bf60f0..71e3fabb 100644 --- a/inst/safetyGraphics_app/ui.R +++ b/inst/safetyGraphics_app/ui.R @@ -30,7 +30,7 @@ tagList( tags$iframe(style="height:800px; width:100%; scrolling=yes;", `data-type`="iframe", src = "https://cran.r-project.org/web/packages/safetyGraphics/vignettes/shinyUserGuide.html") ), - tabPanel("Clinical workflow", + tabPanel("Hep Explorer workflow", tags$iframe(style="height:800px; width:100%; scrolling=yes;", `data-type`="iframe", src = "https://cdn.jsdelivr.net/gh/SafetyGraphics/SafetyGraphics.github.io/ISG%20Hepatic%20Safety%20Explorer%20User's%20Manual%20%26%20Workflow%20v1.0.pdf") ) From 0546b6a5e7a8accef5c452e3890253b0ac9cbee3 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Fri, 14 Jun 2019 10:34:21 -0700 Subject: [PATCH 37/39] update histogram. fix #335 --- inst/htmlwidgets/chartRenderer.yaml | 8 +- .../safetyHistogram.js | 806 +++++++++++++----- .../webcharts.css | 0 .../webcharts.js | 39 +- 4 files changed, 600 insertions(+), 253 deletions(-) rename inst/htmlwidgets/lib/{safety-histogram-2.3.0 => safety-histogram-2.4.0-dev}/safetyHistogram.js (70%) rename inst/htmlwidgets/lib/{webcharts-1.11.5 => webcharts-1.11.6}/webcharts.css (100%) rename inst/htmlwidgets/lib/{webcharts-1.11.5 => webcharts-1.11.6}/webcharts.js (99%) diff --git a/inst/htmlwidgets/chartRenderer.yaml b/inst/htmlwidgets/chartRenderer.yaml index 61da5119..d0232ec3 100644 --- a/inst/htmlwidgets/chartRenderer.yaml +++ b/inst/htmlwidgets/chartRenderer.yaml @@ -4,8 +4,8 @@ dependencies: src: htmlwidgets/lib/d3-3.5.17 script: d3.v3.min.js - name: webcharts - version: 1.11.5 - src: htmlwidgets/lib/webcharts-1.11.5 + version: 1.11.6 + src: htmlwidgets/lib/webcharts-1.11.6 script: webcharts.js stylesheet: webcharts.css - name: safety-eDish @@ -13,8 +13,8 @@ dependencies: src: htmlwidgets/lib/hep-explorer-1.0.1 script: hepexplorer.js - name: safety-histogram - version: 2.3.0 - src: htmlwidgets/lib/safety-histogram-2.3.0 + version: 2.4.0 + src: htmlwidgets/lib/safety-histogram-2.4.0-dev script: safetyHistogram.js - name: safety-outlier-explorer version: 2.5.4 diff --git a/inst/htmlwidgets/lib/safety-histogram-2.3.0/safetyHistogram.js b/inst/htmlwidgets/lib/safety-histogram-2.4.0-dev/safetyHistogram.js similarity index 70% rename from inst/htmlwidgets/lib/safety-histogram-2.3.0/safetyHistogram.js rename to inst/htmlwidgets/lib/safety-histogram-2.4.0-dev/safetyHistogram.js index 8af612ff..c2670775 100644 --- a/inst/htmlwidgets/lib/safety-histogram-2.3.0/safetyHistogram.js +++ b/inst/htmlwidgets/lib/safety-histogram-2.4.0-dev/safetyHistogram.js @@ -165,7 +165,9 @@ //miscellaneous settings start_value: null, normal_range: true, - displayNormalRange: false + displayNormalRange: false, + bin_algorithm: "Scott's normal reference rule", + annotate_bin_boundaries: false }; } @@ -191,8 +193,8 @@ { per: [], // set in ./syncSettings type: 'bar', - summarizeY: 'count', summarizeX: 'mean', + summarizeY: 'count', attributes: { 'fill-opacity': 0.75 } } ], @@ -202,6 +204,7 @@ function syncSettings(settings) { settings.x.column = settings.value_col; + settings.x.bin_algorithm = settings.bin_algorithm; settings.marks[0].per[0] = settings.value_col; //update normal range settings if normal_range is set to false @@ -222,16 +225,20 @@ //Define default details. var defaultDetails = [{ value_col: settings.id_col, label: 'Participant ID' }]; if (Array.isArray(settings.filters)) - settings.filters.forEach(function(filter) { - return defaultDetails.push({ - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter + settings.filters + .filter(function(filter) { + return filter.value_col !== settings.id_col; + }) + .forEach(function(filter) { + return defaultDetails.push({ + value_col: filter.value_col ? filter.value_col : filter, + label: filter.label + ? filter.label + : filter.value_col + ? filter.value_col + : filter + }); }); - }); defaultDetails.push({ value_col: settings.value_col, label: 'Result' }); if (settings.normal_col_low) defaultDetails.push({ @@ -293,10 +300,43 @@ label: 'Upper', require: true }, + { + type: 'dropdown', + option: 'x.bin_algorithm', + label: 'Algorithm', + values: [ + 'Square-root choice', + "Sturges' formula", + 'Rice Rule', + //'Doane\'s formula', + "Scott's normal reference rule", + "Freedman-Diaconis' choice", + "Shimazaki and Shinomoto's choice", + 'Custom' + ], + require: true + }, + { + type: 'number', + option: 'x.bin', + label: 'Quantity' + }, + { + type: 'number', + option: 'x.bin_width', + label: 'Width' + }, { type: 'checkbox', option: 'displayNormalRange', label: 'Normal Range' + }, + { + type: 'radio', + option: 'annotate_bin_boundaries', + label: 'X-axis Ticks', + values: [false, true], + relabels: ['linear', 'bin boundaries'] } ]; } @@ -305,7 +345,7 @@ //Add filters to default controls. if (Array.isArray(settings.filters) && settings.filters.length > 0) { var position = controlInputs.findIndex(function(input) { - return input.label === 'Normal Range'; + return input.label === 'Algorithm'; }); settings.filters.forEach(function(filter) { var filterObj = { @@ -852,6 +892,8 @@ } function identifyControls() { + var context = this; + var controlGroups = this.controls.wrap .style('padding-bottom', '8px') .selectAll('.control-group'); @@ -859,10 +901,12 @@ //Give each control a unique ID. controlGroups .attr('id', function(d) { - return d.label.toLowerCase().replace(' ', '-'); + return d.label.toLowerCase().replace(/ /g, '-'); }) .each(function(d) { - d3.select(this).classed(d.type, true); + var controlGroup = d3.select(this); + controlGroup.classed(d.type, true); + context.controls[d.label] = controlGroup; }); //Give x-axis controls a common class name. @@ -871,31 +915,39 @@ return ['x.domain[0]', 'x.domain[1]'].indexOf(d.option) > -1; }) .classed('x-axis', true); + + //Give binning controls a common class name. + controlGroups + .filter(function(d) { + return ['x.bin_algorithm', 'x.bin', 'x.bin_width'].indexOf(d.option) > -1; + }) + .classed('bin', true); } function addXdomainResetButton() { var _this = this; //Add x-domain reset button container. - var resetContainer = this.controls.wrap - .insert('div', '#lower') - .classed('control-group x-axis', true) - .datum({ - type: 'button', - option: 'x.domain', - label: '' - }) - .attr('title', 'Reset x-axis limits.') - .style('vertical-align', 'bottom'); + this.controls.reset = { + container: this.controls.wrap + .insert('div', '#lower') + .classed('control-group x-axis', true) + .datum({ + type: 'button', + option: 'x.domain', + label: '' + }) + .style('vertical-align', 'bottom') + }; //Add label. - resetContainer + this.controls.reset.label = this.controls.reset.container .append('span') .attr('class', 'wc-control-label') .text(''); //Add button. - resetContainer + this.controls.reset.button = this.controls.reset.container .append('button') .text(' Reset ') .style('padding', '0px 5px') @@ -955,6 +1007,9 @@ //Group filters. if (this.filters.length > 1) insertGrouping.call(this, '.subsetter:not(#measure)', 'Filters'); + + //Group bin controls. + insertGrouping.call(this, '.bin', 'Bins'); } function addXdomainZoomButton() { @@ -1019,6 +1074,45 @@ } } + function customizeBinsEventListener() { + var _this = this; + + var context = this; + + this.controls.Algorithm.selectAll('.wc-control-label') + .append('span') + .classed('algorithm-explanation', true) + .html(' ⓘ') + .style('cursor', 'pointer') + .on('click', function() { + if (_this.config.x.bin_algorithm !== 'Custom') + window.open( + 'https://en.wikipedia.org/wiki/Histogram#' + + _this.config.x.bin_algorithm + .replace(/ /g, '_') + .replace('Freedman-Diaconis', 'Freedman%E2%80%93Diaconis') + ); + }); + + this.controls.Quantity.selectAll('input') + .attr({ + min: 1, + step: 1 + }) + .on('change', function(d) { + if (this.value < 1) this.value = 1; + if (this.value % 1) this.value = Math.round(this.value); + context.config.x.bin = this.value; + context.config.x.bin_algorithm = 'Custom'; + context.controls.Algorithm.selectAll('option').property('selected', function(di) { + return di === 'Custom'; + }); + context.draw(); + }); + + this.controls.Width.selectAll('input').property('disabled', true); + } + function addParticipantCountContainer() { this.participantCount.container = this.controls.wrap .style('position', 'relative') @@ -1123,6 +1217,7 @@ addXdomainResetButton.call(this); groupControls.call(this); addXdomainZoomButton.call(this); + customizeBinsEventListener.call(this); addParticipantCountContainer.call(this); addRemovedRecordsNote.call(this); addBorderAboveChart.call(this); @@ -1132,47 +1227,8 @@ function getCurrentMeasure() { this.measure.previous = this.measure.current; this.measure.current = this.controls.wrap.selectAll('#measure option:checked').text(); - } - - function calculateStatistics(obj) { - var _this = this; - - //Define array of all and unique results. - obj.results = obj.data - .map(function(d) { - return +d[_this.config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }); - obj.uniqueResults = d3.set(obj.results).values(); - - //Calculate statistics. - obj.domain = d3.extent(obj.results); - obj.stats = { - n: obj.results.length, - nUnique: obj.uniqueResults.length, - min: obj.domain[0], - q25: d3.quantile(obj.results, 0.25), - median: d3.quantile(obj.results, 0.5), - q75: d3.quantile(obj.results, 0.75), - max: obj.domain[1], - range: obj.domain[1] - obj.domain[0] - }; - obj.stats.log10range = obj.stats.range > 0 ? Math.log10(obj.stats.range) : NaN; - obj.stats.iqr = obj.stats.q75 - obj.stats.q25; - - //Calculate bin width and number of bins. - obj.stats.calculatedBinWidth = (2 * obj.stats.iqr) / Math.pow(obj.stats.n, 1.0 / 3.0); // https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule - obj.stats.calculatedBins = - obj.stats.calculatedBinWidth > 0 - ? Math.ceil(obj.stats.range / obj.stats.calculatedBinWidth) - : NaN; - obj.stats.nBins = - obj.stats.calculatedBins < obj.stats.nUnique - ? obj.stats.calculatedBins - : obj.stats.nUnique; - obj.stats.binWidth = obj.stats.range / obj.nBins; + this.config.x.label = this.measure.current; + if (this.measure.current !== this.measure.previous) this.config.x.custom_bin = false; } function defineMeasureData() { @@ -1184,7 +1240,6 @@ return d.sh_measure === _this.measure.current; }) }; - calculateStatistics.call(this, this.measure.raw); //Apply other filters to measure data. this.measure.filtered = { @@ -1199,22 +1254,274 @@ : filter.val === d[filter.col]; }); }); - calculateStatistics.call(this, this.measure.filtered); - //Update chart config and set chart data to measure data. - this.config.x.bin = this.measure.filtered.stats.nBins; - this.raw_data = this.measure.raw.data.slice(); + //Filter results on current x-domain. + if (this.measure.current !== this.measure.previous) + this.config.x.domain = d3.extent( + this.measure.raw.data.map(function(d) { + return +d[_this.config.value_col]; + }) + ); + this.measure.custom = { + data: this.measure.raw.data.filter(function(d) { + return ( + _this.config.x.domain[0] <= +d[_this.config.value_col] && + +d[_this.config.value_col] <= _this.config.x.domain[1] + ); + }) + }; + + //Define arrays of results, unique results, and extent of results. + ['raw', 'custom', 'filtered'].forEach(function(property) { + var obj = _this.measure[property]; + + //Define array of all and unique results. + obj.results = obj.data + .map(function(d) { + return +d[_this.config.value_col]; + }) + .sort(function(a, b) { + return a - b; + }); + obj.uniqueResults = d3.set(obj.results).values(); + + //Calculate extent of data. + obj.domain = property !== 'custom' ? d3.extent(obj.results) : _this.config.x.domain; + }); } function setXdomain() { if (this.measure.current !== this.measure.previous) this.config.x.domain = this.measure.raw.domain; else if (this.config.x.domain[0] > this.config.x.domain[1]) this.config.x.domain.reverse(); + + //The x-domain can be in three states: + //- the extent of all results + //- user-defined, e.g. narrower to exclude outliers + // + //Bin width is calculated with two variables: + //- the interquartile range + //- the number of results + // + //1 When the x-domain is set to the extent of all results, the bin width should be calculated + // with the unfiltered set of results, regardless of the state of the current filters. + // + //2 Given a user-defined x-domain, the bin width should be calculated with the results that + // fall inside the current domain. + this.measure.domain_state = + (this.config.x.domain[0] === this.measure.raw.domain[0] && + this.config.x.domain[1] === this.measure.raw.domain[1]) || + this.measure.previous === undefined + ? 'raw' + : 'custom'; + + //Set chart data to measure data. + this.raw_data = this.measure[this.measure.domain_state].data.slice(); + } + + function calculateStatistics(obj) { + var _this = this; + + ['raw', 'custom'].forEach(function(property) { + var obj = _this.measure[property]; + + //Calculate statistics. + obj.stats = { + n: obj.results.length, + nUnique: obj.uniqueResults.length, + min: obj.domain[0], + q25: d3.quantile(obj.results, 0.25), + median: d3.quantile(obj.results, 0.5), + q75: d3.quantile(obj.results, 0.75), + max: obj.domain[1], + range: obj.domain[1] - obj.domain[0], + std: d3.deviation(obj.results) + }; + obj.stats.log10range = obj.stats.range > 0 ? Math.log10(obj.stats.range) : NaN; + obj.stats.iqr = obj.stats.q75 - obj.stats.q25; + }); + } + + function calculateSquareRootBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Square-root_choice + var range = this.config.x.domain[1] - this.config.x.domain[0]; + obj.stats.SquareRootBins = Math.ceil(Math.sqrt(obj.stats.n)); + obj.stats.SquareRootBinWidth = range / obj.stats.SquareRootBins; + } + + function calculateSturgesBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Sturges'_formula + var range = this.config.x.domain[1] - this.config.x.domain[0]; + obj.stats.SturgesBins = Math.ceil(Math.log2(obj.stats.n)) + 1; + obj.stats.SturgesBinWidth = range / obj.stats.SturgesBins; + } + + function calculateRiceBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Rice_Rule + var range = this.config.x.domain[1] - this.config.x.domain[0]; + obj.stats.RiceBins = Math.ceil(2 * Math.pow(obj.stats.n, 1.0 / 3.0)); + obj.stats.RiceBinWidth = range / obj.stats.RiceBins; + } + + function calculateScottBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Scott's_normal_reference_rule + var range = this.config.x.domain[1] - this.config.x.domain[0]; + obj.stats.ScottBinWidth = (3.5 * obj.stats.std) / Math.pow(obj.stats.n, 1.0 / 3.0); + obj.stats.ScottBins = + obj.stats.ScottBinWidth > 0 + ? Math.max(Math.ceil(range / obj.stats.ScottBinWidth), 5) + : NaN; + } + + function calculateFDBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Freedman%E2%80%93Diaconis'_choice + var range = this.config.x.domain[1] - this.config.x.domain[0]; + obj.stats.FDBinWidth = (2 * obj.stats.iqr) / Math.pow(obj.stats.n, 1.0 / 3.0); + obj.stats.FDBins = + obj.stats.FDBinWidth > 0 ? Math.max(Math.ceil(range / obj.stats.FDBinWidth), 5) : NaN; + } + + var toConsumableArray = function(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } else { + return Array.from(arr); + } + }; + + function calculateSSBinWidth(obj) { + //https://en.wikipedia.org/wiki/Histogram#Shimazaki_and_Shinomoto's_choice + var nBins = d3.range(2, 100); // number of bins + var cost = d3.range(nBins.length); // cost function results + var binWidths = [].concat(toConsumableArray(cost)); // bin widths + var binBoundaries = [].concat(toConsumableArray(cost)); // bin boundaries + var bins = [].concat(toConsumableArray(cost)); // bins + var binSizes = [].concat(toConsumableArray(cost)); // bin lengths + var meanBinSizes = [].concat(toConsumableArray(cost)); // mean of bin lengths + var residuals = [].concat(toConsumableArray(cost)); // residuals + + var _loop = function _loop(i) { + binWidths[i] = obj.stats.range / nBins[i]; + binBoundaries[i] = d3.range(obj.stats.min, obj.stats.max, obj.stats.range / nBins[i]); + bins[i] = d3.layout.histogram().bins(nBins[i] - 1)( + /*.bins(binBoundaries[i])*/ obj.results + ); + binSizes[i] = bins[i].map(function(arr) { + return arr.length; + }); + meanBinSizes[i] = d3.mean(binSizes[i]); + residuals[i] = + d3.sum( + binSizes[i].map(function(binSize) { + return Math.pow(binSize - meanBinSizes[i], 2); + }) + ) / nBins[i]; + cost[i] = (2 * meanBinSizes[i] - residuals[i]) / Math.pow(binWidths[i], 2); + }; + + for (var i = 0; i < nBins.length; i++) { + _loop(i); + } + + //consoleLogVars( + // { + // nBins, + // binWidths, + // binBoundaries, + // //bins, + // binSizes, + // meanBinSizes, + // residuals, + // cost + // }, + // 5 + //); + + var minCost = d3.min(cost); + var idx = cost.findIndex(function(c) { + return c === minCost; + }); + + obj.stats.SSBinWidth = binWidths[idx]; + obj.stats.SSBins = nBins[idx]; + //const optBinBoundaries = range(obj.stats.min, obj.stats.max, obj.stats.range/optNBins); + } + + function calcualteBinWidth() { + var _this = this; + + ['raw', 'custom'].forEach(function(property) { + var obj = _this.measure[property]; + + //Calculate bin width with the selected algorithm. + switch (_this.config.x.bin_algorithm) { + case 'Square-root choice': + calculateSquareRootBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.SquareRootBins < obj.stats.nUnique + ? obj.stats.SquareRootBins + : obj.stats.nUnique; + break; + case "Sturges' formula": + calculateSturgesBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.SturgesBins < obj.stats.nUnique + ? obj.stats.SturgesBins + : obj.stats.nUnique; + break; + case 'Rice Rule': + calculateRiceBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.RiceBins < obj.stats.nUnique + ? obj.stats.RiceBins + : obj.stats.nUnique; + break; + //case 'Doane\'s formula': + // console.log(4); + // calculateDoaneBinWidth.call(this, obj); + // obj.stats.nBins = + // obj.stats.DoaneBins < obj.stats.nUnique ? obj.stats.DoaneBins : obj.stats.nUnique; + // break; + case "Scott's normal reference rule": + calculateScottBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.ScottBins < obj.stats.nUnique + ? obj.stats.ScottBins + : obj.stats.nUnique; + break; + case "Freedman-Diaconis' choice": + calculateFDBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.FDBins < obj.stats.nUnique ? obj.stats.FDBins : obj.stats.nUnique; + break; + case "Shimazaki and Shinomoto's choice": + calculateSSBinWidth.call(_this, obj); + obj.stats.nBins = + obj.stats.SSBins < obj.stats.nUnique ? obj.stats.SSBins : obj.stats.nUnique; + break; + default: + //Handle custom number of bins. + obj.stats.nBins = _this.config.x.bin; + //obj.stats.binWidth = this.config.x.domain[1] - this.config.x.domain[0] / this.config.x.bin; + } + + //Calculate bin width. + obj.stats.binWidth = obj.stats.range / obj.stats.nBins; + obj.stats.binBoundaries = d3.range(obj.stats.nBins).concat(obj.domain[1]); + }); + + //Update chart config and set chart data to measure data. + this.config.x.bin = this.measure[this.measure.domain_state].stats.nBins; + this.config.x.bin_width = this.measure[this.measure.domain_state].stats.binWidth; } function calculateXPrecision() { //define the precision of the x-axis - this.config.x.precisionFactor = Math.round(this.measure.raw.stats.log10range); + this.config.x.precisionFactor = Math.round( + this.measure[this.measure.domain_state].stats.log10range + ); this.config.x.precision = Math.pow(10, this.config.x.precisionFactor); //x-axis format @@ -1233,10 +1540,10 @@ //define the size of the x-axis limit increments var step = - this.measure.raw.stats.range > 0 - ? Math.abs(this.measure.raw.stats.range / 15) // non-zero range - : this.measure.raw.results[0] !== 0 - ? Math.abs(this.measure.raw.results[0] / 15) // zero range, non-zero result(s) + this.measure[this.measure.domain_state].stats.range > 0 + ? Math.abs(this.measure[this.measure.domain_state].stats.range / 15) // non-zero range + : this.measure[this.measure.domain_state].results[0] !== 0 + ? Math.abs(this.measure[this.measure.domain_state].results[0] / 15) // zero range, non-zero result(s) : 1; // zero range, zero result(s) if (step < 1) { var x10 = 0; @@ -1249,60 +1556,102 @@ this.measure.step = step || 1; } - function setYaxisLabel() { - this.config.x.label = this.measure.current; + function updateXAxisResetButton() { + //Update tooltip of x-axis domain reset button. + if (this.measure.current !== this.measure.previous) { + this.controls.reset.container.attr( + 'title', + 'Initial Limits: [' + + this.config.x.d3format1(this.config.x.domain[0]) + + ' - ' + + this.config.x.d3format1(this.config.x.domain[1]) + + ']' + ); + } } - function updateXaxisLimitControls() { + function updateXAxisLimits() { this.controls.wrap .selectAll('#lower input') .attr('step', this.measure.step) // set in ./calculateXPrecision .style('box-shadow', 'none') - .property('value', this.config.x.domain[0]); + .property('value', this.config.x.d3format1(this.config.x.domain[0])); this.controls.wrap .selectAll('#upper input') .attr('step', this.measure.step) // set in ./calculateXPrecision .style('box-shadow', 'none') - .property('value', this.config.x.domain[1]); + .property('value', this.config.x.d3format1(this.config.x.domain[1])); } - function updateXaxisResetButton() { - //Update tooltip of x-axis domain reset button. - if (this.currentMeasure !== this.previousMeasure) - this.controls.wrap - .selectAll('.x-axis') - .property( - 'title', - 'Initial Limits: [' + - this.config.x.domain[0] + - ' - ' + - this.config.x.domain[1] + - ']' - ); + function updateBinAlogrithm() { + this.controls.Algorithm.selectAll('.algorithm-explanation') + .style('display', this.config.x.bin_algorithm !== 'Custom' ? null : 'none') + .attr( + 'title', + this.config.x.bin_algorithm !== 'Custom' + ? 'View information on ' + this.config.x.bin_algorithm + : null + ); + } + + function updateBinWidth() { + this.controls.Width.selectAll('input').property( + 'value', + this.config.x.d3format1(this.config.x.bin_width) + ); + } + + function updateBinQuantity() { + this.controls.Quantity.selectAll('input').property('value', this.config.x.bin); + } + + function updateControls() { + updateXAxisResetButton.call(this); + updateXAxisLimits.call(this); + updateBinAlogrithm.call(this); + updateBinWidth.call(this); + updateBinQuantity.call(this); + } + + function defineBinBoundaries() { + var _this = this; + + var obj = this.measure[this.measure.domain_state]; + this.measure.binBoundaries = obj.stats.binBoundaries.map(function(d, i) { + var value = obj.domain[0] + obj.stats.binWidth * i; + return { + value: value, + value1: _this.config.x.d3format(value), + value2: _this.config.x.d3format1(value) + }; + }); } function onPreprocess() { - // 1. Capture currently selected measure. + // 1. Capture currently selected measure - needed in 2a. getCurrentMeasure.call(this); - // 2. Filter data on currently selected measure. + // 2. Filter data on currently selected measure - needed in 3a and 3b. defineMeasureData.call(this); - // 3a Set x-domain given currently selected measure. + // 3a Set x-domain given currently selected measure - needed in 4a and 4b. setXdomain.call(this); - // 3b Define precision of measure. + // 3b Calculate statistics - needed in 4a and 4b. + calculateStatistics.call(this); + + // 4a Define precision of measure - needed in step 5a and 5b. calculateXPrecision.call(this); - // 3c Set x-axis label to current measure. - setYaxisLabel.call(this); + // 4b Calculate bin width - needed in step 5c. + calcualteBinWidth.call(this); - // 4a Update x-axis reset button when measure changes. - updateXaxisResetButton.call(this); + // 5a Update x-axis and bin controls after. + updateControls.call(this); - // 4b Update x-axis limit controls to match x-axis domain. - updateXaxisLimitControls.call(this); + // 5b Define bin boundaries given bin width and precision. + defineBinBoundaries.call(this); } function onDatatransform() {} @@ -1341,6 +1690,9 @@ delete this.highlightedBin; delete this.highlighteD; + //Remove bin boundaries. + this.svg.select('g.bin-boundaries').remove(); + //Reset bar highlighting. this.svg .selectAll('.bar-group') @@ -1359,31 +1711,55 @@ //Reset listing. this.listing.draw([]); - this.listing.wrap.selectAll('*').style('display', 'none'); + this.listing.wrap.style('display', 'none'); + } + + function increasePrecision() { + var _this = this; + + var ticks = this.x.ticks().map(function(d) { + return _this.config.x.d3format(d); + }); + if ( + d3 + .nest() + .key(function(d) { + return d; + }) + .rollup(function(d) { + return d.length; + }) + .entries(ticks) + .some(function(d) { + return d.values > 1; + }) + ) + this.config.x.format = this.config.x.format1; } function onDraw() { updateParticipantCount.call(this); resetRenderer.call(this); + increasePrecision.call(this); } function drawZeroRangeBar() { var _this = this; - if (this.current_data.length === 1) { + if ( + this.current_data.length === 1 && + this.measure.filtered.domain[0] === this.measure.filtered.domain[1] + ) { + var width = this.plot_width / 25; this.svg .selectAll('g.bar-group rect') .transition() .delay(250) // wait for initial marks to transition .attr({ x: function x(d) { - return d.values.x !== 0 ? _this.x(d.values.x * 0.999) : _this.x(-0.1); + return _this.x(d.values.x) - width / 2; }, - width: function width(d) { - return d.values.x !== 0 - ? _this.x(d.values.x * 1.001) - _this.x(d.values.x * 0.999) - : _this.x(0.1) - _this.x(-0.1); - } + width: width }); } } @@ -1410,22 +1786,24 @@ stroke: 'black', 'stroke-opacity': 0 }); + d.footnote = + "" + + d.values.raw.length + + ' records with ' + + (context.measure.current + " values ≥") + + (context.config.x.d3format1(d.rangeLow) + + ' and ' + + (d.rangeHigh < context.config.x.domain[1] ? '<' : '≤') + + "" + + context.config.x.d3format1(d.rangeHigh) + + ''); }); } function mouseout(element, d) { //Update footnote. - this.footnotes.barDetails.text( - this.highlightedBin - ? 'Table displays ' + - this.highlighteD.values.raw.length + - ' records with ' + - (this.measure.current + ' values from ') + - (this.config.x.d3format1(this.highlighteD.rangeLow) + - ' to ' + - this.config.x.d3format1(this.highlighteD.rangeHigh) + - '.') - : '' + this.footnotes.barDetails.html( + this.highlightedBin ? 'Table displays ' + this.highlighteD.footnote + '.' : '' ); //Remove bar highlight. @@ -1434,19 +1812,12 @@ } function mouseover(element, d) { - //Update footnote. - this.footnotes.barDetails.text( - d.values.raw.length + - ' records with ' + - (this.measure.current + ' values from ') + - (this.config.x.d3format1(d.rangeLow) + - ' to ' + - this.config.x.d3format1(d.rangeHigh)) - ); + //Update bar details footnote. + this.footnotes.barDetails.html('Bar encompasses ' + d.footnote + '.'); //Highlight bar. var selection = d3.select(element); - selection.moveToFront(); + if (!/trident/i.test(navigator.userAgent)) selection.moveToFront(); selection.selectAll('.bar').attr('stroke', 'black'); } @@ -1473,28 +1844,19 @@ resetRenderer.call(_this); }); - //Update bar details footnotes. - this.footnotes.barDetails.text( - 'Table displays ' + - d.values.raw.length + - ' records with ' + - (this.measure.current + ' values from ') + - (this.config.x.d3format1(d.rangeLow) + - ' to ' + - this.config.x.d3format1(d.rangeHigh) + - '.') - ); + //Update bar details footnote. + this.footnotes.barDetails.html('Table displays ' + d.footnote + '.'); //Draw listing. this.listing.draw(d.values.raw); - this.listing.wrap.selectAll('*').style('display', null); + this.listing.wrap.style('display', 'inline-block'); } function deselect(element, d) { delete this.highlightedBin; delete this.highlighteD; this.listing.draw([]); - this.listing.wrap.selectAll('*').style('display', 'none'); + this.listing.wrap.style('display', 'none'); this.svg.selectAll('.bar').attr('fill-opacity', 0.75); this.footnotes.barClick @@ -1548,7 +1910,7 @@ this.controls.wrap.select('.normal-range-list').remove(); this.svg.select('.normal-ranges').remove(); - if (this.config.displayNormalRange) { + if (this.config.displayNormalRange && this.filtered_data.length > 0) { //Capture distinct normal ranges in filtered data. var normalRanges = d3 .nest() @@ -1574,13 +1936,14 @@ d.width = d.x2 - d.x1; //tooltip + d.rate = d.values / _this.filtered_data.length; d.tooltip = d.values < _this.filtered_data.length ? d.lower + ' - ' + d.upper + ' (' + - d3.format('%')(d.values / _this.filtered_data.length) + + d3.format('%')(d.rate) + ' of records)' : d.lower + ' - ' + d.upper; @@ -1595,15 +1958,9 @@ return d; }) .sort(function(a, b) { - return a.lower <= b.lower && a.upper >= b.upper - ? 1 // lesser minimum and greater maximum - : a.lower >= b.lower && a.upper <= b.upper - ? -1 // greater minimum and lesser maximum - : a.lower <= b.lower && a.upper <= b.upper - ? 1 // lesser minimum and lesser maximum - : a.lower >= b.lower && a.upper >= b.upper - ? -1 // greater minimum and greater maximum - : 1; + var diff_lower = a.lower - b.lower; + var diff_upper = a.upper - b.upper; + return diff_lower ? diff_lower : diff_upper ? diff_upper : 0; }); // sort normal ranges so larger normal ranges plot beneath smaller normal ranges //Add tooltip to Normal Range control that lists normal ranges. @@ -1651,16 +2008,14 @@ width: function width(d) { return d.width; }, - height: this.plot_height - }) - .style({ - stroke: 'black', - fill: 'black', + height: this.plot_height, + stroke: '#c26683', + fill: '#c26683', 'stroke-opacity': function strokeOpacity(d) { - return (d.values / _this.filtered_data.length) * 0.75; + return (d.values / _this.filtered_data.length) * 0.5; }, 'fill-opacity': function fillOpacity(d) { - return (d.values / _this.filtered_data.length) * 0.5; + return (d.values / _this.filtered_data.length) * 0.25; } }); // opacity as a function of fraction of records with the given normal range } @@ -1679,71 +2034,72 @@ } function removeXAxisTicks() { - this.svg.selectAll('.x.axis .tick').remove(); + if (this.config.annotate_bin_boundaries) this.svg.selectAll('.x.axis .tick').remove(); } function annotateBinBoundaries() { var _this = this; - //Remove bin boundaries. - this.svg.select('g.bin-boundaries').remove(); + if (this.config.annotate_bin_boundaries) { + //Remove bin boundaries. + this.svg.select('g.bin-boundaries').remove(); - //Define set of bin boundaries. - var binBoundaries = d3 - .set( - d3.merge( - this.current_data.map(function(d) { - return [d.rangeLow, d.rangeHigh]; - }) - ) - ) - .values() - .map(function(value) { - return { - value: +value, - value1: _this.config.x.d3format(value), - value2: _this.config.x.d3format1(value) - }; - }) - .sort(function(a, b) { - return a.value - b.value; - }); + //Check for repeats of values formatted with lower precision. + var repeats = d3 + .nest() + .key(function(d) { + return d.value1; + }) + .rollup(function(d) { + return d.length; + }) + .entries(this.measure.binBoundaries) + .some(function(d) { + return d.values > 1; + }); - //Check for repeats of values formatted with lower precision. - var repeats = d3 - .nest() - .key(function(d) { - return d.value1; - }) - .rollup(function(d) { - return d.length; - }) - .entries(binBoundaries) - .some(function(d) { - return d.values > 1; - }); + //Annotate bin boundaries. + var axis = this.svg.append('g').classed('bin-boundaries axis', true); + var ticks = axis + .selectAll('g.bin-boundary') + .data(this.measure.binBoundaries) + .enter() + .append('g') + .classed('bin-boundary tick', true); + var texts = ticks + .append('text') + .attr({ + x: function x(d) { + return _this.x(d.value); + }, + y: this.plot_height, + dy: '16px', + 'text-anchor': 'middle' + }) + .text(function(d) { + return repeats ? d.value2 : d.value1; + }); - //Annotate bin boundaries. - var axis = this.svg.append('g').classed('bin-boundaries axis', true); - var ticks = axis - .selectAll('g.bin-boundary') - .data(binBoundaries) - .enter() - .append('g') - .classed('bin-boundary tick', true); - var texts = ticks - .append('text') - .attr({ - x: function x(d) { - return _this.x(d.value); - }, - y: this.y(0), - dy: '16px', - 'text-anchor': 'middle' - }) - .text(function(d) { - return repeats ? d.value2 : d.value1; + //Thin ticks. + var textDimensions = []; + texts.each(function(d) { + var text = d3.select(this); + var bbox = this.getBBox(); + if ( + textDimensions.some(function(textDimension) { + return textDimension.x + textDimension.width > bbox.x - 5; + }) + ) + text.remove(); + else + textDimensions.push({ + x: bbox.x, + width: bbox.width, + y: bbox.y, + height: bbox.height + }); }); + } } function onResize() { @@ -1831,7 +2187,13 @@ //Initialize listing and hide initially. chart.listing.init([]); - chart.listing.wrap.selectAll('*').style('display', 'none'); + chart.listing.wrap.style('display', 'none'); + chart.listing.wrap.selectAll('.table-top,table,.table-bottom').style({ + float: 'left', + clear: 'left', + width: '100%' + }); + chart.listing.table.style('white-space', 'nowrap'); return chart; } diff --git a/inst/htmlwidgets/lib/webcharts-1.11.5/webcharts.css b/inst/htmlwidgets/lib/webcharts-1.11.6/webcharts.css similarity index 100% rename from inst/htmlwidgets/lib/webcharts-1.11.5/webcharts.css rename to inst/htmlwidgets/lib/webcharts-1.11.6/webcharts.css diff --git a/inst/htmlwidgets/lib/webcharts-1.11.5/webcharts.js b/inst/htmlwidgets/lib/webcharts-1.11.6/webcharts.js similarity index 99% rename from inst/htmlwidgets/lib/webcharts-1.11.5/webcharts.js rename to inst/htmlwidgets/lib/webcharts-1.11.6/webcharts.js index 24ad10ef..b624c116 100644 --- a/inst/htmlwidgets/lib/webcharts-1.11.5/webcharts.js +++ b/inst/htmlwidgets/lib/webcharts-1.11.6/webcharts.js @@ -6,7 +6,7 @@ : (global.webCharts = factory(global.d3)); })(typeof self !== 'undefined' ? self : this, function(d3) { 'use strict'; - var version = '1.11.5'; + var version = '1.11.6'; function init(data) { var _this = this; @@ -542,24 +542,7 @@ y_dom: [] }; - _this.marks[i] = { - id: mark.id, - type: mark.type, - per: mark.per, - data: mark_info.data, - x_dom: mark_info.x_dom, - y_dom: mark_info.y_dom, - split: mark.split, - text: mark.text, - arrange: mark.arrange, - order: mark.order, - summarizeX: mark.summarizeX, - summarizeY: mark.summarizeY, - tooltip: mark.tooltip, - radius: mark.radius, - attributes: mark.attributes, - values: mark.values - }; + _this.marks[i] = Object.assign({}, mark, mark_info); }); //Set domains given extents of summarized mark data. @@ -747,23 +730,25 @@ (this.config.y.type === 'linear' && this.config.y.bin) ) { var xy = this.config.x.type === 'linear' && this.config.x.bin ? 'x' : 'y'; - var quant = d3.scale + mark.quant = d3.scale .quantile() .domain( - d3.extent( - entries.map(function(m) { - return +m[_this.config[xy].column]; - }) - ) + this.config[xy].domain + ? this.config[xy].domain + : d3.extent( + entries.map(function(m) { + return +m[_this.config[xy].column]; + }) + ) ) .range(d3.range(+this.config[xy].bin)); entries.forEach(function(e) { - return (e.wc_bin = quant(e[_this.config[xy].column])); + return (e.wc_bin = mark.quant(e[_this.config[xy].column])); }); this_nest.key(function(d) { - return quant.invertExtent(d.wc_bin); + return mark.quant.invertExtent(d.wc_bin); }); } else { this_nest.key(function(d) { From 287270570fef553651186e559d86bef1bbef7a60 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Fri, 14 Jun 2019 10:51:14 -0700 Subject: [PATCH 38/39] update news. fix spelling typos --- NEWS.md | 6 +++++- R/detectStandard.R | 2 +- R/generateSettings.R | 5 ++--- README.md | 2 +- man/detectStandard.Rd | 2 +- man/generateSettings.Rd | 6 ++---- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/NEWS.md b/NEWS.md index e6d01ee1..37f90ff5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ +# safetyGraphics v1.0.0 + +The first production release of safetyGraphics includes many inprovements including the addition of 5 new interactive graphics and an embedded help page with a detailed clinical workflow for using the tool. + # safetyGraphics v0.7.3 Initial CRAN release for safetyGraphics. The safetyGraphics package provides framework for evaluation of clinical trial safety. Users can interactively explore their data using the 'Shiny' application or create standalone 'htmlwidget' charts. Interactive charts are built using 'd3.js' and 'webcharts.js' 'JavaScript' libraries. -See the [github release tracker](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/releases) for additional release documentation and links to issues. \ No newline at end of file +See the [GitHub release tracker](https://github.com/ASA-DIA-InteractiveSafetyGraphics/safetyGraphics/releases) for additional release documentation and links to issues. \ No newline at end of file diff --git a/R/detectStandard.R b/R/detectStandard.R index da4a806c..29c15bc9 100644 --- a/R/detectStandard.R +++ b/R/detectStandard.R @@ -2,7 +2,7 @@ #' #' This function attempts to detect the clinical data standard used in a given R data frame. #' -#' This function compares the columns in the provided \code{"data"} with the required columns for a given data standard/domain combination. The function is designed to work with the SDTM and AdAM CDISC() standards for clinical trial data by default. Additional standards can be added by modifying the \code{"standardMetadata"} data set included as part of this package. Currently, "labs" is the only domain supported. +#' This function compares the columns in the provided \code{"data"} with the required columns for a given data standard/domain combination. The function is designed to work with the SDTM and ADaM CDISC() standards for clinical trial data by default. Additional standards can be added by modifying the \code{"standardMetadata"} data set included as part of this package. Currently, "labs" is the only domain supported. #' #' @param data A data frame in which to detect the data standard #' @param includeFields specifies whether to check the data set for field level data in addition to columns. Default: \code{TRUE}. diff --git a/R/generateSettings.R b/R/generateSettings.R index a3f30fad..e1bb1955 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -2,14 +2,13 @@ #' #' This function returns a settings object for the eDish chart based on the specified data standard. #' -#' The function is designed to work with the SDTM and AdAM CDISC() standards for clinical trial data. Currently, eDish is the only chart supported. +#' The function is designed to work with the SDTM and ADaM CDISC() standards for clinical trial data. Currently, eDish is the only chart supported. #' -#' @param standard The data standard for which to create settings. Valid options are "SDTM", "AdAM" or "None". Default: \code{"None"}. +#' @param standard The data standard for which to create settings. Valid options are "sdtm", "adam" or "none". Default: \code{"None"}. #' @param charts The chart or charts for which settings should be generated. Default: \code{NULL} (uses all available charts). #' @param useDefaults Specifies whether default values from settingsMetadata should be included in the settings object. Default: \code{TRUE}. #' @param partial Boolean for whether or not the standard is a partial standard. Default: \code{FALSE}. #' @param partial_keys Optional character vector of the matched settings if partial is TRUE. Settings should be identified using the text_key format described in ?settingsMetadata. Setting is ignored when partial is FALSE. Default: \code{NULL}. -#' @param custom_settings a tibble with text_key and customValue columns specifiying customizations to be applied to the settings object. Default: \code{NULL}. #' @return A list containing the appropriate settings for the selected chart #' #' @examples diff --git a/README.md b/README.md index 73f8b312..837963b9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # safetyGraphics: Clinical Trial Safety Graphics with R -The **safetyGraphics** package provides a framework for evaluation of clinical trial safety in R. It includes several safety-focused visualizations to empower clinical data monitoring. Chief among these is the Hepatic Explorer, based on the [Evaluation of the Drug-Induced Serious Hepatotoxicity (eDISH)](https://www.ncbi.nlm.nih.gov/pubmed/21332248) visualization. A demo of the Hepatic Explorer interactive graphic is available [here](https://safetygraphics.github.io/hep-explorer/test-page/example1/) and is shown below. +The **safetyGraphics** package provides a framework for evaluation of clinical trial safety in R. It includes several safety-focused visualizations to empower clinical data monitoring. Chief among these is the Hepatic Explorer, based on the [Evaluation of the Drug-Induced Serious Hepatotoxicity (eDish)](https://www.ncbi.nlm.nih.gov/pubmed/21332248) visualization. A demo of the Hepatic Explorer interactive graphic is available [here](https://safetygraphics.github.io/hep-explorer/test-page/example1/) and is shown below. This package is being built in conjunction with the [hep-explorer](https://github.com/SafetyGraphics/hep-explorer) javascript library. diff --git a/man/detectStandard.Rd b/man/detectStandard.Rd index ec561f35..096e77ec 100644 --- a/man/detectStandard.Rd +++ b/man/detectStandard.Rd @@ -20,7 +20,7 @@ A list containing the matching \code{"standard"} from \code{"standardMetadata"} This function attempts to detect the clinical data standard used in a given R data frame. } \details{ -This function compares the columns in the provided \code{"data"} with the required columns for a given data standard/domain combination. The function is designed to work with the SDTM and AdAM CDISC() standards for clinical trial data by default. Additional standards can be added by modifying the \code{"standardMetadata"} data set included as part of this package. Currently, "labs" is the only domain supported. +This function compares the columns in the provided \code{"data"} with the required columns for a given data standard/domain combination. The function is designed to work with the SDTM and ADaM CDISC() standards for clinical trial data by default. Additional standards can be added by modifying the \code{"standardMetadata"} data set included as part of this package. Currently, "labs" is the only domain supported. } \examples{ detectStandard(adlbc)[["standard"]] #adam diff --git a/man/generateSettings.Rd b/man/generateSettings.Rd index 751fe54b..1109d795 100644 --- a/man/generateSettings.Rd +++ b/man/generateSettings.Rd @@ -9,7 +9,7 @@ generateSettings(standard = "None", charts = NULL, custom_settings = NULL) } \arguments{ -\item{standard}{The data standard for which to create settings. Valid options are "SDTM", "AdAM" or "None". Default: \code{"None"}.} +\item{standard}{The data standard for which to create settings. Valid options are "sdtm", "adam" or "none". Default: \code{"None"}.} \item{charts}{The chart or charts for which settings should be generated. Default: \code{NULL} (uses all available charts).} @@ -18,8 +18,6 @@ generateSettings(standard = "None", charts = NULL, \item{partial}{Boolean for whether or not the standard is a partial standard. Default: \code{FALSE}.} \item{partial_keys}{Optional character vector of the matched settings if partial is TRUE. Settings should be identified using the text_key format described in ?settingsMetadata. Setting is ignored when partial is FALSE. Default: \code{NULL}.} - -\item{custom_settings}{a tibble with text_key and customValue columns specifiying customizations to be applied to the settings object. Default: \code{NULL}.} } \value{ A list containing the appropriate settings for the selected chart @@ -28,7 +26,7 @@ A list containing the appropriate settings for the selected chart This function returns a settings object for the eDish chart based on the specified data standard. } \details{ -The function is designed to work with the SDTM and AdAM CDISC() standards for clinical trial data. Currently, eDish is the only chart supported. +The function is designed to work with the SDTM and ADaM CDISC() standards for clinical trial data. Currently, eDish is the only chart supported. } \examples{ From 5ff55b4ce290a1849d81a342cd3f1cc40ec59700 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Fri, 14 Jun 2019 11:10:47 -0700 Subject: [PATCH 39/39] fix settings mismatch. update broken link --- R/adlbc.R | 2 +- R/generateSettings.R | 2 ++ man/adlbc.Rd | 2 +- man/generateSettings.Rd | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/R/adlbc.R b/R/adlbc.R index 8763cc08..8ad6aa26 100644 --- a/R/adlbc.R +++ b/R/adlbc.R @@ -1,6 +1,6 @@ #' Safety measures sample data #' -#' A dataset containing anonymized lab data from a clinical trial in the CDISC ADaM format. The structure is 1 record per measure per visit per participant. See a full description of the ADaM data standard \href{https://www.cdisc.org/sites/default/files/members/standard/foundational/adam/adam_implementation_guide_v1.0.pdf}{here}. +#' A dataset containing anonymized lab data from a clinical trial in the CDISC ADaM format. The structure is 1 record per measure per visit per participant. See a full description of the ADaM data standard \href{https://www.cdisc.org/standards/foundational/adam/adam-implementation-guide-v11}{here}. #' #' @format A data frame with 10288 rows and 46 variables. #' \describe{ diff --git a/R/generateSettings.R b/R/generateSettings.R index e1bb1955..58cf777e 100644 --- a/R/generateSettings.R +++ b/R/generateSettings.R @@ -9,6 +9,8 @@ #' @param useDefaults Specifies whether default values from settingsMetadata should be included in the settings object. Default: \code{TRUE}. #' @param partial Boolean for whether or not the standard is a partial standard. Default: \code{FALSE}. #' @param partial_keys Optional character vector of the matched settings if partial is TRUE. Settings should be identified using the text_key format described in ?settingsMetadata. Setting is ignored when partial is FALSE. Default: \code{NULL}. +#' @param custom_settings A tibble describing custom settings to be added to the settings object. Custom values overwrite default values when provided. Tibble should have text_key and customValue columns. Default: \code{NULL}. +#' #' @return A list containing the appropriate settings for the selected chart #' #' @examples diff --git a/man/adlbc.Rd b/man/adlbc.Rd index 3a75f8cc..e3af4b57 100644 --- a/man/adlbc.Rd +++ b/man/adlbc.Rd @@ -60,6 +60,6 @@ adlbc } \description{ -A dataset containing anonymized lab data from a clinical trial in the CDISC ADaM format. The structure is 1 record per measure per visit per participant. See a full description of the ADaM data standard \href{https://www.cdisc.org/sites/default/files/members/standard/foundational/adam/adam_implementation_guide_v1.0.pdf}{here}. +A dataset containing anonymized lab data from a clinical trial in the CDISC ADaM format. The structure is 1 record per measure per visit per participant. See a full description of the ADaM data standard \href{https://www.cdisc.org/standards/foundational/adam/adam-implementation-guide-v11}{here}. } \keyword{datasets} diff --git a/man/generateSettings.Rd b/man/generateSettings.Rd index 1109d795..29dc3596 100644 --- a/man/generateSettings.Rd +++ b/man/generateSettings.Rd @@ -18,6 +18,8 @@ generateSettings(standard = "None", charts = NULL, \item{partial}{Boolean for whether or not the standard is a partial standard. Default: \code{FALSE}.} \item{partial_keys}{Optional character vector of the matched settings if partial is TRUE. Settings should be identified using the text_key format described in ?settingsMetadata. Setting is ignored when partial is FALSE. Default: \code{NULL}.} + +\item{custom_settings}{A tibble describing custom settings to be added to the settings object. Custom values overwrite default values when provided. Tibble should have text_key and customValue columns. Default: \code{NULL}.} } \value{ A list containing the appropriate settings for the selected chart