Repository navigation
Fix duplicated x scale and expose cap width in PK line plots #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
a288a58
Fix duplicated x scale and expose cap width in PK line plots (#322)
Melkiades e40e2c6
Dodge PKC error bars and drop misleading factor-encouragement message
wzfrank 62d0bf7
Merge branch 'main' into 322_pkc_lineplot_x_scale_and_caps@main
wzfrank a9d87c4
Add gg_pkc_boxplot() for PK concentration box plots
wzfrank 70f8bd1
Fix spellcheck flag and R CMD check NOTE in gg_pkc_boxplot()
wzfrank b2071b7
Reword percentile whisker description to avoid ordinal suffix
wzfrank 1708f62
Sync generated Rd files with CI's roxygen2 output
wzfrank 069e46e
Add gg_pkc_boxplot to pkgdown reference index
wzfrank b36f05b
Add percentiles parameter and warn on log-scale filtering in gg_pkc_b…
wzfrank e168211
Add dodge_width to gg_pkc_lineplot() and improve gg_pkc_boxplot()
wzfrank 1011bd3
Fix gg_pkc_lineplot.Rd link target for CI's roxygen2 8.1.0
wzfrank 4263eaa
Use whisker-specific ARD stat names and add id_var for outlier tracea…
wzfrank 4c82ccb
Merge remote-tracking branch 'origin/main' into 322_pkc_lineplot_x_sc…
wzfrank 953e1d1
Use variability-specific ARD stat names in gg_pkc_lineplot
wzfrank f419f91
[skip ci] Add sd to WORDLIST
wzfrank 04f5700
[skip ci] Whitespace alignment cleanup
wzfrank 46adc14
Trigger CI
wzfrank 2067888
Use stats::sd() in ARD stat list to fix R CMD check NOTE
wzfrank File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| #' Plot Pharmacokinetic Concentrations as Box Plots | ||
| #' | ||
| #' @description | ||
| #' Creates a standard Pharmacokinetic (PK) concentration box plot, summarizing | ||
| #' the distribution of concentrations at each timepoint and treatment group. | ||
| #' This function wraps `ggplot2` calls to consistently format PK box plots, | ||
| #' handling log transformations, whisker definitions, and outlier detection. | ||
| #' | ||
| #' @param data (`data.frame`)\cr | ||
| #' The dataset containing PK data. | ||
| #' @param time_var ([`tidy-select`][dplyr::dplyr_tidy_select])\cr | ||
| #' The time variable (x-axis). | ||
| #' @param analyte_var ([`tidy-select`][dplyr::dplyr_tidy_select])\cr | ||
| #' The concentration/analyte variable (y-axis). | ||
| #' @param group ([`tidy-select`][dplyr::dplyr_tidy_select])\cr | ||
| #' The grouping/treatment variable. | ||
| #' @param whisker (`string`)\cr | ||
| #' Method used to compute the box whiskers: `"percentile"` (5th/95th | ||
| #' percentiles), `"tukey"` (1.5 * IQR beyond the hinges, the conventional | ||
| #' boxplot definition), or `"minmax"` (full data range, so no points are | ||
| #' flagged as outliers). Default is `"percentile"`. | ||
| #' @param quantile_type (`integer`)\cr | ||
| #' Quantile algorithm `type` passed to [stats::quantile()]. Default is `1`. | ||
| #' @param log_y (`logical`)\cr | ||
| #' Whether to apply log10 scale to the y-axis. Default is `TRUE`. | ||
| #' @param show_mean (`logical`)\cr | ||
| #' Whether to overlay a mean marker on each box. Default is `TRUE`. | ||
| #' @param outlier_size (`numeric`)\cr | ||
| #' Size of the outlier point markers. Default is `2`. | ||
| #' | ||
| #' @returns A `ggplot` object of class `crane_gg_pkc_box`. | ||
| #' @seealso [gg_pkc_lineplot()] for related functionalities. | ||
| #' | ||
| #' @examples | ||
| #' # Prepare PK Data using the built-in Theoph dataset | ||
| #' df_pk <- Theoph | ||
| #' df_pk$Time_Nominal <- round(df_pk$Time) | ||
| #' # Filter to specific timepoints to keep the plot clean | ||
| #' df_pk <- df_pk[df_pk$Time_Nominal %in% c(0, 2, 4, 8, 24), ] | ||
| #' # Create a mock treatment group based on Dose | ||
| #' df_pk$Dose_Group <- ifelse(df_pk$Dose > 4.5, "High Dose", "Low Dose") | ||
| #' | ||
| #' # Default Plot: percentile whiskers on a log10 y-axis | ||
| #' gg_pkc_boxplot( | ||
| #' data = df_pk, | ||
| #' time_var = Time_Nominal, | ||
| #' analyte_var = conc, | ||
| #' group = Dose_Group | ||
| #' ) | ||
| #' | ||
| #' # Tukey whiskers on a linear y-axis, without the mean marker | ||
| #' gg_pkc_boxplot( | ||
| #' data = df_pk, | ||
| #' time_var = Time_Nominal, | ||
| #' analyte_var = conc, | ||
| #' group = Dose_Group, | ||
| #' whisker = "tukey", | ||
| #' log_y = FALSE, | ||
| #' show_mean = FALSE | ||
| #' ) | ||
| #' | ||
| #' @export | ||
| gg_pkc_boxplot <- function( | ||
| data, | ||
| time_var, | ||
| analyte_var, | ||
| group, | ||
| whisker = c("percentile", "tukey", "minmax"), | ||
| quantile_type = 1, | ||
| log_y = TRUE, | ||
| show_mean = TRUE, | ||
| outlier_size = 2 | ||
| ) { | ||
| # Match standard arguments | ||
| whisker <- match.arg(whisker) | ||
|
|
||
| # Mandatory Arguments Validation | ||
| check_not_missing(data) | ||
| check_not_missing(time_var) | ||
| check_not_missing(analyte_var) | ||
| check_not_missing(group) | ||
| check_data_frame(data) | ||
|
|
||
| # Tidy-selection processing | ||
| cards::process_selectors( | ||
| data, | ||
| time_var = {{ time_var }}, | ||
| analyte_var = {{ analyte_var }}, | ||
| group = {{ group }} | ||
| ) | ||
|
|
||
| # Ensure only single columns were selected | ||
| check_string(time_var) | ||
| check_string(analyte_var) | ||
| check_string(group) | ||
|
|
||
| # A log10 y-axis cannot plot zero/negative values, so drop them up front | ||
| # rather than letting ggplot2 silently remove them layer by layer. | ||
| if (log_y) { | ||
| data <- data |> | ||
| dplyr::filter(!is.na(.data[[analyte_var]]), .data[[analyte_var]] > 0) | ||
| } | ||
|
wzfrank marked this conversation as resolved.
|
||
|
|
||
| # Computes the five summary values `geom = "boxplot"` expects (ymin, lower, | ||
| # middle, upper, ymax) for one numeric vector. The hinges (lower/middle/ | ||
| # upper) are always quartiles; only the whisker limits (ymin/ymax) change | ||
| # with the `whisker` method, so outlier detection and box drawing stay | ||
| # consistent with each other. | ||
| box_stats <- function(x) { | ||
|
wzfrank marked this conversation as resolved.
|
||
| x <- x[!is.na(x)] | ||
|
|
||
| if (length(x) == 0L) { | ||
| # No non-missing values for this timepoint/group: return NAs so | ||
| # ggplot2 drops the box instead of erroring. | ||
| return(c(ymin = NA_real_, lower = NA_real_, middle = NA_real_, | ||
| upper = NA_real_, ymax = NA_real_)) | ||
| } | ||
|
|
||
| # Box hinges: 25th/50th (median)/75th percentiles | ||
| q <- stats::quantile( | ||
| x, | ||
| probs = c(0.25, 0.50, 0.75), | ||
| type = quantile_type, | ||
| names = FALSE | ||
| ) | ||
|
|
||
| limits <- switch( | ||
| whisker, | ||
| # 5th/95th percentiles of the data | ||
| percentile = stats::quantile( | ||
| x, | ||
| probs = c(0.05, 0.95), | ||
| type = quantile_type, | ||
| names = FALSE | ||
| ), | ||
| # Full data range: no point can fall outside, so there are no outliers | ||
| minmax = range(x), | ||
| # Standard 1.5 * IQR rule, clipped to the observed data range | ||
| tukey = c( | ||
| max(min(x), q[[1]] - 1.5 * (q[[3]] - q[[1]])), | ||
| min(max(x), q[[3]] + 1.5 * (q[[3]] - q[[1]])) | ||
| ) | ||
| ) | ||
|
|
||
| c( | ||
| ymin = limits[[1]], | ||
| lower = q[[1]], | ||
| middle = q[[2]], | ||
| upper = q[[3]], | ||
| ymax = limits[[2]] | ||
| ) | ||
| } | ||
|
|
||
| # Flags values of x that fall outside the whisker limits computed by | ||
| # `box_stats()`, so they can be drawn as individual points instead of | ||
| # being absorbed into the whiskers. | ||
| is_outlier <- function(x) { | ||
| x <- x[!is.na(x)] | ||
|
|
||
| if (whisker == "minmax") { | ||
| return(rep(FALSE, length(x))) | ||
| } | ||
|
|
||
| limits <- box_stats(x)[c("ymin", "ymax")] | ||
| x < limits[[1]] | x > limits[[2]] | ||
| } | ||
|
|
||
| # Convert time_var to an ordered factor so every timepoint gets its own | ||
| # discrete x position and dodged group of boxes, regardless of whether the | ||
| # original time_var is numeric or character. | ||
| data <- data |> | ||
| dplyr::mutate( | ||
| .time = factor(.data[[time_var]], levels = sort(unique(.data[[time_var]]))) | ||
| ) | ||
|
|
||
| # Pre-compute, per timepoint and group, which observations are outliers. | ||
| # Non-outliers are set to NA so they are skipped by the outlier point layer | ||
| # below (`geom_point(na.rm = TRUE)`), which otherwise overlays every raw | ||
| # observation on top of the boxes. | ||
| outliers <- data |> | ||
| dplyr::group_by(.time, .data[[group]]) |> | ||
| dplyr::mutate(.outlier = dplyr::if_else( | ||
| is_outlier(.data[[analyte_var]]), | ||
| .data[[analyte_var]], | ||
| NA_real_ | ||
| )) |> | ||
| dplyr::ungroup() | ||
|
|
||
| # A single shared dodge width keeps the boxes, whisker caps, outlier points | ||
| # and mean marker aligned within the same timepoint. | ||
| dodge <- ggplot2::position_dodge(width = 0.8) | ||
|
|
||
| # Base Plot: draw the box (hinges + whiskers) via `box_stats()` instead of | ||
| # geom_boxplot()'s built-in stat, so the whisker definition can vary with | ||
| # `whisker`. | ||
| p <- ggplot2::ggplot( | ||
| data, | ||
| ggplot2::aes( | ||
| x = .data$.time, | ||
| y = .data[[analyte_var]], | ||
| fill = .data[[group]] | ||
| ) | ||
| ) + | ||
| ggplot2::stat_summary( | ||
| fun.data = box_stats, | ||
| geom = "boxplot", | ||
| position = dodge | ||
| ) + | ||
| # geom_boxplot() draws the whisker lines but no end caps; add a matching | ||
| # errorbar layer (same box_stats) purely to draw horizontal caps at | ||
| # ymin/ymax for readability. | ||
| ggplot2::stat_summary( | ||
| fun.data = box_stats, | ||
| geom = "errorbar", | ||
| width = 0.8, | ||
| position = dodge | ||
| ) + | ||
| # Overlay the pre-flagged outlier values on top of the boxes; na.rm drops | ||
| # the non-outlier rows that were set to NA above. | ||
| ggplot2::geom_point( | ||
| data = outliers, | ||
| ggplot2::aes(y = .data$.outlier), | ||
| shape = 1, | ||
| size = outlier_size, | ||
| position = dodge, | ||
| na.rm = TRUE, | ||
| show.legend = FALSE | ||
| ) + | ||
| ggplot2::theme_classic() + | ||
| ggplot2::theme( | ||
| legend.position = "bottom", | ||
| legend.title.position = "top", | ||
| legend.title.align = 0.5, | ||
| legend.background = ggplot2::element_rect( | ||
| fill = "white", color = "black", linewidth = 0.5 | ||
| ) | ||
| ) | ||
|
|
||
| # Optional mean marker, dodged the same way so it lines up with its group's | ||
| # box rather than sitting at the center of the timepoint. | ||
| if (show_mean) { | ||
| p <- p + | ||
| ggplot2::stat_summary( | ||
| fun = "mean", | ||
| geom = "point", | ||
| color = "black", | ||
| shape = 8, | ||
| size = 3, | ||
| position = dodge | ||
| ) | ||
| } | ||
|
|
||
| # Log Scale | ||
| if (log_y) { | ||
| p <- p + ggplot2::scale_y_log10() | ||
| } | ||
|
|
||
| class(p) <- c("crane_gg_pkc_box", class(p)) | ||
|
wzfrank marked this conversation as resolved.
|
||
| p | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.