1 |
#' Events by Term Plot
|
|
2 |
#'
|
|
3 |
#' This function plots commonly occurred events by number of unique subjects with events.
|
|
4 |
#' It creates basic summary of events and compares event occurrences between comparison
|
|
5 |
#' and reference arms, and can be used for events data such as Adverse Events.
|
|
6 |
#'
|
|
7 |
#' @inheritParams argument_convention
|
|
8 |
#' @param id (`vector`)\cr contains subject identifier. Length of \code{id} must be the
|
|
9 |
#' same as the length or number of rows of \code{terms}. Usually it is \code{ADAE$USUBJID}.
|
|
10 |
#' @param term \code{character} or \code{factor} vector, or \code{data.frame} \cr
|
|
11 |
#' Represents events information. \code{term} can be a \code{data.frame} produced
|
|
12 |
#' by \code{create_flag_vars}, with each column being a \code{logical} event indicator
|
|
13 |
#' @param arm_N (\code{numeric} vector)\cr
|
|
14 |
#' Contains information of the number of patients in the levels of \code{arm}. This is useful
|
|
15 |
#' if there are patients that have no adverse events can be accounted for with this argument.
|
|
16 |
#' @param ref \code{character} indicates the name of the reference arm. Default is the first
|
|
17 |
#' level of \code{arm}.
|
|
18 |
#' @param trt \code{character} indicates the name of the treatment arm. Default is the second
|
|
19 |
#' level of \code{arm}.
|
|
20 |
#' @param sort_by \code{character} indicates how each \code{term} is sorted in the plot.
|
|
21 |
#' Choose from `"term"` for alphabetic terms, `"riskdiff"` for risk difference, and `"meanrisk"`
|
|
22 |
#' for mean risk. Default is "term".
|
|
23 |
#' @param rate_range Numeric \code{vector} of length 2. Range for overall rate display
|
|
24 |
#' @param diff_range Numeric \code{vector} of length 2. Range for rate difference display
|
|
25 |
#' @param reversed \code{logical} whether to reverse the sorting by \code{sort_by}.
|
|
26 |
#' Default is FALSE.
|
|
27 |
#' @param axis_side \code{character} the side of the axis label, "left" or "right". Default is "left".
|
|
28 |
#' @param color Color for the plot. \code{vector} of length 2. Color for reference and
|
|
29 |
#' treatment arms respectively. Default set to \code{c("blue", "red")}.
|
|
30 |
#' @param shape Shape for the plot. \code{vector} of length 2. Shape for reference and
|
|
31 |
#' treatment arms respectively. Default set to \code{c(16, 17)} per
|
|
32 |
#' \code{\link[ggplot2]{scale_shape}}.
|
|
33 |
#' @details there is no equivalent STREAM output
|
|
34 |
#'
|
|
35 |
#' @return grob object
|
|
36 |
#'
|
|
37 |
#' @export
|
|
38 |
#'
|
|
39 |
#' @author Liming Li (Lil128) \email{liming.li@roche.com}
|
|
40 |
#' @author Molly He (hey59) \email{hey59@gene.com}
|
|
41 |
#'
|
|
42 |
#' @examplesIf require("nestcolor")
|
|
43 |
#' library(dplyr)
|
|
44 |
#' library(grid)
|
|
45 |
#' library(nestcolor)
|
|
46 |
#'
|
|
47 |
#' ADSL <- osprey::rADSL
|
|
48 |
#' ADAE <- osprey::rADAE
|
|
49 |
#'
|
|
50 |
#' # add additional dummy causality flags
|
|
51 |
#' ADAE <- ADAE %>%
|
|
52 |
#' mutate(AEREL1 = (AEREL == "Y" & ACTARM == "A: Drug X")) %>%
|
|
53 |
#' mutate(AEREL2 = (AEREL == "Y" & ACTARM == "B: Placebo"))
|
|
54 |
#' attr(ADAE[["AEREL1"]], "label") <- "AE related to A: Drug X"
|
|
55 |
#' attr(ADAE[["AEREL2"]], "label") <- "AE related to B: Placebo"
|
|
56 |
#'
|
|
57 |
#' term <- ADAE$AEDECOD
|
|
58 |
#' id <- ADAE$USUBJID
|
|
59 |
#' arm <- ADAE$ACTARMCD
|
|
60 |
#' arm_N <- table(ADSL$ACTARMCD)
|
|
61 |
#' ref <- "ARM A"
|
|
62 |
#' trt <- "ARM C"
|
|
63 |
#'
|
|
64 |
#' # Example 1
|
|
65 |
#' p1 <- g_events_term_id(
|
|
66 |
#' term,
|
|
67 |
#' id,
|
|
68 |
#' arm,
|
|
69 |
#' arm_N
|
|
70 |
#' )
|
|
71 |
#' grid::grid.newpage()
|
|
72 |
#' grid::grid.draw(p1)
|
|
73 |
#'
|
|
74 |
#' # Example 2
|
|
75 |
#' p2 <- g_events_term_id(
|
|
76 |
#' term,
|
|
77 |
#' id,
|
|
78 |
#' arm,
|
|
79 |
#' arm_N,
|
|
80 |
#' trt = trt,
|
|
81 |
#' ref = ref,
|
|
82 |
#' sort_by = "riskdiff",
|
|
83 |
#' diff_ci_method = "ac",
|
|
84 |
#' conf_level = 0.9
|
|
85 |
#' )
|
|
86 |
#' grid::grid.newpage()
|
|
87 |
#' grid::grid.draw(p2)
|
|
88 |
#'
|
|
89 |
#' # Example 3
|
|
90 |
#' p3 <- g_events_term_id(
|
|
91 |
#' term,
|
|
92 |
#' id,
|
|
93 |
#' arm,
|
|
94 |
#' arm_N,
|
|
95 |
#' sort_by = "meanrisk",
|
|
96 |
#' axis_side = "right",
|
|
97 |
#' fontsize = 5
|
|
98 |
#' )
|
|
99 |
#' grid::grid.newpage()
|
|
100 |
#' grid::grid.draw(p3)
|
|
101 |
#'
|
|
102 |
#' # Example 4
|
|
103 |
#' term <- create_flag_vars(ADAE)
|
|
104 |
#' g_events_term_id(
|
|
105 |
#' term,
|
|
106 |
#' id,
|
|
107 |
#' arm,
|
|
108 |
#' arm_N,
|
|
109 |
#' fontsize = 3
|
|
110 |
#' )
|
|
111 |
g_events_term_id <- function(term, |
|
112 |
id,
|
|
113 |
arm,
|
|
114 |
arm_N, # nolint |
|
115 |
ref = levels(arm)[1], |
|
116 |
trt = levels(arm)[2], |
|
117 |
sort_by = c("term", "riskdiff", "meanrisk"), |
|
118 |
rate_range = c(0, 1), |
|
119 |
diff_range = c(-1, 1), |
|
120 |
reversed = FALSE, |
|
121 |
conf_level = 0.95, |
|
122 |
diff_ci_method = |
|
123 |
c("wald", "waldcc", "ac", "score", "scorecc", "mn", "mee", "blj", "ha", "beal"), |
|
124 |
axis_side = c("left", "right"), |
|
125 |
color = c(getOption("ggplot2.discrete.colour"), "blue", "red")[1:2], |
|
126 |
shape = c(16, 17), |
|
127 |
fontsize = 4, |
|
128 |
draw = TRUE) { |
|
129 | ! |
if (is.data.frame(term)) { |
130 | ! |
term_levels <- factor(colnames(term), levels = rev(colnames(term))) |
131 | ! |
term <- data.frame(t(term)) |
132 | ! |
term <- lapply(term, function(x) { |
133 | ! |
term_levels[x] |
134 |
}) |
|
135 | ! |
df <- tibble::tibble(id, arm, term) %>% |
136 | ! |
tidyr::unnest(term) |
137 |
} else { |
|
138 | ! |
term_levels <- `if`(is.factor(term), levels(term), unique(term)) |
139 | ! |
df <- tibble::tibble(id, arm, term) |
140 |
}
|
|
141 | ||
142 |
# argument validation
|
|
143 | ! |
sort_by <- match.arg(sort_by) |
144 | ! |
diff_ci_method <- match.arg(diff_ci_method) |
145 | ! |
axis_side <- match.arg(axis_side) |
146 | ||
147 | ! |
checkmate::assert_factor(arm, min.levels = 2) |
148 | ! |
stopifnot( |
149 | ! |
"invalid arguments: check that the length of id, term and arm are identical" = |
150 | ! |
length(id) == length(term) && length(term) == length(arm) |
151 |
)
|
|
152 | ! |
stopifnot("invalid arguments: trt and ref need to be from arm" = all(c(trt, ref) %in% unique(arm))) |
153 | ! |
checkmate::assert_numeric(rate_range, len = 2, any.missing = FALSE) |
154 | ! |
checkmate::assert_numeric(diff_range, len = 2, any.missing = FALSE) |
155 | ! |
checkmate::assert_flag(reversed) |
156 | ! |
checkmate::assert_number(conf_level, lower = 0.5, upper = 1) |
157 | ! |
checkmate::assert_character(color, len = 2, any.missing = FALSE) |
158 | ! |
checkmate::assert_numeric(shape, len = 2, any.missing = FALSE) |
159 | ! |
checkmate::assert_numeric(fontsize, lower = 0, any.missing = FALSE) |
160 | ||
161 |
# construct calculations
|
|
162 | ! |
arms <- c(ref, trt) |
163 | ! |
df_n <- data.frame(arm_N) |
164 | ! |
names(df_n) <- c("arm", "total") |
165 | ! |
df_n <- df_n[df_n$arm %in% arms, ] |
166 | ! |
ref_total <- df_n %>% |
167 | ! |
filter(arm == ref) %>% |
168 | ! |
select("total") %>% |
169 | ! |
pull() |
170 | ! |
trt_total <- df_n %>% |
171 | ! |
filter(arm == trt) %>% |
172 | ! |
select("total") %>% |
173 | ! |
pull() |
174 | ||
175 |
# create full cartesian of (arms) X (term levels) with 0 count to have full list of all possible combination
|
|
176 |
# this is done in order to secure further calls
|
|
177 | ! |
df_full <- cbind(expand.grid(arm = arms, term = term_levels), N = 0) |
178 | ||
179 | ! |
df_reshaped <- df %>% |
180 | ! |
distinct() %>% |
181 | ! |
filter(arm %in% arms) %>% |
182 | ! |
group_by(arm, term) %>% |
183 | ! |
summarise(N = n()) %>% |
184 | ! |
ungroup() %>% |
185 | ! |
rbind(df_full) %>% |
186 | ! |
group_by(arm, term) %>% |
187 | ! |
summarise(N = sum(.data$N)) %>% |
188 | ! |
mutate(arm = ifelse(arm == trt, "trt_count", "ref_count")) %>% |
189 | ! |
tidyr::pivot_wider(names_from = arm, values_from = "N", values_fill = list(N = 0)) |
190 | ||
191 | ! |
df_ci <- df_reshaped %>% |
192 | ! |
group_by(term) %>% |
193 | ! |
do( |
194 | ! |
data.frame( |
195 | ! |
t(c( |
196 | ! |
DescTools::BinomDiffCI( |
197 | ! |
.data$trt_count, |
198 | ! |
trt_total,
|
199 | ! |
.data$ref_count, |
200 | ! |
ref_total,
|
201 | ! |
conf_level,
|
202 | ! |
method = diff_ci_method |
203 | ! |
)[1, ], # wald |
204 | ! |
meanrisk = (.data$trt_count + .data$ref_count) / (trt_total + ref_total) |
205 |
)) |
|
206 |
)
|
|
207 |
) %>% |
|
208 | ! |
ungroup() %>% |
209 | ! |
rename(riskdiff = "est", lower_ci = "lwr.ci", upper_ci = "upr.ci") |
210 | ||
211 | ! |
df_risk <- df_reshaped %>% |
212 | ! |
group_by(term) %>% |
213 | ! |
do( |
214 | ! |
data.frame( |
215 | ! |
risk = c(.data$trt_count / trt_total, .data$ref_count / ref_total), |
216 | ! |
arm = c(trt, ref) |
217 |
)
|
|
218 |
) %>% |
|
219 | ! |
ungroup() |
220 | ||
221 | ! |
names(color) <- arms |
222 | ! |
names(shape) <- arms |
223 | ||
224 |
# if diff_range specified, limit terms
|
|
225 | ! |
terms_needed <- df_ci %>% |
226 | ! |
filter( |
227 | ! |
.data$riskdiff > diff_range[1] & |
228 | ! |
.data$riskdiff < diff_range[2] & |
229 | ! |
.data$meanrisk > rate_range[1] & |
230 | ! |
.data$meanrisk < rate_range[2] |
231 |
) %>% |
|
232 | ! |
select(term) %>% |
233 | ! |
distinct() %>% |
234 | ! |
pull() %>% |
235 | ! |
as.character() |
236 | ||
237 | ! |
if (length(terms_needed) == 0) { |
238 | ! |
ret <- grid::textGrob("All Observations are filtered out") |
239 | ! |
if (draw) { |
240 | ! |
grid::grid.draw(ret) |
241 |
}
|
|
242 | ! |
return(invisible(ret)) |
243 |
}
|
|
244 | ||
245 |
# sorting
|
|
246 | ! |
if (sort_by == "term") { |
247 | ! |
terms_needed <- sort(terms_needed, decreasing = TRUE) |
248 |
} else { |
|
249 | ! |
terms_needed <- df_ci %>% |
250 | ! |
arrange(.data[[sort_by]]) %>% |
251 | ! |
filter(term %in% terms_needed) %>% |
252 | ! |
select(term) %>% |
253 | ! |
pull() |
254 |
}
|
|
255 | ||
256 | ! |
if (reversed) { |
257 | ! |
terms_needed <- rev(terms_needed) |
258 |
}
|
|
259 | ||
260 | ! |
df_ci <- df_ci %>% |
261 | ! |
filter(term %in% terms_needed) %>% |
262 | ! |
mutate(term = factor(term, terms_needed)) |
263 | ! |
df_risk <- df_risk %>% |
264 | ! |
filter(term %in% terms_needed) %>% |
265 | ! |
mutate(term = factor(term, terms_needed)) |
266 | ! |
terms_label <- vapply( |
267 | ! |
lapply(terms_needed, strwrap, width = 30), |
268 | ! |
paste,
|
269 | ! |
FUN.VALUE = character(1), |
270 | ! |
collapse = "\n" |
271 |
)
|
|
272 | ||
273 | ! |
mytheme <- theme_osprey(axis_side = axis_side, fontsize = fontsize) |
274 | ||
275 | ! |
labels <- stats::setNames(sprintf("%s\n(N = %i)", df_n$arm, df_n$total), df_n$arm) |
276 | ||
277 | ! |
y_axis <- scale_y_discrete( |
278 | ! |
limits = terms_needed, |
279 | ! |
breaks = terms_needed, |
280 | ! |
labels = terms_label, |
281 | ! |
position = axis_side |
282 |
)
|
|
283 | ||
284 | ! |
p1 <- ggplot(df_risk) + |
285 | ! |
geom_point( |
286 | ! |
aes( |
287 | ! |
y = term, |
288 | ! |
x = .data$risk, |
289 | ! |
group = arm, |
290 | ! |
color = arm, |
291 | ! |
shape = arm |
292 |
),
|
|
293 | ! |
size = fontsize * 0.7 |
294 |
) + |
|
295 | ! |
mytheme + |
296 | ! |
ggtitle("Proportion") + |
297 | ! |
scale_color_manual(values = color, labels = labels) + |
298 | ! |
scale_shape_manual(values = shape, labels = labels) + |
299 | ! |
y_axis
|
300 | ||
301 | ! |
p2 <- ggplot(df_ci) + |
302 | ! |
geom_point(mapping = aes(y = term, x = .data$riskdiff), size = fontsize * 0.7) + |
303 | ! |
geom_vline(data = NULL, xintercept = 0, linetype = 2) + |
304 | ! |
mytheme + |
305 | ! |
geom_errorbarh(mapping = aes(xmax = .data$upper_ci, xmin = .data$lower_ci, y = term), height = 0.4) + |
306 | ! |
y_axis + |
307 | ! |
ggtitle("Risk Difference") |
308 | ||
309 | ! |
axis_name <- sprintf("axis-%s", substr(axis_side, 1, 1)) |
310 | ||
311 | ! |
p1_parts <- ggplotGrob(p1) |
312 | ! |
p2_parts <- ggplotGrob(p2) |
313 | ||
314 | ! |
mylegend <- grob_part(grob_part(p1_parts, "guide-box-bottom"), "guides") |
315 | ! |
axis <- grob_part(p1_parts, axis_name) |
316 | ||
317 | ! |
less_risk <- grid::textGrob( |
318 | ! |
"Favor\nTreatment",
|
319 | ! |
just = "left", |
320 | ! |
x = grid::unit(fontsize * .pt, "pt"), |
321 | ! |
gp = grid::gpar(fontsize = fontsize * .pt, fontface = "bold") |
322 |
)
|
|
323 | ! |
more_risk <- grid::textGrob( |
324 | ! |
"Favor\nControl",
|
325 | ! |
just = "right", |
326 | ! |
x = grid::unit(1, "npc") - grid::unit(fontsize * .pt, "pt"), |
327 | ! |
gp = grid::gpar(fontsize = fontsize * .pt, fontface = "bold") |
328 |
)
|
|
329 | ||
330 | ! |
risk_label <- gridExtra::arrangeGrob(less_risk, more_risk, nrow = 1) |
331 | ! |
title1 <- grob_part(p1_parts, "title") |
332 | ! |
title2 <- grob_part(p2_parts, "title") |
333 | ! |
panel1 <- grob_part(p1_parts, "panel") |
334 | ! |
panel2 <- grob_part(p2_parts, "panel") |
335 | ! |
axis_b1 <- grob_part(p1_parts, "axis-b") |
336 | ! |
axis_b2 <- grob_part(p2_parts, "axis-b") |
337 | ||
338 | ! |
grobs <- list( |
339 | ! |
title1,
|
340 | ! |
title2,
|
341 | ! |
axis,
|
342 | ! |
panel1,
|
343 | ! |
panel2,
|
344 | ! |
axis_b1,
|
345 | ! |
axis_b2,
|
346 | ! |
mylegend,
|
347 | ! |
risk_label
|
348 |
)
|
|
349 | ||
350 | ! |
if (axis_side == "left") { |
351 | ! |
layout_matrix <- rbind( |
352 | ! |
c(NA, 1, NA, 2), |
353 | ! |
c(3, 4, NA, 5), |
354 | ! |
c(NA, 6, NA, 7), |
355 | ! |
c(8, 8, NA, 9) |
356 |
)
|
|
357 | ! |
widths <- grid::unit.c(grid::grobWidth(axis), grid::unit(c(1, 2 * fontsize, 1), c("null", "pt", "null"))) |
358 |
} else { |
|
359 | ! |
layout_matrix <- rbind( |
360 | ! |
c(1, NA, 2, NA), |
361 | ! |
c(4, NA, 5, 3), |
362 | ! |
c(6, NA, 7, NA), |
363 | ! |
c(8, NA, 9, NA) |
364 |
)
|
|
365 | ! |
widths <- grid::unit.c(grid::unit(c(1, 10, 1), c("null", "pt", "null")), grid::grobWidth(axis)) |
366 |
}
|
|
367 | ||
368 | ! |
heights <- grid::unit.c( |
369 | ! |
grid::grobHeight(title1), |
370 | ! |
grid::unit(1, "null"), |
371 | ! |
grid::grobHeight(axis_b1), |
372 | ! |
max(grid::grobHeight(mylegend), grid::grobHeight(more_risk)) |
373 |
)
|
|
374 | ||
375 | ! |
ret <- gridExtra::arrangeGrob( |
376 | ! |
grobs = grobs, |
377 | ! |
nrow = 4, |
378 | ! |
ncol = 4, |
379 | ! |
layout_matrix = layout_matrix, |
380 | ! |
heights = heights, |
381 | ! |
widths = widths |
382 |
)
|
|
383 | ||
384 | ! |
ret <- grob_add_padding(ret) |
385 | ||
386 | ! |
if (draw) { |
387 | ! |
grid::grid.draw(ret) |
388 |
}
|
|
389 | ! |
invisible(ret) |
390 |
}
|
|
391 | ||
392 | ||
393 |
#' create `AE` overview flags
|
|
394 |
#' @param df data frame of `AE`
|
|
395 |
#' @param fatal `AE` with fatal outcome derivation
|
|
396 |
#' @param serious Serious `AE` derivation.
|
|
397 |
#' @param serious_withdrawl Serious `AE` leading to withdrawal derivation
|
|
398 |
#' @param serious_modified Serious `AE` leading to dose modification derivation
|
|
399 |
#' @param serious_related Related Serious `AE` derivation
|
|
400 |
#' @param withdrawl `AE` leading to withdrawal derivation
|
|
401 |
#' @param modified `AE` leading to dose modification derivation
|
|
402 |
#' @param related Related `AE` derivation
|
|
403 |
#' @param related_withdrawl Related `AE` leading to withdrawal derivation
|
|
404 |
#' @param related_modified Related `AE` leading to dose modification derivation
|
|
405 |
#' @param ctc35 Grade 3-5 `AE` derivation
|
|
406 |
#' @param ... named expressions used to generate categories
|
|
407 |
#' @details in this function, all flags are expressions calls, for simpler usage.
|
|
408 |
#' @export
|
|
409 |
#' @examples
|
|
410 |
#' library(dplyr)
|
|
411 |
#'
|
|
412 |
#' ADAE <- osprey::rADAE
|
|
413 |
#'
|
|
414 |
#' # add additional dummy causality flags
|
|
415 |
#' ADAE <- ADAE %>%
|
|
416 |
#' mutate(AEREL1 = (AEREL == "Y" & ACTARM == "A: Drug X")) %>%
|
|
417 |
#' mutate(AEREL2 = (AEREL == "Y" & ACTARM == "B: Placebo"))
|
|
418 |
#' attr(ADAE[["AEREL1"]], "label") <- "AE related to A: Drug X"
|
|
419 |
#' attr(ADAE[["AEREL2"]], "label") <- "AE related to B: Placebo"
|
|
420 |
#'
|
|
421 |
#' create_flag_vars(ADAE)
|
|
422 |
#' # create other flags
|
|
423 |
#' create_flag_vars(ADAE, `AENSER` = AESER != "Y")
|
|
424 |
#' # remove flags that are not needed
|
|
425 |
#' create_flag_vars(ADAE, fatal = NULL)
|
|
426 |
create_flag_vars <- function(df, |
|
427 |
# nolint start
|
|
428 |
fatal = AESDTH == "Y", |
|
429 |
serious = AESER == "Y", |
|
430 |
serious_withdrawl = AESER == "Y" & |
|
431 |
grepl("DRUG WITHDRAWN", AEACN), |
|
432 |
serious_modified = AESER == "Y" & |
|
433 |
grepl("DRUG (INTERRUPTED|INCREASED|REDUCED)", AEACN), |
|
434 |
serious_related = AESER == "Y" & AEREL == "Y", |
|
435 |
withdrawl = grepl("DRUG WITHDRAWN", AEACN), |
|
436 |
modified = grepl("DRUG (INTERRUPTED|INCREASED|REDUCED)", AEACN), |
|
437 |
related = AEREL == "Y", |
|
438 |
related_withdrawl = AEREL == "Y" & grepl("DRUG WITHDRAWN", AEACN), |
|
439 |
related_modified = AEREL == "Y" & |
|
440 |
grepl("DRUG (INTERRUPTED|INCREASED|REDUCED)", AEACN), |
|
441 |
ctc35 = AETOXGR %in% c("3", "4", "5"), |
|
442 |
# nolint end
|
|
443 |
...) { |
|
444 | ! |
AESDTH <- AESER <- AEACN <- AEREL <- AETOXGR <- NULL # nolint |
445 | ! |
args <- |
446 | ! |
eval(substitute( |
447 | ! |
alist( |
448 | ! |
"AE with fatal outcome" = fatal, |
449 | ! |
"Serious AE" = serious, |
450 | ! |
"Serious AE leading to withdrawal" = serious_withdrawl, |
451 | ! |
"Serious AE leading to dose modification" = serious_modified, |
452 | ! |
"Related Serious AE" = serious_related, |
453 | ! |
"AE leading to withdrawal" = withdrawl, |
454 | ! |
"AE leading to dose modification" = modified, |
455 | ! |
"Related AE" = related, |
456 | ! |
"Related AE leading to withdrawal" = related_withdrawl, |
457 | ! |
"Related AE leading to dose modification" = related_modified, |
458 | ! |
"Grade 3-5 AE" = ctc35 |
459 |
)
|
|
460 |
)) |
|
461 | ! |
args <- c(args, eval(substitute(alist(...)))) |
462 | ! |
stopifnot(all(names(args) != "")) # all elements in ... should be named |
463 | ! |
argnames <- unique(names(args)) |
464 | ! |
df <- as.data.frame(df) |
465 | ! |
ret <- lapply(argnames, function(t) { |
466 | ! |
tryCatch( |
467 | ! |
expr = { |
468 | ! |
with(df, eval(args[[t]])) |
469 |
},
|
|
470 | ! |
error = function(w) { |
471 | ! |
NULL
|
472 |
},
|
|
473 | ! |
warning = function(w) { |
474 | ! |
NULL
|
475 |
}
|
|
476 |
)
|
|
477 |
}) |
|
478 | ! |
names(ret) <- argnames |
479 | ! |
valid <- vapply(argnames, function(x) { |
480 | ! |
valid <- length(ret[[x]]) > 0 |
481 | ! |
if (!valid) { |
482 | ! |
warning(sprintf( |
483 | ! |
"%s with calculation %s not valid",
|
484 | ! |
x,
|
485 | ! |
as.character(as.expression(args[[x]])) |
486 |
)) |
|
487 |
}
|
|
488 | ! |
valid
|
489 | ! |
}, FUN.VALUE = TRUE) |
490 | ! |
do.call(data.frame, args = list(ret[valid], check.names = FALSE)) |
491 |
}
|
1 |
#' Butterfly Plot
|
|
2 |
#'
|
|
3 |
#' The butterfly plot is often used in Early Development (ED) and is an opposed
|
|
4 |
#' barplot that shows instances of `AE`s or # of patients by category separated by
|
|
5 |
#' a dichotomization variable. Each bar can be color coded according
|
|
6 |
#' to a variable of choice and sorted according to either alphabetical order or the
|
|
7 |
#' maximum count.
|
|
8 |
#'
|
|
9 |
#' @param category vector of y values
|
|
10 |
#' @param right_flag vector of \code{logical} of the same length as \code{category}.
|
|
11 |
#' used to filter \code{category} for the right side of the barplot.
|
|
12 |
#' to maintain backward compatibility, a vector of 1s and 0s would also work.
|
|
13 |
#' @param left_flag vector of \code{logical} of the same length as \code{category}.
|
|
14 |
#' used to filter \code{category} for the left side of the barplot.
|
|
15 |
#' to maintain backward compatibility, a vector of 1s and 0s would also work.
|
|
16 |
#' @param group_names string vector of length 2 with desired names of dichotomization variables
|
|
17 |
#' required format : first name corresponds to the name of the right side
|
|
18 |
#' second name corresponds to name of the left side
|
|
19 |
#' default: will extract column names from group
|
|
20 |
#' @param block_count string - what to count by (ex: # of `AE`s or # of patients)
|
|
21 |
#' @param block_color vector - color coding of bar segments
|
|
22 |
#' @param id unique subject identifier variable.
|
|
23 |
#' @param facet_rows vector defines what variable is used to split the
|
|
24 |
#' plot into rows, default here is NULL
|
|
25 |
#' @param x_label string of text for x axis label, default is block_count
|
|
26 |
#' @param y_label string of text for y axis label, default is `AE` Derived Terms
|
|
27 |
#' @param legend_label \code{character} for legend label, default is `"AETOXGR"`
|
|
28 |
#' @param sort_by character string that defines the ordering of the class and term
|
|
29 |
#' variables in the output table,
|
|
30 |
#' options: `"alphabetical"`, `"count"`, `"left"`, `"right"`, default here is set to `"count"`
|
|
31 |
#' @param show_legend `logical(1)` of whether color coding legend is included,
|
|
32 |
#' default here is FALSE
|
|
33 |
#'
|
|
34 |
#' @details there is no equivalent STREAM output
|
|
35 |
#'
|
|
36 |
#' @return `ggplot` object
|
|
37 |
#'
|
|
38 |
#' @export
|
|
39 |
#'
|
|
40 |
#' @template author_zhanc107
|
|
41 |
#' @template author_qit3
|
|
42 |
#'
|
|
43 |
#' @examplesIf require("nestcolor")
|
|
44 |
#' library(dplyr)
|
|
45 |
#' library(nestcolor)
|
|
46 |
#'
|
|
47 |
#' ADSL <- osprey::rADSL %>%
|
|
48 |
#' select(USUBJID, STUDYID, SEX, ARM, RACE) %>%
|
|
49 |
#' dplyr::filter(SEX %in% c("F", "M"))
|
|
50 |
#' ADAE <- osprey::rADAE %>% select(USUBJID, STUDYID, AEBODSYS, AETOXGR)
|
|
51 |
#'
|
|
52 |
#' ANL <- left_join(ADAE, ADSL, by = c("STUDYID", "USUBJID"))
|
|
53 |
#' ANL <- ANL %>%
|
|
54 |
#' dplyr::mutate(flag1 = ifelse(RACE == "ASIAN", 1, 0)) %>%
|
|
55 |
#' dplyr::mutate(flag2 = ifelse(SEX == "M", 1, 0))
|
|
56 |
#' ANL <- na.omit(ANL)
|
|
57 |
#' # Example 1, # of AEs
|
|
58 |
#' g_butterfly(
|
|
59 |
#' category = ANL$AEBODSYS,
|
|
60 |
#' right_flag = ANL$flag1,
|
|
61 |
#' left_flag = ANL$flag2,
|
|
62 |
#' group_names = c("flag1 Asian", "flag2 M"),
|
|
63 |
#' block_count = "# of AEs",
|
|
64 |
#' block_color = ANL$AETOXGR,
|
|
65 |
#' id = ANL$USUBJID,
|
|
66 |
#' x_label = "# of AEs",
|
|
67 |
#' y_label = "AE Body System",
|
|
68 |
#' legend_label = "AETOXGR",
|
|
69 |
#' sort_by = "count",
|
|
70 |
#' show_legend = TRUE
|
|
71 |
#' )
|
|
72 |
#'
|
|
73 |
#' # Example 2, # of patients with facet
|
|
74 |
#' g_butterfly(
|
|
75 |
#' category = ANL$AEBODSYS,
|
|
76 |
#' right_flag = ANL$flag1,
|
|
77 |
#' left_flag = ANL$flag2,
|
|
78 |
#' group_names = c("flag1 Asian", "flag2 M"),
|
|
79 |
#' block_count = "# of patients",
|
|
80 |
#' block_color = ANL$AETOXGR,
|
|
81 |
#' facet_rows = ANL$ARM,
|
|
82 |
#' id = ANL$USUBJID,
|
|
83 |
#' x_label = "# of patients",
|
|
84 |
#' y_label = "AE Derived Terms",
|
|
85 |
#' legend_label = "AETOXGR",
|
|
86 |
#' sort_by = "count",
|
|
87 |
#' show_legend = TRUE
|
|
88 |
#' )
|
|
89 |
g_butterfly <- function(category, |
|
90 |
right_flag,
|
|
91 |
left_flag,
|
|
92 |
id = NULL, |
|
93 |
group_names = NULL, |
|
94 |
block_count = c("# of patients", "# of AEs"), |
|
95 |
block_color = NULL, |
|
96 |
facet_rows = NULL, |
|
97 |
x_label = block_count, |
|
98 |
y_label = "AE Derived Terms", |
|
99 |
legend_label = "AETOXGR", |
|
100 |
sort_by = c("count", "alphabetical", "right", "left"), |
|
101 |
show_legend = TRUE) { |
|
102 | ! |
stopifnot( |
103 | ! |
"invalid arguments: check that the length of input arguments are identical" = |
104 | ! |
length(category) == length(right_flag) && length(right_flag) == length(left_flag) |
105 |
)
|
|
106 | ! |
stopifnot( |
107 | ! |
"invalid arguments: right_flag or left_flag contains values other than 1/TRUE or 0/FALSE" = |
108 | ! |
all(union(right_flag, left_flag) %in% c(1, 0)) |
109 |
)
|
|
110 | ! |
stopifnot( |
111 | ! |
"invalid arguments: right_flag and left_flag contain only 0/FALSE values" = |
112 | ! |
any(union(right_flag, left_flag) == 1) |
113 |
)
|
|
114 | ! |
stopifnot( |
115 | ! |
"invalid arguments: check that the length of block_color is equal as other inputs" = |
116 | ! |
is.null(block_color) || length(block_color) == length(category) |
117 |
)
|
|
118 | ! |
block_count <- match.arg(block_count) |
119 | ! |
checkmate::assert_character(id, null.ok = isFALSE(block_count == "# of patients")) |
120 | ||
121 | ! |
stopifnot( |
122 | ! |
"invalid arguments: check that the length of block_color is equal as other inputs" = |
123 | ! |
is.null(id) || length(id) == length(category) |
124 |
)
|
|
125 | ! |
stopifnot( |
126 | ! |
"invalid arguments: check that the length of block_color is equal as other inputs" = |
127 | ! |
is.null(facet_rows) || |
128 | ! |
(length(facet_rows) == length(category)) || # nolint: indentation_linter. |
129 | ! |
(is.data.frame(facet_rows) && nrow(facet_rows) == length(category)) |
130 |
)
|
|
131 | ! |
checkmate::assert_string(x_label) |
132 | ! |
checkmate::assert_string(y_label) |
133 | ! |
checkmate::assert_string(legend_label) |
134 | ! |
sort_by <- match.arg(sort_by) |
135 | ||
136 |
# set up data-------
|
|
137 | ! |
dat <- data.frame(y = stringr::str_wrap(category, width = 30), r_flag = right_flag, l_flag = left_flag) |
138 | ||
139 | ! |
groups <- "y" |
140 | ||
141 | ! |
if (!is.null(id)) { |
142 | ! |
dat$id <- id |
143 |
}
|
|
144 | ! |
if (!is.null(facet_rows)) { |
145 | ! |
facet_rows <- interaction(facet_rows) |
146 | ! |
dat$f_rows <- facet_rows |
147 | ! |
groups <- c(groups, "f_rows") |
148 |
}
|
|
149 | ! |
if (!is.null(block_color)) { |
150 | ! |
dat$bar_color <- block_color |
151 | ! |
groups <- c(groups, "bar_color") |
152 |
}
|
|
153 | ||
154 | ! |
get_counts <- function(.data, block_count) { |
155 | ! |
if (block_count == "# of patients") { |
156 | ! |
length(unique(.data$id)) |
157 | ! |
} else if (block_count == "# of AEs") { |
158 | ! |
n() |
159 |
}
|
|
160 |
}
|
|
161 | ! |
highest_grade <- function(.data, block_count) { |
162 | ! |
if (block_count == "# of patients" && "bar_color" %in% colnames(.data)) { |
163 | ! |
.data %>% |
164 | ! |
dplyr::group_by(.data$y, .data$id) %>% |
165 | ! |
dplyr::filter(.data$bar_color == sort(.data$bar_color, decreasing = TRUE)[1]) |
166 |
} else { |
|
167 | ! |
.data
|
168 |
}
|
|
169 |
}
|
|
170 | ||
171 | ! |
counts_r <- dat %>% |
172 | ! |
dplyr::filter(.data$r_flag == 1) %>% |
173 | ! |
highest_grade(block_count) %>% |
174 | ! |
dplyr::group_by_at(groups) %>% |
175 | ! |
dplyr::summarize(n_i = get_counts(.data, block_count)) %>% |
176 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
177 | ! |
dplyr::mutate(label_ypos = rev(cumsum(rev(.data$n_i)))) |
178 | ||
179 | ! |
counts_l <- dat %>% |
180 | ! |
filter(.data$l_flag == 1) %>% |
181 | ! |
highest_grade(block_count) %>% |
182 | ! |
dplyr::group_by_at(groups) %>% |
183 | ! |
dplyr::summarize(n_i = get_counts(.data, block_count)) %>% |
184 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
185 | ! |
dplyr::mutate(label_ypos = rev(cumsum(rev(.data$n_i)))) |
186 | ||
187 | ! |
total_label_pos_r <- counts_r %>% |
188 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
189 | ! |
dplyr::summarize(label_ypos = max(.data$label_ypos)) |
190 | ||
191 | ! |
total_label_pos_l <- counts_l %>% |
192 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
193 | ! |
dplyr::summarize(label_ypos = max(.data$label_ypos)) |
194 | ||
195 | ! |
total_text_ann_r <- dat %>% |
196 | ! |
dplyr::filter(.data$r_flag == 1) %>% |
197 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
198 | ! |
dplyr::summarize(n = get_counts(.data, block_count)) %>% |
199 | ! |
dplyr::left_join(total_label_pos_r, by = setdiff(groups, "bar_color")) |
200 | ||
201 | ! |
total_text_ann_l <- dat %>% |
202 | ! |
dplyr::filter(.data$l_flag == 1) %>% |
203 | ! |
dplyr::group_by_at(setdiff(groups, "bar_color")) %>% |
204 | ! |
dplyr::summarize(n = get_counts(.data, block_count)) %>% |
205 | ! |
dplyr::left_join(total_label_pos_l, by = setdiff(groups, "bar_color")) |
206 | ||
207 | ||
208 | ! |
if (sort_by == "alphabetical") { |
209 | ! |
levels_all <- unique(sort(as.character(union(counts_l$y, counts_r$y)), decreasing = TRUE)) |
210 | ! |
counts_r$y <- factor(counts_r$y, levels = levels_all) |
211 | ! |
counts_l$y <- factor(counts_l$y, levels = levels_all) |
212 | ! |
} else if (sort_by == "count") { |
213 | ! |
tot <- dplyr::bind_rows(total_text_ann_r, total_text_ann_l) %>% |
214 | ! |
dplyr::group_by(.data$y) %>% |
215 | ! |
dplyr::summarize(n = sum(n)) %>% |
216 | ! |
dplyr::arrange(n) |
217 | ||
218 | ! |
counts_r$y <- factor(counts_r$y, levels = tot$y) |
219 | ! |
counts_l$y <- factor(counts_l$y, levels = tot$y) |
220 | ! |
} else if (sort_by == "right") { |
221 | ! |
tot <- dplyr::full_join(total_text_ann_r, select(total_text_ann_l, -n), by = "y") %>% |
222 | ! |
dplyr::group_by(.data$y) %>% |
223 | ! |
dplyr::summarize(n = sum(n, na.rm = TRUE)) %>% |
224 | ! |
dplyr::arrange(n) |
225 | ||
226 | ! |
counts_r$y <- factor(counts_r$y, levels = tot$y) |
227 | ! |
counts_l$y <- factor(counts_l$y, levels = tot$y) |
228 | ! |
} else if (sort_by == "left") { |
229 | ! |
tot <- dplyr::full_join(total_text_ann_l, select(total_text_ann_r, -n), by = "y") %>% |
230 | ! |
dplyr::group_by(.data$y) %>% |
231 | ! |
dplyr::summarize(n = sum(n, na.rm = TRUE)) %>% |
232 | ! |
dplyr::arrange(n) |
233 | ||
234 | ! |
counts_r$y <- factor(counts_r$y, levels = tot$y) |
235 | ! |
counts_l$y <- factor(counts_l$y, levels = tot$y) |
236 |
}
|
|
237 | ||
238 | ! |
max_c <- max(c(total_text_ann_r$label_ypos, total_text_ann_l$label_ypos)) |
239 | ||
240 | ! |
if (is.null(group_names)) { |
241 | ! |
g_r <- "" |
242 | ! |
g_l <- "" |
243 |
} else { |
|
244 | ! |
g_r <- group_names[1] |
245 | ! |
g_l <- group_names[2] |
246 |
}
|
|
247 | ||
248 |
# plot butterfly plot --------------------
|
|
249 | ! |
if (!is.null(block_color)) { |
250 | ! |
pl <- ggplot(NULL, aes(x = .data$y)) + |
251 | ! |
geom_bar(data = counts_r, aes(y = .data$n_i, fill = .data$bar_color), stat = "identity") + |
252 | ! |
geom_bar(data = counts_l, aes(y = -.data$n_i, fill = .data$bar_color), stat = "identity") + |
253 | ! |
geom_text(data = counts_r, aes(y = .data$label_ypos, label = .data$n_i), hjust = 0.9) + |
254 | ! |
geom_text(data = counts_l, aes(y = -.data$label_ypos, label = .data$n_i), hjust = -0.9) + |
255 | ! |
geom_text(data = total_text_ann_r, aes(y = .data$label_ypos, label = .data$n), fontface = "bold", hjust = -1) + |
256 | ! |
geom_text( |
257 | ! |
data = total_text_ann_l, aes(y = -.data$label_ypos - 0.4, label = "n"), |
258 | ! |
fontface = "bold", hjust = 0.9 |
259 |
) + |
|
260 | ! |
geom_hline(yintercept = 0, colour = "black", lwd = 0.4) + |
261 | ! |
coord_flip() + |
262 | ! |
scale_y_continuous(labels = abs, limits = (max_c * 1.2) * c(-1, 1)) + |
263 | ! |
labs(x = y_label, y = block_count, fill = legend_label) |
264 |
} else { |
|
265 | ! |
pl <- ggplot(NULL, aes(x = .data$y)) + |
266 | ! |
geom_bar(data = counts_r, aes(y = .data$n_i), stat = "identity") + |
267 | ! |
geom_bar(data = counts_l, aes(y = -.data$n_i), stat = "identity") + |
268 | ! |
geom_hline(yintercept = 0, colour = "black", lwd = 0.4) + |
269 | ! |
geom_text(data = total_text_ann_r, aes(y = .data$label_ypos, label = "n"), fontface = "bold", hjust = -1) + |
270 | ! |
geom_text( |
271 | ! |
data = total_text_ann_l, aes(y = -.data$label_ypos - 0.4, label = "n"), |
272 | ! |
fontface = "bold", hjust = 0.9 |
273 |
) + |
|
274 | ! |
coord_flip() + |
275 | ! |
scale_y_continuous(labels = abs, limits = (max_c * 1.2) * c(-1, 1)) + |
276 | ! |
labs(x = y_label, y = block_count, fill = legend_label) |
277 |
}
|
|
278 | ||
279 | ! |
if (!is.null(facet_rows)) { |
280 | ! |
pl <- pl + facet_wrap(~f_rows, ncol = 1) |
281 |
}
|
|
282 | ||
283 | ! |
pl <- pl + |
284 | ! |
theme_bw() + |
285 | ! |
theme( |
286 | ! |
strip.background = element_rect(colour = "white", fill = "white"), |
287 | ! |
strip.text.x = element_text(color = "black", size = 14), |
288 | ! |
title = element_text(size = 9), |
289 | ! |
axis.title = element_text(size = 20), |
290 | ! |
axis.text = element_text(color = "black", size = 9), |
291 | ! |
legend.text = element_text(size = 9), |
292 | ! |
legend.title = element_text(size = 9), |
293 | ! |
panel.grid.major.y = element_line(colour = "gray", linetype = "dotted"), |
294 | ! |
plot.margin = grid::unit(c(1.5, 1, 1, 1), "cm"), |
295 | ! |
legend.position = if (show_legend) "right" else "none" |
296 |
) + |
|
297 | ! |
scale_x_discrete(limits = levels(counts_r$y)) |
298 | ||
299 |
# labs pl <- pl + labs(title = stringr::str_wrap(g2, width = 30))
|
|
300 | ! |
g_0 <- ggplotGrob(pl) |
301 | ||
302 | ! |
g_1 <- gtable::gtable_add_grob( |
303 | ! |
g_0,
|
304 | ! |
grid::grid.text( |
305 | ! |
stringr::str_wrap(g_r, width = 30), |
306 | ! |
x = 1, just = "center", hjust = 1, gp = grid::gpar(fontsize = 11) |
307 |
),
|
|
308 | ! |
t = 1.5, l = g_0$layout[grep("axis-r", g_0$layout$name)[1], 2], b = 3, name = "right-title", clip = "off" |
309 |
)
|
|
310 | ! |
g_2 <- gtable::gtable_add_grob( |
311 | ! |
g_1,
|
312 | ! |
grid::grid.text( |
313 | ! |
stringr::str_wrap(g_l, width = 30), |
314 | ! |
x = 1, just = "center", hjust = 0, gp = grid::gpar(fontsize = 11) |
315 |
),
|
|
316 | ! |
t = 1.5, l = g_0$layout[grep("axis-l", g_0$layout$name)[1], 2], b = 3, name = "left-title", clip = "off" |
317 |
)
|
|
318 | ! |
grid::grid.draw(g_2) |
319 | ! |
invisible(g_2) |
320 |
}
|
1 |
#' Adverse Event Category Plot
|
|
2 |
#'
|
|
3 |
#' Draw adverse event category plot.
|
|
4 |
#' @inheritParams argument_convention
|
|
5 |
#' @param id (`vector`)\cr contains subject identifier. Usually it is \code{ADAE$USUBJID}.
|
|
6 |
#' @param arm_sl (`vector`)\cr contains the subject level treatment variable.
|
|
7 |
#' For example, \code{ADSL$ACTARM}.
|
|
8 |
#' @param subgroups (`data.frame`)\cr Variables to conduct analysis.
|
|
9 |
#' @param subgroups_sl (`data.frame`)\cr Subject level variables to conduct analysis.
|
|
10 |
#' Usually from `ADSL`.
|
|
11 |
#' @param ref (`character`)\cr indicates the name of the reference arm. Default is the first
|
|
12 |
#' level of \code{arm}.
|
|
13 |
#' @param trt (`character`)\cr indicates the name of the treatment arm. Default is the second
|
|
14 |
#' level of \code{arm}.
|
|
15 |
#' @param indent (`numeric`)\cr non-negative integer where 0 means that the subgroup levels should not be indented
|
|
16 |
#' @param subgroups_levels (`list`)\cr A nested named list of variables to conduct analysis.
|
|
17 |
#' The names of the nested lists are used to show as the label.
|
|
18 |
#' The children lists should start with "Total" = variable label,
|
|
19 |
#' followed by labels for each level of said variable. See example for reference.
|
|
20 |
#' @param xmax (`numeric`)\cr maximum range for the x-axis.
|
|
21 |
#' x-axis range will be automatically assigned based on risk output when `xmax` is less than or equal to 0.
|
|
22 |
#' `xmax` is 0 by default
|
|
23 |
#' @param arm_n (`logical`)\cr whether to display the N in each arm.
|
|
24 |
#'
|
|
25 |
#' @author Liming Li (Lil128) \email{liming.li@roche.com}
|
|
26 |
#'
|
|
27 |
#' @details there is no equivalent STREAM output
|
|
28 |
#'
|
|
29 |
#' @return grob object
|
|
30 |
#'
|
|
31 |
#' @export
|
|
32 |
#'
|
|
33 |
#' @examples
|
|
34 |
#' library(grid)
|
|
35 |
#' ADAE <- osprey::rADAE
|
|
36 |
#' ADSL <- osprey::rADSL
|
|
37 |
#'
|
|
38 |
#' id <- ADAE$USUBJID
|
|
39 |
#' arm <- ADAE$ACTARMCD
|
|
40 |
#' arm_sl <- as.character(ADSL$ACTARMCD)
|
|
41 |
#' subgroups_sl <- ADSL[, c("SEX", "RACE", "STRATA1")]
|
|
42 |
#' subgroups <- ADAE[, c("SEX", "RACE", "STRATA1")]
|
|
43 |
#' subgroups_levels <- list(
|
|
44 |
#' RACE = list(
|
|
45 |
#' "Total" = "Race",
|
|
46 |
#' "AMERICAN INDIAN OR ALASKA NATIVE" = "American",
|
|
47 |
#' "WHITE" = "White",
|
|
48 |
#' "ASIAN" = "Asian",
|
|
49 |
#' "BLACK OR AFRICAN AMERICAN" = "African"
|
|
50 |
#' ),
|
|
51 |
#' STRATA1 = list(
|
|
52 |
#' "Total" = "Strata",
|
|
53 |
#' "A" = "TypeA",
|
|
54 |
#' "B" = "TypeB",
|
|
55 |
#' "C" = "Typec"
|
|
56 |
#' ),
|
|
57 |
#' SEX = list(
|
|
58 |
#' "Total" = "Sex",
|
|
59 |
#' "M" = "Male",
|
|
60 |
#' "F" = "Female"
|
|
61 |
#' )
|
|
62 |
#' )
|
|
63 |
#' # Example 1
|
|
64 |
#' p1 <- g_ae_sub(id,
|
|
65 |
#' arm,
|
|
66 |
#' arm_sl,
|
|
67 |
#' subgroups,
|
|
68 |
#' subgroups_sl,
|
|
69 |
#' trt = "ARM A",
|
|
70 |
#' ref = "ARM C",
|
|
71 |
#' subgroups_levels = subgroups_levels,
|
|
72 |
#' arm_n = FALSE
|
|
73 |
#' )
|
|
74 |
#' grid::grid.newpage()
|
|
75 |
#'
|
|
76 |
#' # Example 2: display number of patients in each arm
|
|
77 |
#' p2 <- g_ae_sub(id,
|
|
78 |
#' arm,
|
|
79 |
#' arm_sl,
|
|
80 |
#' subgroups,
|
|
81 |
#' subgroups_sl,
|
|
82 |
#' trt = "ARM A",
|
|
83 |
#' ref = "ARM C",
|
|
84 |
#' subgroups_levels = subgroups_levels,
|
|
85 |
#' arm_n = TRUE
|
|
86 |
#' )
|
|
87 |
#' grid::grid.newpage()
|
|
88 |
#'
|
|
89 |
#' # Example 3: preprocess data to only include treatment and control arm patients
|
|
90 |
#' trt <- "ARM A"
|
|
91 |
#' ref <- "ARM C"
|
|
92 |
#' ADAE <- osprey::rADAE
|
|
93 |
#' ADSL <- osprey::rADSL %>% filter(ACTARMCD %in% c(trt, ref))
|
|
94 |
#' id <- ADAE$USUBJID
|
|
95 |
#' arm <- ADAE$ACTARMCD
|
|
96 |
#' arm_sl <- as.character(ADSL$ACTARMCD)
|
|
97 |
#' subgroups_sl <- ADSL[, c("SEX", "RACE", "STRATA1")]
|
|
98 |
#' subgroups <- ADAE[, c("SEX", "RACE", "STRATA1")]
|
|
99 |
#' subgroups_levels <- list(
|
|
100 |
#' RACE = list(
|
|
101 |
#' "Total" = "Race",
|
|
102 |
#' "AMERICAN INDIAN OR ALASKA NATIVE" = "American",
|
|
103 |
#' "WHITE" = "White",
|
|
104 |
#' "ASIAN" = "Asian",
|
|
105 |
#' "BLACK OR AFRICAN AMERICAN" = "African"
|
|
106 |
#' ),
|
|
107 |
#' STRATA1 = list(
|
|
108 |
#' "Total" = "Strata",
|
|
109 |
#' "A" = "TypeA",
|
|
110 |
#' "B" = "TypeB",
|
|
111 |
#' "C" = "Typec"
|
|
112 |
#' ),
|
|
113 |
#' SEX = list(
|
|
114 |
#' "Total" = "Sex",
|
|
115 |
#' "M" = "Male",
|
|
116 |
#' "F" = "Female"
|
|
117 |
#' )
|
|
118 |
#' )
|
|
119 |
#' p3 <- g_ae_sub(id,
|
|
120 |
#' arm,
|
|
121 |
#' arm_sl,
|
|
122 |
#' subgroups,
|
|
123 |
#' subgroups_sl,
|
|
124 |
#' trt,
|
|
125 |
#' ref,
|
|
126 |
#' subgroups_levels = subgroups_levels,
|
|
127 |
#' arm_n = FALSE
|
|
128 |
#' )
|
|
129 |
g_ae_sub <- function(id, |
|
130 |
arm,
|
|
131 |
arm_sl,
|
|
132 |
subgroups,
|
|
133 |
subgroups_sl,
|
|
134 |
trt = levels(arm)[1], |
|
135 |
ref = levels(arm)[2], |
|
136 |
indent = 4, |
|
137 |
subgroups_levels = NULL, |
|
138 |
xmax = 0, |
|
139 |
conf_level = 0.95, |
|
140 |
diff_ci_method = c( |
|
141 |
"wald", "waldcc", "ac", "score", "scorecc", |
|
142 |
"mn", "mee", "blj", "ha", "beal" |
|
143 |
),
|
|
144 |
fontsize = 4, |
|
145 |
arm_n = FALSE, |
|
146 |
draw = TRUE) { |
|
147 | ! |
diff_ci_method <- match.arg(diff_ci_method) |
148 | ||
149 | ! |
if (!is.null(subgroups_levels)) { |
150 | ! |
labels <- unlist(subgroups_levels) |
151 | ! |
label_df <- |
152 | ! |
tibble( |
153 | ! |
level = stringr::str_replace_all(names(labels), "\\.", "__"), |
154 | ! |
label = labels |
155 |
) %>% |
|
156 | ! |
bind_rows(c(level = "TOTAL__Total", label = "Overall")) %>% |
157 | ! |
mutate( |
158 | ! |
indents = stringr::str_dup(" ", if_else( |
159 | ! |
stringr::str_detect(.data$level, "__Total"), 0, indent |
160 |
)), |
|
161 |
# create label with indents if not total
|
|
162 | ! |
label = paste0(.data$indents, .data$label) |
163 |
)
|
|
164 |
}
|
|
165 | ! |
stopifnot("invalid arguments: check that the length of id and arm are identical" = length(id) == length(arm)) |
166 | ! |
checkmate::assert_character(arm_sl, min.len = 2) |
167 | ! |
stopifnot("invalid arguments: trt and ref need to be from arm" = all(c(trt, ref) %in% unique(arm))) |
168 | ! |
checkmate::assert_number(conf_level, lower = 0.5, upper = 1) |
169 | ! |
checkmate::assert_number(fontsize, lower = 0) |
170 | ! |
checkmate::assert_data_frame(subgroups) |
171 | ! |
stopifnot( |
172 | ! |
"invalid argument: subgroups needs to be a data.frame with nrow = length(arm)" = |
173 | ! |
nrow(subgroups) == length(arm) |
174 |
)
|
|
175 | ! |
checkmate::assert_data_frame(subgroups_sl) |
176 | ! |
stopifnot( |
177 | ! |
"invalid argument: subgroups_sl need to be a data.frame with nrow = length(arm_sl)" = |
178 | ! |
nrow(subgroups_sl) == length(arm_sl) |
179 |
)
|
|
180 | ! |
checkmate::assert_number(indent, lower = 0, finite = TRUE) |
181 | ! |
checkmate::assert_number(xmax) |
182 | ! |
stopifnot( |
183 | ! |
"invalid argument: please only use the subgroups column names as the lists names in subgroups_levels" = |
184 | ! |
all(names(subgroups_levels) %in% names(lapply(subgroups, levels))) |
185 |
)
|
|
186 | ||
187 | ! |
stopifnot( |
188 | ! |
"invalid argument: please only include levels in subgroups columns in the nested subgroups_levels" = |
189 | ! |
all(unlist(lapply(names(subgroups_levels), function(level_name) { |
190 | ! |
all(names(subgroups_levels[[level_name]]) %in% c("Total", levels(subgroups[, level_name] %>% dplyr::pull()))) |
191 |
}))) |
|
192 |
)
|
|
193 | ||
194 | ! |
checkmate::assert_flag(arm_n) |
195 | ||
196 | ! |
subgroups <- subgroups %>% |
197 | ! |
mutate_all(as.character) %>% |
198 | ! |
mutate(TOTAL = "Total") |
199 | ||
200 | ! |
arms <- c(trt, ref) |
201 | ! |
xs <- syms(paste("count", arms, sep = "__")) |
202 | ! |
ns <- syms(paste("total", arms, sep = "__")) |
203 | ! |
rs <- syms(paste("risk", arms, sep = "__")) |
204 | ||
205 | ! |
x1 <- xs[[1]] |
206 | ! |
n1 <- ns[[1]] |
207 | ! |
x2 <- xs[[2]] |
208 | ! |
n2 <- ns[[2]] |
209 | ! |
r1 <- rs[[1]] |
210 | ! |
r2 <- rs[[2]] |
211 | ||
212 | ! |
df_ref <- tibble(arm = c(ref, trt)) |
213 | ||
214 |
# a wide data frame contains event counts by arm in each subgroup
|
|
215 | ! |
df <- cbind(id = id, arm = arm, subgroups) %>% |
216 | ! |
filter(arm %in% c(ref, trt)) %>% |
217 | ! |
unique() %>% |
218 | ! |
tidyr::pivot_longer( |
219 | ! |
names_to = "strata", |
220 | ! |
cols = colnames(subgroups), |
221 | ! |
values_to = "value" |
222 |
) %>% |
|
223 | ! |
group_by(arm, .data$strata, .data$value) %>% |
224 | ! |
summarise(n = n()) %>% |
225 | ! |
full_join(df_ref, by = "arm") %>% |
226 | ! |
tidyr::replace_na(list(n = 0)) %>% |
227 | ! |
ungroup() %>% |
228 | ! |
tidyr::pivot_wider( |
229 | ! |
id_cols = c("strata", "value"), |
230 | ! |
names_from = "arm", |
231 | ! |
values_from = "n", |
232 | ! |
names_prefix = "count__", |
233 | ! |
values_fill = list(n = 0) |
234 |
)
|
|
235 | ||
236 |
# a wide data frame contains total counts by arm in each subgroup
|
|
237 | ! |
df_sl <- cbind(arm = arm_sl, subgroups_sl) %>% |
238 | ! |
filter(arm %in% c(ref, trt)) %>% |
239 | ! |
mutate(TOTAL = "Total") %>% |
240 | ! |
tidyr::pivot_longer( |
241 | ! |
names_to = "strata", |
242 | ! |
cols = c(colnames(subgroups_sl), "TOTAL"), |
243 | ! |
values_to = "value" |
244 |
) %>% |
|
245 | ! |
group_by(arm, .data$strata, .data$value) %>% |
246 | ! |
summarise(n = n()) %>% |
247 | ! |
ungroup() %>% |
248 | ! |
tidyr::pivot_wider( |
249 | ! |
id_cols = c("strata", "value"), |
250 | ! |
names_from = "arm", |
251 | ! |
values_from = "n", |
252 | ! |
names_prefix = "total__", |
253 | ! |
values_fill = list(n = 0) |
254 |
)
|
|
255 | ||
256 |
# calculate the risk difference and risk difference CI in each subgroup
|
|
257 | ! |
df <- df %>% |
258 | ! |
left_join(df_sl, by = c("strata", "value")) %>% |
259 | ! |
group_by(.data$strata, .data$value) %>% |
260 | ! |
mutate( |
261 | ! |
!!r1 := !!x1 / !!n1, |
262 | ! |
!!r2 := !!x2 / !!n2, |
263 | ! |
lower = DescTools::BinomDiffCI( |
264 | ! |
!!x1, !!n1, !!x2, !!n2, |
265 | ! |
conf_level,
|
266 | ! |
method = diff_ci_method |
267 | ! |
)[2], |
268 | ! |
upper = DescTools::BinomDiffCI( |
269 | ! |
!!x1, !!n1, !!x2, !!n2, |
270 | ! |
conf_level,
|
271 | ! |
method = diff_ci_method |
272 | ! |
)[3], |
273 | ! |
riskdiff = !!r1 - !!r2 |
274 |
) %>% |
|
275 | ! |
tidyr::pivot_longer( |
276 | ! |
matches("__"), |
277 | ! |
names_to = c(".value", "arm"), |
278 | ! |
names_sep = "__" |
279 |
) %>% |
|
280 | ! |
ungroup() %>% |
281 | ! |
tidyr::unite("level", "strata", "value", remove = FALSE, sep = "__") |
282 | ||
283 |
# create label for plotting
|
|
284 | ! |
level_format_df <- df %>% |
285 | ! |
select("strata", "value") %>% |
286 | ! |
unique() %>% |
287 | ! |
group_by(.data$strata) %>% |
288 | ! |
summarise(value = paste(c("Total", .data$value), collapse = ",")) %>% |
289 | ! |
tidyr::separate_rows("value", sep = ",") %>% |
290 | ! |
unique() %>% |
291 | ! |
tidyr::unite("level", "strata", "value", sep = "__", remove = FALSE) %>% |
292 | ! |
mutate(order = if_else(.data$strata == "TOTAL", integer(1), -row_number())) %>% |
293 | ! |
arrange(order) |
294 | ||
295 | ! |
if (is.null(subgroups_levels)) { |
296 | ! |
level_format_df <- level_format_df %>% |
297 | ! |
mutate(label = if_else( |
298 | ! |
.data$strata == "TOTAL", |
299 | ! |
"Overall",
|
300 | ! |
if_else(.data$value == "Total", .data$strata, paste0(stringr::str_c( |
301 | ! |
rep(" ", indent), |
302 | ! |
collapse = "" |
303 | ! |
), .data$value)) |
304 |
)) |
|
305 |
} else { |
|
306 | ! |
level_format_df <- level_format_df %>% |
307 | ! |
inner_join(label_df, by = "level") |
308 |
}
|
|
309 | ||
310 | ! |
df <- df %>% |
311 | ! |
semi_join(level_format_df, by = "level") |
312 | ||
313 | ! |
mytheme <- theme_osprey(fontsize = fontsize, blank = TRUE) |
314 | ||
315 | ! |
y_axis <- |
316 | ! |
scale_y_discrete( |
317 | ! |
limits = level_format_df$level, |
318 | ! |
breaks = level_format_df$level, |
319 | ! |
labels = level_format_df$label |
320 |
)
|
|
321 | ||
322 | ! |
df_risk <- df %>% |
323 | ! |
select("level", "lower", "upper", "riskdiff") %>% |
324 | ! |
unique() |
325 | ! |
if (xmax <= 0) { |
326 | ! |
xmax <- max(abs(df_risk$upper), abs(df_risk$lower), na.rm = TRUE) |
327 |
}
|
|
328 | ! |
p1 <- |
329 | ! |
ggplot(df_risk) + |
330 | ! |
geom_point( |
331 | ! |
aes(x = .data$riskdiff, y = .data$level), |
332 | ! |
size = fontsize |
333 |
) + |
|
334 | ! |
geom_vline( |
335 | ! |
data = NULL, |
336 | ! |
xintercept = 0, |
337 | ! |
linetype = 1, |
338 | ! |
color = "grey" |
339 |
) + |
|
340 | ! |
geom_errorbarh( |
341 | ! |
aes(xmin = .data$lower, xmax = .data$upper, y = .data$level), |
342 | ! |
height = 0.3 |
343 |
) + |
|
344 | ! |
mytheme + |
345 | ! |
theme( |
346 | ! |
axis.ticks.x = element_line(), |
347 | ! |
axis.line.x = element_line() |
348 |
) + |
|
349 | ! |
y_axis + |
350 | ! |
coord_cartesian(xlim = c(-xmax, xmax)) |
351 | ! |
p1_grob <- ggplotGrob(p1) |
352 | ||
353 | ! |
df_total <- df %>% |
354 | ! |
group_by(.data$level) %>% |
355 | ! |
summarise(n = sum(.data$total)) %>% |
356 | ! |
mutate(percent = n / length(arm_sl) * 100) |
357 | ||
358 | ! |
if (arm_n) { |
359 | ! |
df_byarm <- df %>% |
360 | ! |
group_by(.data$level, .data$arm) %>% |
361 | ! |
summarise(n = sum(.data$total)) %>% |
362 | ! |
tidyr::pivot_wider(names_from = arm, values_from = n) %>% |
363 | ! |
rename(n_trt = matches(trt), n_ref = matches(ref)) |
364 | ||
365 | ! |
df_total <- df_total %>% |
366 | ! |
left_join(df_byarm, by = "level") %>% |
367 | ! |
mutate( |
368 | ! |
percent_trt = .data$n_trt / .data$n * 100, |
369 | ! |
percent_ref = .data$n_ref / .data$n * 100 |
370 |
)
|
|
371 |
}
|
|
372 | ||
373 | ! |
p2 <- ggplot(df_total) + |
374 | ! |
geom_text(aes( |
375 | ! |
x = "Patients(%)", |
376 | ! |
y = .data$level, |
377 | ! |
label = sprintf("%i (%.1f)", n, .data$percent) |
378 | ! |
), size = fontsize) + |
379 | ! |
mytheme + |
380 | ! |
y_axis + |
381 | ! |
scale_x_discrete(position = "top") |
382 | ||
383 | ! |
p2_grob <- ggplotGrob(p2) |
384 | ||
385 | ! |
if (arm_n) { |
386 | ! |
p2_trt <- ggplot(df_total) + |
387 | ! |
geom_text(aes( |
388 | ! |
x = "TRT", |
389 | ! |
y = .data$level, |
390 | ! |
label = sprintf("%i", .data$n_trt) |
391 | ! |
), size = fontsize) + |
392 | ! |
mytheme + |
393 | ! |
theme(axis.text.y = element_blank()) + |
394 | ! |
y_axis + |
395 | ! |
scale_x_discrete(position = "top") |
396 | ||
397 | ! |
p2_ref <- ggplot(df_total) + |
398 | ! |
geom_text(aes( |
399 | ! |
x = "CONT", |
400 | ! |
y = .data$level, |
401 | ! |
label = sprintf("%i", .data$n_ref) |
402 | ! |
), size = fontsize) + |
403 | ! |
mytheme + |
404 | ! |
theme(axis.text.y = element_blank()) + |
405 | ! |
y_axis + |
406 | ! |
scale_x_discrete(position = "top") |
407 | ||
408 | ! |
p2_trt_grob <- ggplotGrob(p2_trt) |
409 | ! |
p2_ref_grob <- ggplotGrob(p2_ref) |
410 |
}
|
|
411 | ||
412 | ! |
df_ci <- df %>% |
413 | ! |
tidyr::pivot_wider( |
414 | ! |
names_from = "arm", |
415 | ! |
values_from = "risk", |
416 | ! |
id_cols = c("level", "lower", "upper", "riskdiff") |
417 |
) %>% |
|
418 | ! |
tidyr::pivot_longer( |
419 | ! |
names_to = "x", |
420 | ! |
values_to = "v", |
421 | ! |
cols = c("lower", "upper", "riskdiff", matches(trt), matches(ref)) |
422 |
) %>% |
|
423 | ! |
mutate(vlabel = sprintf("%.2f", .data$v)) |
424 | ||
425 | ||
426 | ! |
labels <- c("TRT", "CONT", "Diff", "Lower", "Upper") |
427 | ! |
p3 <- ggplot(df_ci) + |
428 | ! |
geom_text(aes(x = .data$x, y = .data$level, label = .data$vlabel), size = fontsize) + |
429 | ! |
mytheme + |
430 | ! |
y_axis + |
431 | ! |
scale_x_discrete( |
432 | ! |
limits = c(trt, ref, "riskdiff", "lower", "upper"), |
433 | ! |
labels = labels, |
434 | ! |
position = "top" |
435 |
)
|
|
436 | ||
437 | ! |
p3_grob <- ggplotGrob(p3) |
438 | ||
439 | ! |
grobs <- grob_parts(p1_grob, "axis-l") |
440 | ! |
grobs <- append(grobs, grob_parts(p2_grob, c("axis-t", "panel"))) |
441 | ! |
if (arm_n) { |
442 | ! |
grobs <- append(grobs, grob_parts(p2_trt_grob, c("axis-t", "panel"))) |
443 | ! |
grobs <- append(grobs, grob_parts(p2_ref_grob, c("axis-t", "panel"))) |
444 |
}
|
|
445 | ! |
grobs <- append(grobs, grob_parts(p1_grob, c("panel", "axis-b"))) |
446 | ! |
grobs <- append(grobs, grob_parts(p3_grob, c("axis-t", "panel"))) |
447 | ! |
less_risk <- grid::textGrob( |
448 | ! |
"Favor\nTreatment",
|
449 | ! |
just = "centre", |
450 | ! |
x = grid::unit(fontsize * .pt, "pt"), |
451 | ! |
gp = grid::gpar(fontsize = fontsize * .pt, fontface = "bold") |
452 |
)
|
|
453 | ! |
more_risk <- grid::textGrob( |
454 | ! |
"Favor\nControl",
|
455 | ! |
just = "centre", |
456 | ! |
x = grid::unit(1, "npc") - grid::unit(fontsize * .pt, "pt"), |
457 | ! |
gp = grid::gpar(fontsize = fontsize * .pt, fontface = "bold") |
458 |
)
|
|
459 | ! |
risk_label <- gridExtra::arrangeGrob(less_risk, more_risk, nrow = 1) |
460 | ! |
grobs <- append(grobs, list( |
461 | ! |
grid::textGrob( |
462 | ! |
"Risk Difference (CI)",
|
463 | ! |
just = "centre", |
464 | ! |
x = grid::unit(0.5, "npc"), |
465 | ! |
y = grid::unit(0.5, "npc"), |
466 | ! |
gp = grid::gpar(fontsize = fontsize * .pt, fontface = "bold") |
467 |
),
|
|
468 | ! |
risk_label
|
469 |
)) |
|
470 | ||
471 | ! |
widths <- if (arm_n) { |
472 | ! |
grid::unit.c( |
473 | ! |
grid::grobWidth(grobs[[1]]), |
474 | ! |
grid::unit( |
475 | ! |
c(14 * fontsize, rep(10 * fontsize, 2), 1, 50 * fontsize), |
476 | ! |
c(rep("pt", 3), "null", "pt") |
477 |
)
|
|
478 |
)
|
|
479 |
} else { |
|
480 | ! |
grid::unit.c( |
481 | ! |
grid::grobWidth(grobs[[1]]), |
482 | ! |
grid::unit( |
483 | ! |
c(14 * fontsize, 1, 50 * fontsize), |
484 | ! |
c("pt", "null", "pt") |
485 |
)
|
|
486 |
)
|
|
487 |
}
|
|
488 | ||
489 | ! |
heights <- if (arm_n) { |
490 | ! |
grid::unit.c( |
491 | ! |
grid::grobHeight(grobs[[10]]), |
492 | ! |
grid::unit(1, "null"), |
493 | ! |
rep(grid::grobHeight(grobs[[9]]), 3), |
494 | ! |
grid::unit(fontsize * .pt, "pt") |
495 |
)
|
|
496 |
} else { |
|
497 | ! |
grid::unit.c( |
498 | ! |
grid::grobHeight(grobs[[6]]), |
499 | ! |
grid::unit(1, "null"), |
500 | ! |
grid::grobHeight(grobs[[5]]), |
501 | ! |
grid::unit(fontsize * .pt * 3, "pt") |
502 |
)
|
|
503 |
}
|
|
504 | ||
505 | ! |
boldfont <- grid::gpar( |
506 | ! |
fontsize = fontsize * 4, |
507 | ! |
fontface = "bold", |
508 | ! |
lineheight = 1 |
509 |
)
|
|
510 | ! |
layout_matrix <- if (arm_n) { |
511 | ! |
rbind( |
512 | ! |
c(NA, 2, 4, 6, 12, 10), |
513 | ! |
c(1, 3, 5, 7, 8, 11), |
514 | ! |
c(NA, NA, NA, NA, 9, NA), |
515 | ! |
c(NA, NA, NA, NA, 13, NA) |
516 |
)
|
|
517 |
} else { |
|
518 | ! |
rbind( |
519 | ! |
c(NA, 2, 8, 6), |
520 | ! |
c(1, 3, 4, 7), |
521 | ! |
c(NA, NA, 5, NA), |
522 | ! |
c(NA, NA, 9, NA) |
523 |
)
|
|
524 |
}
|
|
525 | ||
526 | ! |
ret <- gridExtra::arrangeGrob( |
527 | ! |
grobs = grobs, |
528 | ! |
layout_matrix = layout_matrix, |
529 | ! |
heights = heights, |
530 | ! |
widths = widths, |
531 | ! |
clip = "on" |
532 |
)
|
|
533 | ! |
if (draw) { |
534 | ! |
grid::grid.draw(ret) |
535 |
}
|
|
536 | ! |
invisible(ret) |
537 |
}
|
1 |
#' Applies STREAM style filtering to datasets
|
|
2 |
#'
|
|
3 |
#' One of `slref` or `anl` need to be specified. The conversion from `SAS` code in filters dataset may not work in all
|
|
4 |
#' cases. In case of failure a sensible error message should be returned.
|
|
5 |
#'
|
|
6 |
#' @param slref The subject level data frame (typically `ADSL`)
|
|
7 |
#' @param anl The analysis data frame
|
|
8 |
#' @param suffix The suffix to apply in quotes (e.g. `"ITT_PFSINV"`)
|
|
9 |
#' @param slref_keep Variables to keep from `slref` (e.g. `c("REGION", "SEX")`)
|
|
10 |
#' @param usubjid The unique subject identifier variable in quotes (e.g. `"USUBJID"`)
|
|
11 |
#' @param filters The name of the filters dataset
|
|
12 |
#'
|
|
13 |
#' @return \code{dataframe} object
|
|
14 |
#' @author Iain Bennett
|
|
15 |
#' @export
|
|
16 |
#' @examples
|
|
17 |
#' ADSL <- osprey::rADSL
|
|
18 |
#' ADTTE <- osprey::rADTTE
|
|
19 |
#' filters <- as.data.frame(rbind(
|
|
20 |
#' c(ID = "IT", FLTTARGET = "SLREF", FLTWHERE = "where 1 eq 1"),
|
|
21 |
#' c(ID = "BIO", FLTTARGET = "SLREF", FLTWHERE = "where BMRKR1 ge 4.3"),
|
|
22 |
#' c(ID = "M", FLTTARGET = "SLREF", FLTWHERE = "where SEX eq 'M'"),
|
|
23 |
#' c(ID = "PFS", FLTTARGET = "ANL", FLTWHERE = "where PARAMCD eq 'PFS'"),
|
|
24 |
#' c(ID = "OS", FLTTARGET = "ANL", FLTWHERE = "where PARAMCD eq 'OS'")
|
|
25 |
#' ))
|
|
26 |
#'
|
|
27 |
#' ANL <- stream_filter(
|
|
28 |
#' slref = ADSL,
|
|
29 |
#' anl = ADTTE,
|
|
30 |
#' suffix = "IT_PFS_BIO",
|
|
31 |
#' filters = filters
|
|
32 |
#' )
|
|
33 |
stream_filter <- function(slref = NULL, anl = NULL, filters, suffix, slref_keep = NULL, usubjid = "USUBJID") { |
|
34 | ! |
actual_suffix <- NULL |
35 | ||
36 | ! |
if (is.null(anl) && is.null(slref)) { |
37 | ! |
stop("At least one of anl= or slref= must be provided") |
38 |
}
|
|
39 | ||
40 | ! |
if (is.null(anl)) { |
41 | ! |
anl <- slref |
42 |
}
|
|
43 | ||
44 | ! |
if (is.null(slref)) { |
45 | ! |
slref <- anl |
46 |
}
|
|
47 | ||
48 | ! |
asl_out <- slref |
49 | ! |
anl_out <- anl |
50 | ||
51 |
# step 1 get a list of filters
|
|
52 | ||
53 | ! |
filters_to_apply <- strsplit(suffix, split = "_", fixed = TRUE) %>% |
54 | ! |
unlist() |
55 | ||
56 | ! |
n_filters <- length(filters_to_apply) |
57 | ||
58 | ! |
for (i in 1:n_filters) { |
59 | ! |
this_filter <- filters_to_apply[i] |
60 | ||
61 |
# find filter meta data
|
|
62 | ||
63 | ! |
this_filter_df <- unique(dplyr::filter(filters, .data$ID == this_filter)) |
64 | ||
65 | ! |
if (nrow(this_filter_df) == 0) { |
66 | ! |
stop(paste("Filter", this_filter, "not found in filters")) |
67 |
}
|
|
68 | ||
69 | ! |
if (nrow(this_filter_df) > 1) { |
70 | ! |
warning(paste("Filter", this_filter, "is duplicated in filters")) |
71 | ! |
this_filter_df <- slice(this_filter_df, 1) |
72 |
}
|
|
73 | ||
74 |
# try and convert where clause from sas to R
|
|
75 | ! |
this_sasclause <- this_filter_df$FLTWHERE |
76 | ! |
this_rclause <- stream_filter_convwhere(this_sasclause) |
77 | ||
78 | ! |
msg1 <- paste("\nSAS code:", this_sasclause, "\nwas converted to\nR code:", this_rclause) |
79 | ||
80 |
# what is the target df?
|
|
81 | ||
82 | ! |
if (this_filter_df$FLTTARGET == "ANL") { |
83 | ! |
this_df <- anl_out |
84 |
}
|
|
85 | ||
86 | ! |
if (this_filter_df$FLTTARGET == "SLREF") { |
87 | ! |
this_df <- asl_out |
88 |
}
|
|
89 | ||
90 |
# try and apply the filtering
|
|
91 | ! |
new_df <- NULL |
92 | ||
93 | ! |
new_df <- try( |
94 | ! |
eval(parse(text = sprintf("dplyr::filter(this_df, %s)", this_rclause))), |
95 | ! |
silent = TRUE |
96 |
)
|
|
97 | ||
98 | ! |
if (is(new_df, "try-error")) { |
99 |
# failed - retain original dataset
|
|
100 | ! |
warning(paste("\nFilter ID=", this_filter, "was NOT applied.", msg1, "\n Error message:", new_df)) |
101 | ! |
cat(paste("\nFilter ID=", this_filter, "was NOT applied.", msg1, "\n Error message:", new_df)) |
102 | ! |
new_df <- this_df |
103 |
} else { |
|
104 |
# success
|
|
105 | ! |
msg2 <- paste0( |
106 | ! |
"\n",
|
107 | ! |
nrow(new_df), |
108 | ! |
" of ",
|
109 | ! |
nrow(this_df), |
110 | ! |
" observations selected from ", this_filter_df$FLTTARGET |
111 |
)
|
|
112 | ! |
cat(paste("\nFilter", this_filter, "applied", msg1, msg2, "\n")) |
113 | ! |
actual_suffix <- if (is.null(actual_suffix)) { |
114 | ! |
this_filter
|
115 |
} else { |
|
116 | ! |
paste(actual_suffix, this_filter, sep = "_") |
117 |
}
|
|
118 |
}
|
|
119 | ||
120 |
# update the output data sets
|
|
121 | ! |
if (this_filter_df$FLTTARGET == "ANL") { |
122 | ! |
anl_out <- new_df |
123 | ! |
} else if (this_filter_df$FLTTARGET == "SLREF") { |
124 | ! |
asl_out <- new_df |
125 |
}
|
|
126 |
}
|
|
127 | ||
128 |
# finished filtering - combine results data
|
|
129 |
# what variables to keep from SLREF?
|
|
130 | ! |
slref_keep <- if (is.null(slref_keep)) { |
131 | ! |
usubjid
|
132 |
} else { |
|
133 | ! |
unique(c(slref_keep, usubjid)) |
134 |
}
|
|
135 | ||
136 |
# keep these variables only
|
|
137 | ! |
asl_out <- eval(parse(text = sprintf("dplyr::transmute(asl_out, %s)", paste(slref_keep, collapse = ",")))) |
138 | ||
139 |
# use inner join to apply both slref and anl restrictions
|
|
140 | ! |
rc <- inner_join(asl_out, anl_out, by = usubjid) |
141 | ||
142 |
# report out what was applied in case of errors
|
|
143 | ! |
actual_suffix <- ifelse(is.null(actual_suffix), " ", actual_suffix) |
144 | ||
145 | ! |
if (actual_suffix == suffix) { |
146 | ! |
cat(paste0("\nSuffix ", suffix, " was applied")) |
147 |
} else { |
|
148 | ! |
cat(paste0("\nNot all filters applied. \n", actual_suffix, " was applied instead of ", suffix)) |
149 |
}
|
|
150 | ! |
cat(paste0("\n", nrow(rc), " of ", nrow(anl), " observations selected from ANL\n")) |
151 | ||
152 |
# return the filtered dataset
|
|
153 | ! |
return(rc) |
154 |
}
|
|
155 | ||
156 |
#' Replicates the use of index function in `SAS` for logic options
|
|
157 |
#'
|
|
158 |
#' Assumption is that use in filters is to only resolve true vs false
|
|
159 |
#' Primarily for use with stream_filter and related `stream_filter_convwhere` functions
|
|
160 |
#' @param string1 The string to search within - can be a vector
|
|
161 |
#' @param string2 The string to search for - must have length 1
|
|
162 |
#'
|
|
163 |
#' @return \code{boolean} indicator
|
|
164 |
#' @author Iain Bennett
|
|
165 |
#' @export
|
|
166 |
#'
|
|
167 |
#' @examples
|
|
168 |
#' AEACN <- c("DRUG MODIFIED", "DRUG STOPPED", "DOSE/DRUG MODIFIED")
|
|
169 |
#' stream_filter_index(AEACN, "DRUG MODIFIED")
|
|
170 |
stream_filter_index <- function(string1, string2) { |
|
171 | ! |
rc <- regexpr(string2, string1, fixed = TRUE) |
172 | ! |
rc <- ifelse(rc == -1, FALSE, TRUE) |
173 | ! |
return(rc) |
174 |
}
|
|
175 | ||
176 | ||
177 |
#' Convert `SAS` code to R code
|
|
178 |
#'
|
|
179 |
#' Will convert following `SAS` operators: `eq, =, le, lt, ge, gt, index`
|
|
180 |
#' Will convert following logic: and, or, ()
|
|
181 |
#' Will convert all unquoted values to upper case (assumed to be variable names)
|
|
182 |
#' All quoted values will be returned with single quotes - may fail if have quotes within quotes
|
|
183 |
#' @param x a character string of `SAS` code
|
|
184 |
#'
|
|
185 |
#' @return a character string of R code
|
|
186 |
#' @author Iain Bennett
|
|
187 |
#' @export
|
|
188 |
#'
|
|
189 |
#' @examples
|
|
190 |
#' stream_filter_convwhere(x = "where X in (1 2 3 4) and Y gt 4 ")
|
|
191 |
#' stream_filter_convwhere(x = "where X = \"fred\" and Y gt 4 ")
|
|
192 |
stream_filter_convwhere <- function(x) { |
|
193 |
# convert double quotes to single quotes. May fail if quoted values exist.
|
|
194 | ! |
this_rclause <- gsub("\"", "'", x, fixed = TRUE) |
195 | ||
196 |
# convert non quoted values to upper case
|
|
197 | ||
198 | ! |
this_rclause_quotes <- strsplit(paste0(" ", this_rclause, " "), split = "'", fixed = TRUE) %>% |
199 | ! |
unlist() |
200 | ||
201 | ! |
inquotes <- rep(c(0, 1), length.out = length(this_rclause_quotes)) |
202 | ||
203 | ! |
for (j in seq_along(inquotes)) { |
204 |
# try and convert logic outside quotes
|
|
205 | ! |
if (inquotes[j] == 0) { |
206 | ! |
this_rclause_quotes[j] <- toupper(this_rclause_quotes[j]) |
207 | ! |
this_rclause_quotes[j] <- gsub("=", "==", this_rclause_quotes[j], fixed = TRUE) |
208 | ! |
this_rclause_quotes[j] <- gsub(" EQ ", " == ", this_rclause_quotes[j], fixed = TRUE) |
209 | ! |
this_rclause_quotes[j] <- gsub(" NE ", " != ", this_rclause_quotes[j], fixed = TRUE) |
210 | ! |
this_rclause_quotes[j] <- gsub(" LE ", " <= ", this_rclause_quotes[j], fixed = TRUE) |
211 | ! |
this_rclause_quotes[j] <- gsub(" LT ", " < ", this_rclause_quotes[j], fixed = TRUE) |
212 | ! |
this_rclause_quotes[j] <- gsub(" GE ", " >= ", this_rclause_quotes[j], fixed = TRUE) |
213 | ! |
this_rclause_quotes[j] <- gsub(" GT ", " > ", this_rclause_quotes[j], fixed = TRUE) |
214 | ! |
this_rclause_quotes[j] <- gsub(" AND ", " & ", this_rclause_quotes[j], fixed = TRUE) |
215 | ! |
this_rclause_quotes[j] <- gsub(" OR ", " || ", this_rclause_quotes[j], fixed = TRUE) |
216 | ! |
this_rclause_quotes[j] <- gsub(" IN ", " %in% c", this_rclause_quotes[j], fixed = TRUE) |
217 | ! |
this_rclause_quotes[j] <- gsub("WHERE ", " ", this_rclause_quotes[j], fixed = TRUE) |
218 | ! |
this_rclause_quotes[j] <- gsub("INDEX(", " stream_filter_index(", this_rclause_quotes[j], fixed = TRUE) |
219 | ! |
this_rclause_quotes[j] <- gsub("UPCASE(", " toupper(", this_rclause_quotes[j], fixed = TRUE) |
220 | ! |
this_rclause_quotes[j] <- gsub("DATEPART(", " as.Date(", this_rclause_quotes[j], fixed = TRUE) |
221 |
}
|
|
222 |
}
|
|
223 | ||
224 |
# collapse back to have quoted
|
|
225 | ! |
this_rclause <- paste(this_rclause_quotes, collapse = "'") |
226 | ||
227 |
# if contains an in statement need to ensure commas exist
|
|
228 | ! |
if (grepl(" %in% c", this_rclause, fixed = TRUE)) { |
229 |
# get the clause (assume only 1 per filter...)
|
|
230 | ! |
temp1_str <- strsplit(this_rclause, split = " %in% c(", fixed = TRUE) %>% |
231 | ! |
unlist() |
232 | ||
233 | ! |
if (length(temp1_str) != 2) { |
234 | ! |
stop("ERROR - function can't handle multiple IN operators.") |
235 |
} else { |
|
236 | ! |
left_str <- temp1_str[1] |
237 | ! |
in_right_str <- temp1_str[2] |
238 | ||
239 |
# find quoted items bracket
|
|
240 | ! |
temp2_str <- strsplit(in_right_str, split = "'", fixed = TRUE) %>% |
241 | ! |
unlist() |
242 | ! |
inquotes <- rep(c(0, 1), length.out = length(temp2_str)) |
243 | ||
244 |
# find first not quoted right bracket
|
|
245 | ||
246 | ! |
right_idxv <- which(inquotes == 0 & grepl(")", temp2_str, fixed = TRUE)) |
247 | ! |
right_idxc <- regexpr(")", temp2_str[right_idxv], fixed = TRUE) |
248 | ||
249 | ! |
temp3_str <- temp2_str |
250 | ||
251 | ! |
temp3_str[right_idxv] <- substr(temp2_str[right_idxv], right_idxc, nchar(temp2_str[right_idxv])) |
252 | ||
253 | ! |
right_str <- temp3_str[right_idxv:length(temp3_str)] %>% |
254 | ! |
paste(collapse = "'") |
255 | ||
256 | ! |
in_idx <- regexpr(right_str, in_right_str, fixed = TRUE) %>% |
257 | ! |
as.numeric() |
258 | ||
259 | ! |
in_str <- substr(in_right_str, 1, in_idx - 1) |
260 | ||
261 |
# now have left.str, in.str and right.str that contain seperate code parts
|
|
262 |
# need to check the list of in and remove any commas to later replace between each element
|
|
263 |
# first get any unquoted spaces or commas and split these
|
|
264 | ||
265 | ! |
temp4_str <- strsplit(in_str, split = "'", fixed = TRUE) %>% |
266 | ! |
unlist() |
267 | ! |
inquotes <- rep(c(0, 1), length.out = length(temp4_str)) |
268 | ||
269 | ! |
unquoted <- temp4_str[which(inquotes == 0)] |
270 | ! |
quoted_items <- temp4_str[which(inquotes == 1)] |
271 | ||
272 |
# seperate any items unqouted
|
|
273 | ! |
temp5_str <- strsplit(unquoted, split = ",", fixed = TRUE) %>% |
274 | ! |
unlist() %>% |
275 | ! |
strsplit(split = " ", fixed = TRUE) %>% |
276 | ! |
unlist() |
277 | ||
278 | ! |
unquoted_items <- temp5_str[which(!(temp5_str %in% ""))] |
279 | ||
280 |
# should now have two vectors of strings
|
|
281 |
# unquoted.items and quoted.items
|
|
282 |
# first add the quoted items back in quoted.items <- unquoted.items
|
|
283 | ! |
if (length(quoted_items) > 0) { |
284 | ! |
quoted_items <- paste0("'", quoted_items, "'") |
285 |
}
|
|
286 | ||
287 |
# now collapse both strings adding commas
|
|
288 | ||
289 | ! |
all_items <- c(quoted_items, unquoted_items) %>% |
290 | ! |
paste(collapse = " , ") |
291 | ||
292 |
# rebuild the complete code piece
|
|
293 | ! |
this_rclause <- paste0(left_str, " %in% c(", all_items, right_str) |
294 |
}
|
|
295 |
}
|
|
296 | ! |
return(this_rclause) |
297 |
}
|
1 |
#' `Swimlane` Plot
|
|
2 |
#'
|
|
3 |
#' `Swimlane` plot is often used in Early Development (ED) and displays individual
|
|
4 |
#' patient bar plot with markers of events and patient level annotation
|
|
5 |
#'
|
|
6 |
#' @param bar_id vector of IDs to identify each bar
|
|
7 |
#' @param bar_length numeric vector to be plotted as length for each bar
|
|
8 |
#' @param sort_by vector to sort bars
|
|
9 |
#' @param col_by vector to color bars
|
|
10 |
#' @param marker_id vector of IDs to identify markers within each bar. Default is the same as bar_id.
|
|
11 |
#' @param marker_pos numeric vector to specify position for each marker point
|
|
12 |
#' @param marker_shape vector to specify shape for markers
|
|
13 |
#' @param marker_shape_opt aesthetic values to map shape values (named vector to map shape values to each name)
|
|
14 |
#' @param marker_color vector to specify color for markers
|
|
15 |
#' @param marker_color_opt aesthetic values to map shape values (named vector to map shape values to each name)
|
|
16 |
#' @param anno_txt dataframe of subject-level variables to be displayed as annotation on the left
|
|
17 |
#' @param xref_line numeric vector to plot reference lines
|
|
18 |
#' @param xtick_at optional break interval of bar length axis
|
|
19 |
#' @param xlab label for bar length
|
|
20 |
#' @param title string to be displayed as plot title
|
|
21 |
#'
|
|
22 |
#' @template author_qit3
|
|
23 |
#' @return plot object
|
|
24 |
#'
|
|
25 |
#' @export
|
|
26 |
#'
|
|
27 |
#' @examplesIf require("nestcolor")
|
|
28 |
#' # Example 1
|
|
29 |
#' library(dplyr)
|
|
30 |
#' library(nestcolor)
|
|
31 |
#'
|
|
32 |
#' ADSL <- osprey::rADSL[1:20, ]
|
|
33 |
#' ADRS <- filter(rADRS, PARAMCD == "OVRINV")
|
|
34 |
#' ANL <- left_join(ADSL, ADRS, by = c("STUDYID", "USUBJID"), multiple = "all")
|
|
35 |
#' anno_txt <- ADSL[, c("ARMCD", "SEX")]
|
|
36 |
#'
|
|
37 |
#' g_swimlane(
|
|
38 |
#' bar_id = ADSL$USUBJID,
|
|
39 |
#' bar_length = as.integer(ADSL$TRTEDTM - ADSL$TRTSDTM),
|
|
40 |
#' sort_by = ADSL$ARM,
|
|
41 |
#' col_by = ADSL$ARM,
|
|
42 |
#' marker_id = ANL$USUBJID,
|
|
43 |
#' marker_pos = ANL$ADY,
|
|
44 |
#' marker_shape = ANL$AVALC,
|
|
45 |
#' marker_shape_opt = c("CR" = 16, "PR" = 17, "SD" = 18, "PD" = 15, "NE" = 4),
|
|
46 |
#' marker_color = NULL,
|
|
47 |
#' marker_color_opt = NULL,
|
|
48 |
#' anno_txt = anno_txt,
|
|
49 |
#' xref_line = c(50, 100),
|
|
50 |
#' xtick_at = waiver(),
|
|
51 |
#' xlab = "Time from First Treatment (Day)",
|
|
52 |
#' title = "Swimlane Plot"
|
|
53 |
#' )
|
|
54 |
#'
|
|
55 |
#' # Example 2
|
|
56 |
#' library(dplyr)
|
|
57 |
#' library(nestcolor)
|
|
58 |
#'
|
|
59 |
#' ADSL <- osprey::rADSL[1:20, ]
|
|
60 |
#' ADRS <- osprey::rADRS
|
|
61 |
#'
|
|
62 |
#' anno_txt_vars <- c("ARMCD", "SEX", "COUNTRY")
|
|
63 |
#' anno_txt <- ADSL[, anno_txt_vars]
|
|
64 |
#'
|
|
65 |
#' # markers from ADRS
|
|
66 |
#' ADRS <- dplyr::filter(ADRS, PARAMCD == "OVRINV") %>% select(USUBJID, ADY, AVALC)
|
|
67 |
#'
|
|
68 |
#' # markers from ADSL - discontinuation
|
|
69 |
#' ADS <- ADSL %>%
|
|
70 |
#' dplyr::filter(EOSSTT == "Discontinued" | DCSREAS != "") %>%
|
|
71 |
#' select(USUBJID, EOSDY, DCSREAS) %>%
|
|
72 |
#' dplyr::rename(ADY = EOSDY, AVALC = DCSREAS)
|
|
73 |
#'
|
|
74 |
#' # combine ADRS with ADS records as one data for markers and join with ADSL
|
|
75 |
#' ANL <- inner_join(ADSL, rbind(ADRS, ADS), by = "USUBJID", multiple = "all")
|
|
76 |
#'
|
|
77 |
#' g_swimlane(
|
|
78 |
#' bar_id = sub(".*-", "", ADSL$USUBJID),
|
|
79 |
#' bar_length = as.integer(ADSL$TRTEDTM - ADSL$TRTSDTM),
|
|
80 |
#' sort_by = NULL,
|
|
81 |
#' col_by = ADSL$ARMCD,
|
|
82 |
#' marker_id = sub(".*-", "", ANL$USUBJID),
|
|
83 |
#' marker_pos = ANL$ADY,
|
|
84 |
#' marker_shape = ANL$AVALC,
|
|
85 |
#' marker_shape_opt = c(
|
|
86 |
#' "CR" = 16, "PR" = 17, "SD" = 18, "PD" = 15, "NE" = 0,
|
|
87 |
#' "Adverse Event" = 7, "Death" = 8, "Physician Decision" = 9, "Progressive Disease" = 10,
|
|
88 |
#' "Symptomatic Deterioation" = 11, "Withdrawal by Subject" = 12
|
|
89 |
#' ),
|
|
90 |
#' marker_color = ANL$AVALC,
|
|
91 |
#' marker_color_opt = c(
|
|
92 |
#' "CR" = "green", "PR" = "blue", "SD" = "yellow", "PD" = "red",
|
|
93 |
#' "NE" = "grey", "Adverse Event" = "orange", "Death" = "black", "Physician Decision" = "navy",
|
|
94 |
#' "Progressive Disease" = "purple", "Symptomatic Deterioation" = "cyan",
|
|
95 |
#' "Withdrawal by Subject" = "darkred"
|
|
96 |
#' ),
|
|
97 |
#' anno_txt = anno_txt,
|
|
98 |
#' xref_line = c(50, 100),
|
|
99 |
#' xtick_at = waiver(),
|
|
100 |
#' xlab = "Time from First Treatment (Day)",
|
|
101 |
#' title = "Swimlane Plot"
|
|
102 |
#' )
|
|
103 |
g_swimlane <- function(bar_id, |
|
104 |
bar_length,
|
|
105 |
sort_by = NULL, |
|
106 |
col_by = NULL, |
|
107 |
marker_id = NULL, |
|
108 |
marker_pos = NULL, |
|
109 |
marker_shape = NULL, |
|
110 |
marker_shape_opt = NULL, |
|
111 |
marker_color = NULL, |
|
112 |
marker_color_opt = NULL, |
|
113 |
anno_txt = NULL, |
|
114 |
xref_line = NULL, |
|
115 |
xtick_at = waiver(), |
|
116 |
xlab,
|
|
117 |
title) { |
|
118 |
# check data
|
|
119 | ! |
if (!is.null(sort_by)) { |
120 | ! |
check_same_N(bar_id = bar_id, bar_length = bar_length, sort_by = sort_by) |
121 |
}
|
|
122 | ! |
if (!is.null(col_by)) { |
123 | ! |
check_same_N(bar_id = bar_id, bar_length = bar_length, col_by = col_by) |
124 |
}
|
|
125 | ||
126 | ! |
if (!is.null(marker_id) && length(which(!marker_id %in% bar_id)) > 0) { |
127 | ! |
stop("marker_id ", marker_id[which(!marker_id %in% bar_id)], " is not in bar_id") |
128 |
}
|
|
129 | ||
130 | ! |
if (!is.null(marker_id) && !is.null(marker_pos)) { |
131 | ! |
check_same_N(marker_id = marker_id, marker_pos = marker_pos) |
132 |
}
|
|
133 | ! |
if (!is.null(marker_id) && !is.null(marker_shape)) { |
134 | ! |
check_same_N(marker_id = marker_id, marker_shape = marker_shape) |
135 |
}
|
|
136 | ! |
if (!is.null(marker_id) && !is.null(marker_color)) { |
137 | ! |
check_same_N(marker_id = marker_id, marker_color = marker_color) |
138 |
}
|
|
139 | ! |
if (!is.null(xref_line) && (!is.numeric(xref_line) || length(xref_line) == 0)) { |
140 | ! |
stop("xref_line must be a non-empty numeric vector or NULL") |
141 |
}
|
|
142 | ||
143 |
# data for plot
|
|
144 | ! |
bar_data <- data.frame( |
145 | ! |
bar_id,
|
146 | ! |
bar_length,
|
147 | ! |
sort_by = if (is.null(sort_by)) "x" else to_n(sort_by, length(bar_length)), |
148 | ! |
col_by = if (is.null(col_by)) "x" else to_n(col_by, length(bar_length)) |
149 |
)
|
|
150 | ||
151 |
# data for marker
|
|
152 | ! |
if (is.null(marker_id)) marker_id <- bar_id |
153 | ! |
marker_data <- data.frame( |
154 | ! |
marker_id,
|
155 | ! |
marker_pos = if (is.null(marker_pos)) "x" else to_n(marker_pos, length(marker_id)), |
156 | ! |
marker_shape = if (is.null(marker_shape)) "x" else to_n(marker_shape, length(marker_id)), |
157 | ! |
marker_color = if (is.null(marker_color)) "x" else to_n(marker_color, length(marker_id)) |
158 |
)
|
|
159 | ||
160 |
# if sort by a variable, reorder bar_id by sort var and then bar length; otherwise sort by bar length
|
|
161 | ! |
if (!is.null(sort_by)) { |
162 | ! |
bar_data$bar_id <- factor(bar_data$bar_id, |
163 | ! |
levels = rev(unique(bar_data$bar_id[order(bar_data$sort_by, -bar_data$bar_length)])) |
164 |
)
|
|
165 |
} else { |
|
166 | ! |
bar_data$bar_id <- factor(bar_data$bar_id, |
167 | ! |
levels = rev(unique(bar_data$bar_id[order(-bar_data$bar_length)])) |
168 |
)
|
|
169 |
}
|
|
170 | ||
171 |
# labeling
|
|
172 | ! |
xlabel <- deparse(substitute(bar_length)) |
173 | ! |
xlab <- if (is.null(xlab)) xlabel else xlab |
174 | ||
175 |
# plot bar plot first
|
|
176 | ! |
p <- ggplot(data = bar_data, aes(x = bar_id, y = bar_length)) + |
177 | ! |
geom_bar(stat = "identity", aes(fill = col_by)) + |
178 | ! |
coord_flip(xlim = c(1, length(unique(bar_id)) + 1)) + |
179 | ! |
theme_bw() + |
180 | ! |
theme( |
181 | ! |
panel.background = element_blank(), |
182 | ! |
panel.grid = element_blank(), |
183 | ! |
axis.line = element_line(colour = "black"), |
184 | ! |
axis.text.y = element_blank(), |
185 | ! |
axis.title.y = element_blank() |
186 |
) + |
|
187 |
# Note ylab as we have coord_flip above
|
|
188 | ! |
ylab(xlab) |
189 | ||
190 | ! |
if (is.null(col_by)) { |
191 | ! |
p <- p + guides(fill = "none") |
192 |
} else { |
|
193 | ! |
p <- p + |
194 | ! |
guides(fill = guide_legend("Bar Color", order = 1, ncol = 1)) + |
195 | ! |
theme( |
196 | ! |
legend.title = element_text(size = 8), |
197 | ! |
legend.text = element_text(size = 8), |
198 | ! |
legend.key = element_rect(fill = NA), |
199 | ! |
legend.key.size = grid::unit(1, "line"), |
200 | ! |
legend.spacing.y = grid::unit(0, "cm"), |
201 | ! |
legend.key.height = grid::unit(1, "line") |
202 |
)
|
|
203 |
}
|
|
204 | ||
205 | ! |
limits_x <- NULL |
206 | ||
207 |
# plot marker
|
|
208 | ! |
if (!is.null(marker_pos)) { |
209 | ! |
p <- p + geom_point( |
210 | ! |
data = marker_data, |
211 | ! |
aes(x = marker_id, y = marker_pos, shape = marker_shape, color = marker_color), |
212 | ! |
size = 2.5, na.rm = TRUE |
213 |
)
|
|
214 | ! |
limits_x <- c(0, max(bar_length, marker_pos, na.rm = TRUE) + 5) |
215 | ||
216 | ! |
if (!is.null(marker_shape)) { |
217 | ! |
p <- p + guides(shape = guide_legend("Marker Shape", order = 2)) |
218 |
} else { |
|
219 | ! |
p <- p + guides(shape = "none") |
220 |
}
|
|
221 | ||
222 | ! |
if (!is.null(marker_color)) { |
223 | ! |
p <- p + guides(color = guide_legend("Marker Color", order = 3)) |
224 |
} else { |
|
225 | ! |
p <- p + guides(color = "none") |
226 |
}
|
|
227 | ||
228 | ! |
p <- p + |
229 | ! |
scale_shape_manual( |
230 | ! |
name = "Marker Shape", |
231 | ! |
breaks = levels(factor(marker_data$marker_shape)), |
232 | ! |
values = if (!is.null(marker_shape_opt)) marker_shape_opt else c(15:25, 0:14) |
233 |
)
|
|
234 | ||
235 | ! |
if (is.null(marker_color_opt)) { |
236 | ! |
if (!is.null(getOption("ggplot2.discrete.colour"))) { |
237 | ! |
marker_color_opt <- getOption("ggplot2.discrete.colour")[-seq_len(length(unique(col_by)))] |
238 |
} else { |
|
239 | ! |
marker_color_opt <- c("x" = "black") |
240 |
}
|
|
241 |
}
|
|
242 | ||
243 | ! |
p <- p + scale_color_manual( |
244 | ! |
name = "Marker Color", |
245 | ! |
breaks = levels(factor(marker_data$marker_color)), |
246 | ! |
values = marker_color_opt |
247 |
)
|
|
248 |
}
|
|
249 | ||
250 |
# plot reference lines
|
|
251 | ! |
if (!is.null(xref_line)) { |
252 | ! |
x_axis_min <- pmax(ggplot_build(p)$layout$panel_params[[1]]$x.range[1], 0) |
253 | ! |
x_axis_max <- pmax(ggplot_build(p)$layout$panel_params[[1]]$x.range[2], 0) |
254 | ! |
xref_line_min <- min(xref_line) |
255 | ! |
xref_line_max <- max(xref_line) |
256 | ! |
min_res <- min(c(limits_x[1], x_axis_min, xref_line_min)) |
257 | ! |
max_res <- max(c(limits_x[2], x_axis_max, xref_line_max)) |
258 | ! |
p <- p + geom_hline(yintercept = xref_line, linetype = "dashed", color = "red") + |
259 | ! |
scale_y_continuous( |
260 | ! |
limits = c(min_res, max_res), |
261 | ! |
breaks = xtick_at, |
262 | ! |
expand = c(`if`(min_res == xref_line_min, .01, 0), `if`(max_res == xref_line_max, .01, 0)) |
263 |
)
|
|
264 |
} else { |
|
265 | ! |
if (!is.null(limits_x)) { |
266 | ! |
p <- p + scale_y_continuous(limits = limits_x, breaks = xtick_at, expand = c(0, 0)) |
267 |
}
|
|
268 |
}
|
|
269 | ||
270 |
# plot title and labels
|
|
271 | ! |
if (!is.null(title)) { |
272 | ! |
p <- p + |
273 | ! |
labs(title = title) + |
274 | ! |
theme(plot.title = element_text(face = "bold")) |
275 |
}
|
|
276 | ||
277 | ||
278 |
# create annotation as a separate table plot
|
|
279 | ! |
if (is.null(anno_txt)) { |
280 | ! |
t <- data.frame(bar_id, bar_length, |
281 | ! |
sort_by = if (is.null(sort_by)) "x" else to_n(sort_by, length(bar_length)) |
282 |
)
|
|
283 |
} else { |
|
284 | ! |
t <- data.frame(bar_id, bar_length, |
285 | ! |
sort_by = if (is.null(sort_by)) "x" else to_n(sort_by, length(bar_length)), |
286 | ! |
anno_txt
|
287 |
)
|
|
288 |
}
|
|
289 | ||
290 |
# if sort by a variable, reorder bar_id; otherwise sort by bar length
|
|
291 | ! |
if (!is.null(sort_by)) { |
292 | ! |
t <- t[with(t, order(sort_by, -bar_length, levels(as.factor(bar_id)))), -c(2, 3)] |
293 |
} else { |
|
294 | ! |
t <- t[with(t, order(-bar_length, levels(as.factor(bar_id)))), -c(2, 3)] |
295 |
}
|
|
296 | ||
297 | ! |
t <- as.data.frame(t) |
298 | ! |
colnames(t)[1] <- " " |
299 | ||
300 | ! |
my_theme <- gridExtra::ttheme_default( |
301 | ! |
core = list( |
302 | ! |
bg_params = list(fill = NA, col = NA), |
303 | ! |
fg_params = list(cex = 0.8) |
304 |
),
|
|
305 | ! |
colhead = list( |
306 | ! |
bg_params = list(fill = NA, col = NA), |
307 | ! |
fg_params = list(cex = 0.8) |
308 |
)
|
|
309 |
)
|
|
310 | ! |
tb <- gridExtra::tableGrob(t, rows = NULL, theme = my_theme) |
311 | ! |
tb$heights <- grid::unit(rep(1 / nrow(tb), nrow(tb)), "null") |
312 | ||
313 |
# grab plot and table as one plot
|
|
314 | ! |
g0 <- ggplotGrob(p) |
315 | ! |
g1 <- gtable::gtable_add_cols(g0, sum(tb$widths), 0) |
316 | ! |
g <- gtable::gtable_add_grob(g1, tb, t = g1$layout[g1$layout$name == "panel", 1], l = 1) |
317 | ||
318 | ! |
grid::grid.newpage() |
319 | ! |
grid::grid.draw(g) |
320 | ! |
invisible(g) |
321 |
}
|
1 |
#' Waterfall Plot
|
|
2 |
#'
|
|
3 |
#' Waterfall plot is often used in Early Development (ED) to present each individual patient’s best
|
|
4 |
#' response to a particular drug based on a parameter.
|
|
5 |
#'
|
|
6 |
#' @param bar_id (`vector`)\cr contains IDs to identify each bar
|
|
7 |
#' @param bar_height numeric vector to be plotted as height of each bar
|
|
8 |
#' @param sort_by (`vector`)\cr used to sort bars, default is `NULL` in which case bars are ordered
|
|
9 |
#' by decreasing height
|
|
10 |
#' @param col_by (`vector`)\cr used to color bars, default is `NULL` in which case bar_id is taken if
|
|
11 |
#' the argument \code{bar_color_opt} is provided
|
|
12 |
#' @param bar_color_opt (`vector`)\cr
|
|
13 |
#' aesthetic values to map color values (named vector to map color values to each name).
|
|
14 |
#' If not `NULL`, please make sure this contains all possible values for \code{col_by} values,
|
|
15 |
#' otherwise default `ggplot` color will be assigned, please note that `NULL` needs to be specified
|
|
16 |
#' in this case
|
|
17 |
#' @param anno_txt (`dataframe`)\cr
|
|
18 |
#' contains subject-level variables to be displayed as annotation below the waterfall plot,
|
|
19 |
#' default is `NULL`
|
|
20 |
#' @param href_line (`numeric vector`)\cr to plot horizontal reference lines, default is `NULL`
|
|
21 |
#' @param facet_by (`vector`)\cr to facet plot and annotation table, default is `NULL`
|
|
22 |
#' @param show_datavalue (`boolean`)\cr controls whether value of bar height is shown, default is \code{TRUE}
|
|
23 |
#' @param add_label (`vector`)\cr of one subject-level variable to be added to each bar except for bar_height,
|
|
24 |
#' default is `NULL`
|
|
25 |
#' @param gap_point (`numeric`)\cr value for adding bar break when some bars are significantly higher than
|
|
26 |
#' others, default is `NULL`
|
|
27 |
#' @param ytick_at (`numeric`)\cr optional bar height axis interval, default is 20
|
|
28 |
#' @param y_label (`string`)\cr label for bar height axis, default is "Best % Change from Baseline"
|
|
29 |
#' @param title (`string`)\cr displayed as plot title, default is "Waterfall Plot"
|
|
30 |
#'
|
|
31 |
#' @author Xuefeng Hou (houx14) \email{houx14@gene.com}
|
|
32 |
#' @template author_qit3
|
|
33 |
#'
|
|
34 |
#' @return plot object
|
|
35 |
#'
|
|
36 |
#' @export
|
|
37 |
#'
|
|
38 |
#' @examplesIf require("nestcolor")
|
|
39 |
#' library(tidyr)
|
|
40 |
#' library(dplyr)
|
|
41 |
#' library(nestcolor)
|
|
42 |
#'
|
|
43 |
#' g_waterfall(
|
|
44 |
#' bar_id = letters[1:3], bar_height = c(3, 5, -1),
|
|
45 |
#' bar_color_opt = c("red", "green", "blue")
|
|
46 |
#' )
|
|
47 |
#'
|
|
48 |
#' # Example 1
|
|
49 |
#' ADSL <- osprey::rADSL[1:15, ]
|
|
50 |
#' ADRS <- osprey::rADRS %>%
|
|
51 |
#' filter(USUBJID %in% ADSL$USUBJID)
|
|
52 |
#' ADTR <- osprey::rADTR %>%
|
|
53 |
#' filter(USUBJID %in% ADSL$USUBJID) %>%
|
|
54 |
#' select(USUBJID, PCHG) %>%
|
|
55 |
#' group_by(USUBJID) %>%
|
|
56 |
#' slice(which.min(PCHG))
|
|
57 |
#'
|
|
58 |
#' TR_SL <- inner_join(ADSL, ADTR, by = "USUBJID", multiple = "all")
|
|
59 |
#'
|
|
60 |
#' SUB_ADRS <- ADRS %>%
|
|
61 |
#' filter(PARAMCD == "BESRSPI" | PARAMCD == "INVET") %>%
|
|
62 |
#' select(USUBJID, PARAMCD, AVALC, AVISIT, ADY) %>%
|
|
63 |
#' spread(PARAMCD, AVALC)
|
|
64 |
#'
|
|
65 |
#' ANL <- TR_SL %>%
|
|
66 |
#' left_join(SUB_ADRS, by = "USUBJID", multiple = "all") %>%
|
|
67 |
#' mutate(TRTDURD = as.integer(TRTEDTM - TRTSDTM) + 1)
|
|
68 |
#'
|
|
69 |
#' anno_txt_vars <- c("TRTDURD", "BESRSPI", "INVET", "SEX", "BMRKR2")
|
|
70 |
#'
|
|
71 |
#' g_waterfall(
|
|
72 |
#' bar_height = ANL$PCHG,
|
|
73 |
#' bar_id = sub(".*-", "", ANL$USUBJID),
|
|
74 |
#' col_by = ANL$SEX,
|
|
75 |
#' sort_by = ANL$ARM,
|
|
76 |
#' # bar_color_opt = c("F" = "red", "M" = "green", "U" = "blue"),
|
|
77 |
#' anno_txt = ANL[, anno_txt_vars],
|
|
78 |
#' facet_by = NULL,
|
|
79 |
#' href_line = c(-30, 20),
|
|
80 |
#' add_label = ANL$BESRSPI,
|
|
81 |
#' ytick_at = 20,
|
|
82 |
#' gap_point = NULL,
|
|
83 |
#' show_datavalue = TRUE,
|
|
84 |
#' y_label = "Best % Change from Baseline",
|
|
85 |
#' title = "Waterfall Plot"
|
|
86 |
#' )
|
|
87 |
#'
|
|
88 |
#' # Example 2 facetting
|
|
89 |
#' anno_txt_vars <- c("BESRSPI", "INVET")
|
|
90 |
#'
|
|
91 |
#' g_waterfall(
|
|
92 |
#' bar_id = sub(".*-", "", ANL$USUBJID),
|
|
93 |
#' bar_height = ANL$PCHG,
|
|
94 |
#' sort_by = ANL$COUNTRY,
|
|
95 |
#' col_by = ANL$SEX,
|
|
96 |
#' bar_color_opt = c("F" = "tomato", "M" = "skyblue3", "U" = "darkgreen"),
|
|
97 |
#' anno_txt = ANL[, anno_txt_vars],
|
|
98 |
#' facet_by = ANL$STRATA2,
|
|
99 |
#' href_line = c(-30, 20),
|
|
100 |
#' add_label = ANL$BESRSPI,
|
|
101 |
#' ytick_at = 20,
|
|
102 |
#' gap_point = 260,
|
|
103 |
#' y_label = "Best % Change from Baseline",
|
|
104 |
#' title = "Waterfall Plot"
|
|
105 |
#' )
|
|
106 |
#'
|
|
107 |
#' # Example 3 extreme value
|
|
108 |
#' ANL$PCHG[3] <- 99
|
|
109 |
#' ANL$PCHG[5] <- 199
|
|
110 |
#' ANL$PCHG[7] <- 599
|
|
111 |
#' ANL$BESRSPI[3] <- "PD"
|
|
112 |
#' ANL$BESRSPI[5] <- "PD"
|
|
113 |
#' ANL$BESRSPI[7] <- "PD"
|
|
114 |
#'
|
|
115 |
#' g_waterfall(
|
|
116 |
#' bar_id = sub(".*-", "", ANL$USUBJID),
|
|
117 |
#' bar_height = ANL$PCHG,
|
|
118 |
#' sort_by = ANL$ARM,
|
|
119 |
#' col_by = ANL$SEX,
|
|
120 |
#' bar_color_opt = c("F" = "tomato", "M" = "skyblue3", "U" = "darkgreen"),
|
|
121 |
#' anno_txt = ANL[, anno_txt_vars],
|
|
122 |
#' facet_by = NULL,
|
|
123 |
#' href_line = c(-30, 20),
|
|
124 |
#' add_label = ANL$BESRSPI,
|
|
125 |
#' ytick_at = 20,
|
|
126 |
#' gap_point = 260,
|
|
127 |
#' y_label = "Best % Change from Baseline",
|
|
128 |
#' title = "Waterfall Plot"
|
|
129 |
#' )
|
|
130 |
#'
|
|
131 |
g_waterfall <- function(bar_id, |
|
132 |
bar_height,
|
|
133 |
sort_by = NULL, |
|
134 |
col_by = NULL, |
|
135 |
bar_color_opt = NULL, |
|
136 |
anno_txt = NULL, |
|
137 |
href_line = NULL, |
|
138 |
facet_by = NULL, |
|
139 |
show_datavalue = TRUE, |
|
140 |
add_label = NULL, |
|
141 |
gap_point = NULL, |
|
142 |
ytick_at = 20, |
|
143 |
y_label = "Best % Change from Baseline", |
|
144 |
title = "Waterfall Plot") { |
|
145 |
# check data
|
|
146 | ! |
check_input_length <- c(nrow(data.frame(bar_id)), nrow(data.frame(bar_height))) |
147 | ||
148 | ! |
if (length(unique(check_input_length)) > 1) { |
149 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
150 |
}
|
|
151 | ||
152 | ! |
if (any(check_input_length == 0)) { |
153 | ! |
stop("invalid arguments: check that inputs are not null") |
154 |
}
|
|
155 | ||
156 | ! |
if (!is.null(bar_color_opt)) { |
157 | ! |
if (is.null(col_by)) { |
158 | ! |
col_by <- bar_id |
159 |
} else { |
|
160 | ! |
ls1 <- levels(factor(col_by)) |
161 | ! |
ls2 <- names(bar_color_opt) |
162 | ! |
if (length(intersect(ls1, ls2)) == 0) { |
163 | ! |
stop("invalid argument: check that the col_by and bar_color_opt have overlapping categories") |
164 | ! |
} else if (length(ls2) < length(ls1)) { |
165 | ! |
stop("invalid argument: More categories in col_by than the ones listed in bar_color_opt") |
166 |
}
|
|
167 |
}
|
|
168 |
}
|
|
169 | ||
170 | ! |
if (!is.null(sort_by)) check_same_N(bar_id = bar_id, bar_height = bar_height, sort_by = sort_by) |
171 | ! |
if (!is.null(col_by)) check_same_N(bar_id = bar_id, bar_height = bar_height, col_by = col_by) |
172 | ||
173 | ! |
facet_plot <- function(bar_id = bar_id, |
174 | ! |
bar_height = bar_height, |
175 | ! |
sort_by = sort_by, |
176 | ! |
col_by = col_by, |
177 | ! |
bar_color_opt = bar_color_opt, |
178 | ! |
anno_txt = anno_txt, |
179 | ! |
href_line = href_line, |
180 | ! |
facet_by = facet_by, |
181 | ! |
show_datavalue = show_datavalue, |
182 | ! |
add_label = add_label, |
183 | ! |
gap_point = gap_point, |
184 | ! |
ytick_at = ytick_at, |
185 | ! |
y_label = y_label, |
186 | ! |
title = title) { |
187 |
# Data for plot
|
|
188 | ! |
bar_data <- data.frame( |
189 | ! |
bar_id,
|
190 | ! |
bar_height,
|
191 | ! |
add_label = if (is.null(add_label)) to_n("x", length(bar_height)) else to_n(add_label, length(bar_height)), |
192 | ! |
sort_by = if (is.null(sort_by)) to_n("x", length(bar_height)) else to_n(sort_by, length(bar_height)), |
193 | ! |
col_by = if (is.null(col_by)) to_n("x", length(bar_height)) else to_n(col_by, length(bar_height)) |
194 |
)
|
|
195 | ||
196 |
# if sort by a variable, reorder bar_id by sort var and then bar length; otherwise sort by bar length
|
|
197 | ! |
if (!is.null(sort_by)) { |
198 | ! |
bar_data$bar_id <- factor( |
199 | ! |
bar_data$bar_id, |
200 | ! |
levels = unique(bar_data$bar_id[order(bar_data$sort_by, bar_data$bar_height, decreasing = TRUE)]) |
201 |
)
|
|
202 |
} else { |
|
203 | ! |
bar_data$bar_id <- factor( |
204 | ! |
bar_data$bar_id, |
205 | ! |
levels = unique(bar_data$bar_id[order(bar_data$bar_height, decreasing = TRUE)]) |
206 |
)
|
|
207 |
}
|
|
208 | ||
209 |
# bar_id has already been sorted based on bar_height so keeping the 1st entry is equivalent to keeping
|
|
210 |
# the best % change from baseline
|
|
211 | ! |
bar_data <- bar_data %>% distinct(bar_id, .keep_all = TRUE) |
212 | ||
213 |
# plot bar plot
|
|
214 | ! |
if (is.null(gap_point)) { |
215 | ! |
ybreaks <- seq( |
216 | ! |
ytick_at * floor(min(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
217 | ! |
ytick_at * ceiling(max(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
218 | ! |
by = ytick_at |
219 |
)
|
|
220 | ||
221 | ! |
p <- ggplot(data = bar_data, aes(x = bar_id, y = bar_height)) + |
222 | ! |
geom_col(position = "identity", aes(fill = col_by)) + |
223 | ! |
scale_y_continuous(breaks = ybreaks) + |
224 | ! |
scale_x_discrete(expand = expansion(add = 0.5)) + |
225 | ! |
geom_hline(yintercept = 0, colour = "black") + |
226 | ! |
theme_bw() + |
227 | ! |
theme( |
228 | ! |
panel.background = element_blank(), |
229 | ! |
panel.grid = element_blank(), |
230 | ! |
axis.line = element_line(colour = "black"), |
231 | ! |
axis.text.x = element_text(angle = 90, hjust = 0, vjust = 0.5), |
232 | ! |
axis.title.x = element_blank() |
233 |
) + |
|
234 | ! |
ylab(y_label) |
235 | ! |
} else if (all(is.na(bar_data$bar_height))) { |
236 | ! |
ybreaks <- seq(ytick_at * -2, ytick_at * 2, by = ytick_at) |
237 | ||
238 | ! |
p <- ggplot(data = bar_data, aes(x = bar_id, y = bar_height)) + |
239 | ! |
geom_col(position = "identity", aes(fill = col_by)) + |
240 | ! |
scale_y_continuous(breaks = ybreaks) + |
241 | ! |
scale_x_discrete(expand = expansion(add = 0.5)) + |
242 | ! |
geom_hline(yintercept = 0, colour = "black") + |
243 | ! |
theme_bw() + |
244 | ! |
theme( |
245 | ! |
panel.background = element_blank(), |
246 | ! |
panel.grid = element_blank(), |
247 | ! |
axis.line = element_line(colour = "black"), |
248 | ! |
axis.text.x = element_text(angle = 90, hjust = 0, vjust = 0.5), |
249 | ! |
axis.title.x = element_blank() |
250 |
) + |
|
251 | ! |
ylab(y_label) |
252 | ! |
} else if (max(bar_data$bar_height, na.rm = TRUE) <= gap_point) { |
253 | ! |
ybreaks <- seq( |
254 | ! |
ytick_at * floor(min(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
255 | ! |
ytick_at * ceiling(max(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
256 | ! |
by = ytick_at |
257 |
)
|
|
258 | ||
259 | ! |
p <- ggplot(data = bar_data, aes(x = bar_id, y = bar_height)) + |
260 | ! |
geom_col(position = "identity", aes(fill = col_by)) + |
261 | ! |
scale_y_continuous(breaks = ybreaks) + |
262 | ! |
scale_x_discrete(expand = expansion(add = 0.5)) + |
263 | ! |
geom_hline(yintercept = 0, colour = "black") + |
264 | ! |
theme_bw() + |
265 | ! |
theme( |
266 | ! |
panel.background = element_blank(), |
267 | ! |
panel.grid = element_blank(), |
268 | ! |
axis.line = element_line(colour = "black"), |
269 | ! |
axis.text.x = element_text(angle = 90, hjust = 0, vjust = 0.5), |
270 | ! |
axis.title.x = element_blank() |
271 |
) + |
|
272 | ! |
ylab(y_label) |
273 |
} else { |
|
274 | ! |
length_cut <- ytick_at * |
275 | ! |
floor(0.8 * (min(bar_data$bar_height[bar_data$bar_height > gap_point], na.rm = TRUE) - gap_point) / ytick_at) |
276 | ||
277 | ! |
cut_fun <- function(x) { |
278 | ! |
if (is.na(x)) { |
279 | ! |
x <- x |
280 | ! |
} else if (x > gap_point) { |
281 | ! |
x <- x - length_cut |
282 |
} else { |
|
283 | ! |
x <- x |
284 |
}
|
|
285 |
}
|
|
286 | ||
287 | ! |
bar_data$new_bar_height <- sapply(bar_data$bar_height, cut_fun) |
288 | ||
289 | ! |
ylabel1 <- seq( |
290 | ! |
ytick_at * floor(min(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
291 | ! |
ytick_at * floor(gap_point / ytick_at), |
292 | ! |
by = ytick_at |
293 |
)
|
|
294 | ! |
ylabel2 <- seq( |
295 | ! |
ytick_at * floor((gap_point + length_cut) / ytick_at), |
296 | ! |
ytick_at * ceiling(max(bar_data$bar_height, na.rm = TRUE) / ytick_at), |
297 | ! |
by = ytick_at |
298 |
)
|
|
299 | ||
300 | ! |
ybreaks <- seq( |
301 | ! |
ytick_at * floor(min(bar_data$new_bar_height, na.rm = TRUE) / ytick_at), |
302 | ! |
ytick_at * ceiling(max(bar_data$new_bar_height, na.rm = TRUE) / ytick_at), |
303 | ! |
by = ytick_at |
304 |
)
|
|
305 | ||
306 | ! |
if (length(ylabel1) + length(ylabel2) == length(ybreaks)) { |
307 | ! |
ylabels <- c(ylabel1, ylabel2) |
308 |
} else { |
|
309 | ! |
dif <- length(ylabel1) + length(ylabel2) - length(ybreaks) |
310 | ! |
ylabel2 <- ylabel2[-(1:dif)] # nolint |
311 | ! |
ylabels <- c(ylabel1, ylabel2) |
312 |
}
|
|
313 | ||
314 | ! |
p <- ggplot(data = bar_data, aes(x = bar_id, y = .data$new_bar_height)) + |
315 | ! |
geom_col(position = "identity", aes(fill = col_by)) + |
316 | ! |
geom_rect(aes(xmin = 0.5, xmax = length(bar_id), ymin = gap_point, ymax = gap_point + 3), fill = "white") + |
317 | ! |
scale_y_continuous(breaks = ybreaks, labels = ylabels) + |
318 | ! |
scale_x_discrete(expand = expansion(add = 0.5)) + |
319 | ! |
geom_hline(yintercept = 0, colour = "black") + |
320 | ! |
theme_bw() + |
321 | ! |
theme( |
322 | ! |
panel.background = element_blank(), |
323 | ! |
panel.grid = element_blank(), |
324 | ! |
axis.line = element_line(colour = "black"), |
325 | ! |
axis.text.x = element_text(angle = 90, hjust = 0, vjust = 0.5), |
326 | ! |
axis.title.x = element_blank() |
327 |
) + |
|
328 | ! |
ylab(y_label) |
329 |
}
|
|
330 | ||
331 | ! |
if (show_datavalue == TRUE) { |
332 | ! |
p <- p + geom_text( |
333 | ! |
label = format(bar_data$bar_height, digits = 1), |
334 | ! |
size = 2.5, |
335 | ! |
vjust = ifelse(bar_data$bar_height >= 0, -0.5, 1.5) |
336 |
)
|
|
337 |
}
|
|
338 | ||
339 | ! |
if (!is.null(add_label)) { |
340 | ! |
p <- p + geom_text( |
341 | ! |
aes(x = bar_id, y = 0, label = add_label), |
342 | ! |
size = 3, |
343 | ! |
vjust = ifelse(bar_data$bar_height >= 0, 1.5, -0.5) |
344 |
)
|
|
345 |
}
|
|
346 | ||
347 | ! |
if (is.null(col_by)) { |
348 | ! |
p <- p + guides(fill = "none") |
349 |
} else { |
|
350 | ! |
p <- p + guides(fill = guide_legend("Bar Color", order = 1, ncol = 1)) + |
351 | ! |
theme( |
352 | ! |
legend.title = element_text(size = 9), |
353 | ! |
legend.text = element_text(size = 9), |
354 | ! |
legend.key = element_rect(fill = NA) |
355 |
)
|
|
356 |
}
|
|
357 | ||
358 | ! |
if (is.null(bar_color_opt) && !is.null(getOption("ggplot2.discrete.colour"))) { |
359 | ! |
bar_color_opt <- getOption("ggplot2.discrete.colour") |
360 |
}
|
|
361 | ! |
if (!is.null(bar_color_opt)) { |
362 | ! |
p <- p + scale_fill_manual(values = bar_color_opt) |
363 |
}
|
|
364 | ||
365 |
# plot reference lines
|
|
366 | ! |
if (!is.null(href_line)) { |
367 | ! |
p <- p + geom_hline(yintercept = href_line, linetype = "dashed", color = "red") |
368 |
}
|
|
369 | ||
370 |
# add plot title
|
|
371 | ! |
if (is.null(facet_by)) { |
372 | ! |
p <- p + |
373 | ! |
labs(title = title) + |
374 | ! |
theme(plot.title = element_text(face = "bold")) |
375 |
} else { |
|
376 | ! |
p <- p + |
377 | ! |
labs(title = paste(title, "-", as.character(unique(facet_by)))) + |
378 | ! |
theme(plot.title = element_text(face = "bold")) |
379 |
}
|
|
380 | ||
381 | ! |
if (!is.null(anno_txt)) { |
382 | ! |
t_anno <- data.frame( |
383 | ! |
bar_id,
|
384 | ! |
bar_height,
|
385 | ! |
sort_by = if (is.null(sort_by)) to_n("x", length(bar_height)) else to_n(sort_by, length(bar_height)), |
386 | ! |
anno_txt
|
387 |
)
|
|
388 | ||
389 |
# if sort by a variable, reorder bar_id; otherwise sort by bar length
|
|
390 | ! |
if (!is.null(sort_by)) { |
391 | ! |
t_anno$bar_id <- factor( |
392 | ! |
t_anno$bar_id, |
393 | ! |
levels = unique(t_anno$bar_id[order(t_anno$sort_by, t_anno$bar_height, decreasing = TRUE)]) |
394 |
)
|
|
395 |
} else { |
|
396 | ! |
t_anno$bar_id <- factor( |
397 | ! |
t_anno$bar_id, |
398 | ! |
levels = unique(t_anno$bar_id[order(t_anno$bar_height, decreasing = TRUE)]) |
399 |
)
|
|
400 |
}
|
|
401 | ||
402 |
# bar_id has already been sorted based on bar_height so keeping the 1st entry is equivalent to keeping
|
|
403 |
# the best % change from baseline
|
|
404 | ! |
t_anno <- t_anno %>% distinct(bar_id, .keep_all = TRUE) |
405 | ! |
t_anno <- t_anno[order(t_anno$bar_id), ] |
406 | ! |
t_anno <- t_anno[, -c(1, 2, 3)] |
407 | ||
408 | ! |
t_anno <- t(t_anno) |
409 | ||
410 | ! |
my_theme <- gridExtra::ttheme_default( |
411 | ! |
core = list(bg_params = list(fill = NA, col = NA), fg_params = list(cex = 0.8)), |
412 | ! |
rowhead = list(bg_params = list(fill = NA, col = NA), fg_params = list(cex = 0.8)), |
413 | ! |
padding = grid::unit(c(0, 4), "mm") |
414 |
)
|
|
415 | ||
416 | ! |
tb <- gridExtra::tableGrob( |
417 | ! |
t_anno,
|
418 | ! |
rows = NULL, |
419 | ! |
cols = NULL, |
420 | ! |
theme = my_theme |
421 |
)
|
|
422 | ||
423 | ! |
tb$widths <- grid::unit(rep(1 / (ncol(tb)), ncol(tb)), "null") |
424 | ! |
tb <- gtable::gtable_add_grob( |
425 | ! |
tb,
|
426 | ! |
grobs = grid::rectGrob(gp = grid::gpar(fill = NA, lwd = 2)), |
427 | ! |
t = 1, b = nrow(tb), l = 1, r = ncol(tb) |
428 |
)
|
|
429 | ||
430 | ! |
t_anno_name <- names(anno_txt) |
431 | ! |
tb_rowname <- gridExtra::tableGrob( |
432 | ! |
t_anno_name,
|
433 | ! |
rows = NULL, |
434 | ! |
cols = NULL, |
435 | ! |
theme = gridExtra::ttheme_minimal( |
436 | ! |
core = list(bg_params = list(fill = NA, col = NA), fg_params = list(cex = 0.8)) |
437 |
)
|
|
438 |
)
|
|
439 | ||
440 |
# grab plot and table as one plot
|
|
441 | ! |
g0 <- ggplotGrob(p) |
442 | ! |
g1 <- gtable::gtable_add_rows(g0, sum(tb$heights), pos = -1) |
443 | ! |
g2 <- gtable::gtable_add_grob( |
444 | ! |
g1,
|
445 | ! |
tb,
|
446 | ! |
t = -1, |
447 | ! |
l = g1$layout[g1$layout$name == "panel", 2], |
448 | ! |
r = g1$layout[g1$layout$name == "panel", 4] |
449 |
)
|
|
450 | ! |
g3 <- gtable::gtable_add_cols(g2, tb_rowname$widths, pos = 0) |
451 | ! |
g <- gtable::gtable_add_grob(g3, tb_rowname, t = -1, l = 2) |
452 |
} else { |
|
453 | ! |
p <- p + |
454 | ! |
theme(axis.title.x = element_text()) + |
455 | ! |
labs(x = "Unique Subject ID") |
456 | ! |
g <- ggplotGrob(p) |
457 |
}
|
|
458 | ! |
g
|
459 |
}
|
|
460 | ||
461 | ! |
if (is.null(facet_by)) { |
462 | ! |
gt <- facet_plot( |
463 | ! |
bar_id = bar_id, |
464 | ! |
bar_height = bar_height, |
465 | ! |
sort_by = sort_by, |
466 | ! |
col_by = col_by, |
467 | ! |
bar_color_opt = bar_color_opt, |
468 | ! |
anno_txt = anno_txt, |
469 | ! |
href_line = href_line, |
470 | ! |
facet_by = facet_by, |
471 | ! |
show_datavalue = show_datavalue, |
472 | ! |
add_label = add_label, |
473 | ! |
gap_point = gap_point, |
474 | ! |
ytick_at = ytick_at, |
475 | ! |
y_label = y_label, |
476 | ! |
title = title |
477 |
)
|
|
478 |
} else { |
|
479 | ! |
facet_by <- factor(facet_by) |
480 | ! |
g_list <- rep(list(NA), length(levels(facet_by))) |
481 | ! |
for (i in seq_along(levels(facet_by))) { |
482 | ! |
facet_level <- levels(facet_by)[i] |
483 | ! |
g_list[[i]] <- facet_plot( |
484 | ! |
bar_id = bar_id[facet_by == facet_level], |
485 | ! |
bar_height = bar_height[facet_by == facet_level], |
486 | ! |
sort_by = sort_by[facet_by == facet_level], |
487 | ! |
col_by = col_by[facet_by == facet_level], |
488 | ! |
bar_color_opt = bar_color_opt, |
489 | ! |
anno_txt = anno_txt[facet_by == facet_level, , drop = FALSE], |
490 | ! |
href_line = href_line, |
491 | ! |
facet_by = facet_by[facet_by == facet_level], |
492 | ! |
show_datavalue = show_datavalue, |
493 | ! |
add_label = add_label[facet_by == facet_level], |
494 | ! |
gap_point = gap_point, |
495 | ! |
ytick_at = ytick_at, |
496 | ! |
y_label = y_label, |
497 | ! |
title = title |
498 |
)
|
|
499 |
}
|
|
500 | ||
501 | ! |
gt <- gridExtra::grid.arrange(grobs = g_list, ncol = 1, nrow = length(levels(facet_by))) |
502 |
}
|
|
503 | ||
504 | ! |
grid::grid.newpage() |
505 | ! |
grid::grid.draw(gt) |
506 | ! |
invisible(gt) |
507 |
}
|
1 |
#' Output decorated grob (`gTree`) objects as PDF
|
|
2 |
#'
|
|
3 |
#' This is an utility function to output a decorated grob (`gTree`) object
|
|
4 |
#'
|
|
5 |
#' @param grobs a grid grob (`gTree`) object, optionally `NULL` if only a grob with
|
|
6 |
#' the decoration should be shown.
|
|
7 |
#' @param outpath specify full path to output pdf to `BCE` or `BEE`
|
|
8 |
#' @param pagesize name of `pagesize` (print size) and orientation, accepted values include
|
|
9 |
#' \code{"a4.landscape"}, \code{"a4.portrait"}, \code{"letter.portrait"} and
|
|
10 |
#' \code{"letter.landscape"} (default)
|
|
11 |
#'
|
|
12 |
#' @return a pdf file
|
|
13 |
#' @seealso [grobs2pdf()]
|
|
14 |
#' @export
|
|
15 |
#'
|
|
16 |
#' @author Chendi Liao (liaoc10) \email{chendi.liao@roche.com}
|
|
17 |
#'
|
|
18 |
#' @examples
|
|
19 |
#' \dontrun{
|
|
20 |
#' library(ggplot2)
|
|
21 |
#'
|
|
22 |
#' g <- list(
|
|
23 |
#' ggplotGrob(
|
|
24 |
#' ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
|
|
25 |
#' geom_point()
|
|
26 |
#' ),
|
|
27 |
#' ggplotGrob(
|
|
28 |
#' ggplot(iris, aes(x = Sepal.Length, y = Petal.Length, color = Species)) +
|
|
29 |
#' geom_point()
|
|
30 |
#' ),
|
|
31 |
#' ggplotGrob(
|
|
32 |
#' ggplot(iris, aes(x = Sepal.Length, y = Petal.Width, color = Species)) +
|
|
33 |
#' geom_point()
|
|
34 |
#' )
|
|
35 |
#' )
|
|
36 |
#'
|
|
37 |
#' # output to pdf
|
|
38 |
#' as_pdf(g, "~/example_aspdf1.pdf")
|
|
39 |
#' }
|
|
40 |
as_pdf <- function(grobs, |
|
41 |
outpath,
|
|
42 |
pagesize = "letter.landscape") { |
|
43 | ! |
paper_sizes <- paper_size(pagesize) |
44 | ! |
paper_width <- paper_sizes[1] |
45 | ! |
paper_height <- paper_sizes[2] |
46 | ||
47 |
# Output to PDF
|
|
48 | ! |
grDevices::pdf(outpath, width = paper_width, height = paper_height) |
49 | ||
50 | ! |
lapply(grobs, function(x) { |
51 | ! |
grid::grid.newpage() |
52 | ! |
grid::grid.draw(x) |
53 |
}) |
|
54 | ||
55 | ! |
grDevices::dev.off() |
56 |
}
|
|
57 | ||
58 |
paper_size <- function(pagesize) { |
|
59 | ! |
if (pagesize == "a4.landscape") { |
60 | ! |
paper_width <- 11.7 |
61 | ! |
paper_height <- 8.3 |
62 | ! |
} else if (pagesize == "a4.portrait") { |
63 | ! |
paper_width <- 8.3 |
64 | ! |
paper_height <- 11.7 |
65 | ! |
} else if (pagesize == "letter.portrait") { |
66 | ! |
paper_width <- 8.5 |
67 | ! |
paper_height <- 11 |
68 | ! |
} else if (pagesize == "letter.landscape") { |
69 | ! |
paper_width <- 11 |
70 | ! |
paper_height <- 8.5 |
71 |
} else { |
|
72 | ! |
paper_width <- 11 |
73 | ! |
paper_height <- 8.5 |
74 |
}
|
|
75 | ! |
return(c(paper_width, paper_height)) |
76 |
}
|
|
77 | ||
78 |
#' Decorate grob (`gTree`) objects then outputs as `IDM` compatible PDF
|
|
79 |
#'
|
|
80 |
#' This is an utility function to decorated grob (`gTree`) object with titles and
|
|
81 |
#' footnotes in accordance with `IDM` specification and export as PDF file with
|
|
82 |
#' full path to program and the output for easy tracking and archiving.
|
|
83 |
#'
|
|
84 |
#' @param grobs A grid grob (`gTree`) object, optionally `NULL` if only a
|
|
85 |
#' grob with the decoration should be shown
|
|
86 |
#' @param titles Vector of character strings. Vector elements are separated by a
|
|
87 |
#' newline and strings are wrapped according to the page with
|
|
88 |
#' @param footnotes Vector of character string. Same rules as for \code{titles}
|
|
89 |
#' @param progpath Specify the full path to the R program that generate the
|
|
90 |
#' grobs and the PDF
|
|
91 |
#' @param outpath Specify full path to output pdf to `BCE` or `BEE`
|
|
92 |
#' @param fontsize Base font size used in pdf, default set to 9. Font size for
|
|
93 |
#' title is set to \code{fontsize} + 1 (default = 10) and for footnotes set to
|
|
94 |
#' \code{fontsize} - 1 (default = 8)
|
|
95 |
#' @param pagesize name of paper size and orientation, accepted values include
|
|
96 |
#' \code{"a4.landscape"}, \code{"a4.portrait"}, \code{"letter.portrait"} and
|
|
97 |
#' \code{"letter.landscape"} (default)
|
|
98 |
#'
|
|
99 |
#' @return a pdf file
|
|
100 |
#'
|
|
101 |
#' @export
|
|
102 |
#'
|
|
103 |
#' @author Chendi Liao (liaoc10) \email{chendi.liao@roche.com}
|
|
104 |
#'
|
|
105 |
#' @examples
|
|
106 |
#' \dontrun{
|
|
107 |
#' library(ggplot2)
|
|
108 |
#'
|
|
109 |
#' g <- list(
|
|
110 |
#' ggplotGrob(
|
|
111 |
#' ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
|
|
112 |
#' geom_point()
|
|
113 |
#' ),
|
|
114 |
#' ggplotGrob(
|
|
115 |
#' ggplot(iris, aes(x = Sepal.Length, y = Petal.Length, color = Species)) +
|
|
116 |
#' geom_point()
|
|
117 |
#' ),
|
|
118 |
#' ggplotGrob(
|
|
119 |
#' ggplot(iris, aes(x = Sepal.Length, y = Petal.Width, color = Species)) +
|
|
120 |
#' geom_point()
|
|
121 |
#' )
|
|
122 |
#' )
|
|
123 |
#'
|
|
124 |
#' grobs2pdf(
|
|
125 |
#' grobs = g,
|
|
126 |
#' titles = "Visualization of Iris Data",
|
|
127 |
#' footnotes = "This is a footnote",
|
|
128 |
#' progpath = "~/example_prog.R",
|
|
129 |
#' outpath = "~/example_grobs2pdf.pdf"
|
|
130 |
#' )
|
|
131 |
#' }
|
|
132 |
#'
|
|
133 |
grobs2pdf <- function(grobs, |
|
134 |
titles,
|
|
135 |
footnotes,
|
|
136 |
progpath,
|
|
137 |
outpath,
|
|
138 |
fontsize = 9, |
|
139 |
pagesize = "letter.landscape") { |
|
140 | ! |
if (!requireNamespace("tern", quietly = TRUE)) { |
141 | ! |
stop("This function requires the R package tern to be available - please install the package.") |
142 |
}
|
|
143 | ||
144 |
# Loads rapid.base.settings list and a few other
|
|
145 | ||
146 |
# Page type (default is letter.landscape, options=a4.portrait, a4.landscape, letter.portrait, letter.landscape)
|
|
147 | ! |
if (pagesize == "a4.landscape") { |
148 | ! |
top_margin <- 1.44 |
149 | ! |
bottom_margin <- 0.83 |
150 | ! |
left_margin <- 1.3 |
151 | ! |
right_margin <- 1.32 |
152 | ! |
} else if (pagesize == "a4.portrait") { |
153 | ! |
top_margin <- 1.32 |
154 | ! |
bottom_margin <- 1.3 |
155 | ! |
left_margin <- 1.44 |
156 | ! |
right_margin <- 0.83 |
157 | ! |
} else if (pagesize == "letter.portrait") { |
158 | ! |
top_margin <- 0.95 |
159 | ! |
bottom_margin <- 0.98 |
160 | ! |
left_margin <- 1.5 |
161 | ! |
right_margin <- 1.0 |
162 | ! |
} else if (pagesize == "letter.landscape") { |
163 | ! |
top_margin <- 1.5 |
164 | ! |
bottom_margin <- 1.0 |
165 | ! |
left_margin <- 0.98 |
166 | ! |
right_margin <- 0.95 |
167 |
} else { |
|
168 | ! |
top_margin <- 1.5 |
169 | ! |
bottom_margin <- 1.0 |
170 | ! |
left_margin <- 0.98 |
171 | ! |
right_margin <- 0.95 |
172 |
}
|
|
173 | ||
174 | ! |
paper_sizes <- paper_size(pagesize) |
175 | ! |
paper_width <- paper_sizes[1] |
176 | ! |
paper_height <- paper_sizes[2] |
177 | ||
178 |
## Adding log text to footnotes
|
|
179 | ! |
log1 <- paste0("Program: ", progpath, "; Output: ", outpath) # nolint |
180 | ! |
log2 <- paste0(format(Sys.time(), "%d%b%Y %H:%M %Z"), ", generated by ", Sys.getenv("USER")) # nolint |
181 | ! |
logtext <- paste(mget(ls(pattern = "log")), collapse = "\n") |
182 | ||
183 |
## Make the grobs
|
|
184 | ! |
if (!is.list(grobs)) { |
185 | ! |
grobs <- list(grobs) |
186 |
}
|
|
187 | ||
188 |
## Decorate grobs
|
|
189 | ! |
dg <- tern::decorate_grob_set( |
190 | ! |
grobs = grobs, |
191 | ! |
titles = titles, |
192 | ! |
footnotes = paste(footnotes, logtext, sep = "\n\n"), |
193 | ! |
outer_margins = grid::unit(c(0, 0, 0, 0), "lines"), |
194 | ! |
padding = grid::unit(0.5, "lines"), |
195 | ! |
gp_titles = grid::gpar(fontsize = fontsize + 1, fontface = 2, lineheight = 1), |
196 | ! |
gp_footnotes = grid::gpar(fontsize = fontsize - 1, fontface = 1, lineheight = 1), |
197 | ! |
gp = grid::gpar(fontsize = fontsize), |
198 | ! |
vp = grid::viewport( |
199 | ! |
x = grid::unit(left_margin, "inches"), |
200 | ! |
y = grid::unit(bottom_margin, "inches"), |
201 | ! |
width = grid::unit(paper_width - left_margin - right_margin, "inches"), |
202 | ! |
height = grid::unit(paper_height - top_margin - bottom_margin, "inches"), |
203 | ! |
just = c("left", "bottom"), |
204 | ! |
name = "OuterMargin" |
205 |
)
|
|
206 |
)
|
|
207 | ||
208 |
# Output as PDF
|
|
209 | ! |
as_pdf( |
210 | ! |
grobs = dg, |
211 | ! |
outpath = outpath, |
212 | ! |
pagesize = pagesize |
213 |
)
|
|
214 |
}
|
|
215 | ||
216 |
#' Add padding to grob
|
|
217 |
#' @param grob grob object
|
|
218 |
#' @param pad_v padding to add vertically
|
|
219 |
#' @param pad_h padding to add horizontally
|
|
220 |
#' @keywords internal
|
|
221 |
#'
|
|
222 |
grob_add_padding <- function(grob, pad_v = grid::unit(5, "pt"), pad_h = grid::unit(5, "pt")) { |
|
223 | ! |
ret <- gtable::gtable( |
224 | ! |
heights = grid::unit.c(pad_v, grid::unit(1, "null"), pad_v), |
225 | ! |
widths = grid::unit.c(pad_h, grid::unit(1, "null"), pad_h) |
226 |
)
|
|
227 |
# t, b, l, r, z arguments do not need modification
|
|
228 |
# same effect can be achieved by modifying pad_v and pad_h
|
|
229 | ! |
ret <- gtable::gtable_add_grob(ret, grob, t = 2, b = 2, l = 2, r = 2, z = 1, name = "panel") |
230 | ! |
ret <- gtable::gtable_add_grob(ret, grid::rectGrob(), t = 1, b = 3, l = 1, r = 3, z = 0, name = "background") |
231 | ! |
return(ret) |
232 |
}
|
|
233 | ||
234 |
#' this theme is used across many figures. can be safely removed if update the theme in each function
|
|
235 |
#' @param axis_side axis position
|
|
236 |
#' @param fontsize font size in 'mm'
|
|
237 |
#' @keywords internal
|
|
238 |
#'
|
|
239 |
theme_osprey <- function(axis_side = "left", fontsize = 4) { |
|
240 |
theme( |
|
241 |
panel.background = element_rect(fill = "white", colour = "white"), |
|
242 |
panel.grid.major.y = element_line(colour = "grey50", linetype = 2), |
|
243 |
panel.border = element_rect(colour = "black", fill = NA, linewidth = 1), |
|
244 |
axis.title = element_blank(), |
|
245 |
legend.title = element_blank(), |
|
246 |
legend.position = "bottom", |
|
247 |
axis.ticks.y = element_blank(), |
|
248 |
axis.text = element_text(color = "black", size = fontsize * .pt), |
|
249 |
axis.text.y = element_text(hjust = ifelse(axis_side == "left", 1, 0)), |
|
250 |
text = element_text(size = fontsize * .pt, face = "bold", color = "black"), |
|
251 |
legend.text = element_text(size = fontsize * .pt), |
|
252 |
plot.title = element_text(hjust = 0.5) |
|
253 |
)
|
|
254 |
}
|
|
255 | ||
256 |
check_same_N <- function(..., omit_null = TRUE) { # nolint |
|
257 | ! |
dots <- list(...) |
258 | ||
259 | ! |
n_list <- Map( |
260 | ! |
function(x, name) { |
261 | ! |
if (is.null(x)) { |
262 | ! |
if (omit_null) { |
263 | ! |
NA_integer_
|
264 |
} else { |
|
265 | ! |
stop("arg", name, "is not supposed to be NULL") |
266 |
}
|
|
267 | ! |
} else if (is.data.frame(x)) { |
268 | ! |
nrow(x) |
269 | ! |
} else if (is.atomic(x)) { |
270 | ! |
length(x) |
271 |
} else { |
|
272 | ! |
stop("data structure for ", name, "is currently not supported") |
273 |
}
|
|
274 |
},
|
|
275 | ! |
dots, names(dots) |
276 |
)
|
|
277 | ||
278 | ! |
n <- stats::na.omit(unlist(n_list)) |
279 | ||
280 | ! |
if (length(unique(n)) > 1) { |
281 | ! |
sel <- which(n != n[1]) |
282 | ! |
stop("dimension mismatch:", paste(names(n)[sel], collapse = ", "), " do not have N=", n[1]) |
283 |
}
|
|
284 | ||
285 | ! |
TRUE
|
286 |
}
|
|
287 | ||
288 |
to_n <- function(x, n) { |
|
289 | ! |
if (is.null(x)) { |
290 | ! |
NULL
|
291 | ! |
} else if (length(x) == 1) { |
292 | ! |
rep(x, n) |
293 | ! |
} else if (length(x) == n) { |
294 | ! |
x
|
295 |
} else { |
|
296 | ! |
stop("dimension mismatch") |
297 |
}
|
|
298 |
}
|
|
299 | ||
300 |
#' Extract specific part of a `ggplot` or grob
|
|
301 |
#'
|
|
302 |
#' @param gplot_grob `ggplot` or grob object
|
|
303 |
#' @param part name of the part to be extracted. `NA` will return `zeroGrob()`
|
|
304 |
#' @keywords internal
|
|
305 |
#'
|
|
306 |
grob_part <- function(gplot_grob, part) { |
|
307 | ! |
if (is.na(part)) { |
308 | ! |
return(zeroGrob()) |
309 |
}
|
|
310 | ! |
stopifnot(length(part) == 1 && is.character(part)) |
311 | ! |
index <- match(part, gplot_grob$layout$name) |
312 | ! |
if (is.na(index)) { |
313 | ! |
stop(c( |
314 | ! |
part, " not in plot object. Allowed parts are ", |
315 | ! |
paste(gplot_grob$layout$name, collapse = ", ") |
316 |
)) |
|
317 |
}
|
|
318 | ! |
grob <- gplot_grob$grobs[[index]] |
319 | ||
320 | ! |
return(grob) |
321 |
}
|
|
322 | ||
323 |
#' Extract specific parts of a `ggplot` or grob
|
|
324 |
#'
|
|
325 |
#' @param gplot `ggplot` or grob object
|
|
326 |
#' @param parts names vector of the parts to be extracted.
|
|
327 |
#' @keywords internal
|
|
328 |
#'
|
|
329 |
grob_parts <- function(gplot, parts) { |
|
330 | ! |
stopifnot("gplot must inherit from class 'ggplot' or 'grob'" = inherits(gplot, c("ggplot", "grob"))) |
331 | ||
332 | ! |
if (is(gplot, "ggplot")) { |
333 | ! |
gplot_grob <- ggplotGrob(gplot) |
334 | ! |
} else if (is(gplot, "grob")) { |
335 | ! |
gplot_grob <- gplot |
336 |
}
|
|
337 | ! |
ret <- lapply(parts, grob_part, gplot_grob = gplot_grob) |
338 | ! |
names(ret) <- parts |
339 | ! |
return(ret) |
340 |
}
|
|
341 | ||
342 | ||
343 |
#' this theme is used across many figures. can be safely removed if update the theme in each function
|
|
344 |
#' @param axis_side axis position
|
|
345 |
#' @param fontsize font size in 'mm'
|
|
346 |
#' @param blank whether to have blank or background with grids and borders
|
|
347 |
theme_osprey <- function(axis_side = "left", fontsize = 4, blank = FALSE) { |
|
348 | ! |
theme( |
349 | ! |
panel.background = element_rect(fill = "white", colour = "white"), |
350 | ! |
panel.grid.major.y = if (blank) element_blank() else element_line(colour = "grey50", linetype = 2), |
351 | ! |
panel.border = if (blank) element_blank() else element_rect(colour = "black", fill = NA, linewidth = 1), |
352 | ! |
axis.title = element_blank(), |
353 | ! |
legend.title = element_blank(), |
354 | ! |
legend.position = "bottom", |
355 | ! |
axis.ticks.y = element_blank(), |
356 | ! |
axis.ticks.x.top = element_blank(), |
357 | ! |
axis.text = element_text(color = "black", size = fontsize * .pt), |
358 | ! |
axis.text.y = element_text(hjust = ifelse(axis_side == "left", 0, 1)), |
359 | ! |
text = element_text(size = fontsize * .pt, face = "bold", color = "black"), |
360 | ! |
legend.text = element_text(size = fontsize * .pt), |
361 | ! |
plot.title = element_text(hjust = 0.5) |
362 |
)
|
|
363 |
}
|
1 |
#' Patient Domain Profile
|
|
2 |
#'
|
|
3 |
#' Patient domain profile provides information for a specific subject that participated in the study.
|
|
4 |
#' The plot includes relevant data for one subject in a user specified domain, including
|
|
5 |
#' adverse events (\code{ADAE}), response (\code{ADRS}), concomitant medications
|
|
6 |
#' (\code{ADCM}), exposure (\code{ADEX}), and laboratory (\code{ADLB}).
|
|
7 |
#'
|
|
8 |
#' @param domain string of domain name to be shown as y-axis label, default is `NULL`
|
|
9 |
#' (no y-axis label shown)
|
|
10 |
#' @param var_names character vector to identify each lane
|
|
11 |
#' @param marker_pos Depending on the domain, this can be
|
|
12 |
#' \itemize{
|
|
13 |
#' \item marker position numeric vector for domains \code{ADEX}, \code{ADLB}, and \code{ADRS}
|
|
14 |
#' \item numeric data frame with two columns, start and end time marker position,
|
|
15 |
#' for domains \code{ADAE} and \code{ADCM}
|
|
16 |
#' }
|
|
17 |
#' @param arrow_end numeric value indicates the end of arrow when arrows are requested
|
|
18 |
#' @param xtick_at numeric vector with the locations of the x-axis tick marks
|
|
19 |
#' @param line_col_list a list may contain \cr
|
|
20 |
#' \itemize{
|
|
21 |
#' \item \code{line_col}: factor vector to specify color for segments , default is `NULL`
|
|
22 |
#' (no line color is specified)\cr
|
|
23 |
#' \item \code{line_col_opt} aesthetic values to map color values (named vector to map color values to each name).
|
|
24 |
#' If not `NULL`, please make sure this contains all possible values for \code{line_col} values,
|
|
25 |
#' otherwise color will be assigned by \code{\link[grDevices]{hcl.colors}}
|
|
26 |
#' \item \code{line_col_legend}: a string to be displayed as line color legend title when \code{line_col} is specified,
|
|
27 |
#' default is `NULL` (no legend title is displayed)
|
|
28 |
#' }
|
|
29 |
#' @param line_width numeric value for segment width, default is \code{line_width = 1}
|
|
30 |
#' @param arrow_size numeric value for arrow size, default is \code{arrow_size = 0.1}
|
|
31 |
#' @param no_enddate_extention numeric value for extending the arrow when end date is missing for \code{ADAE}
|
|
32 |
#' or \code{ADCM} domain. Default is \code{no_enddate_extention = 0}.
|
|
33 |
#' @param marker_col_list a list may contain \cr
|
|
34 |
#' \itemize{
|
|
35 |
#' \item \code{marker_col} a factor vector to specify color for markers,
|
|
36 |
#' default is `NULL` (no color markers is specified)
|
|
37 |
#' \item \code{marker_col_opt} aesthetic values to map color values (named vector to map color values to each name)
|
|
38 |
#' If not `NULL`, please make sure this contains all possible values for \code{marker_col} values,
|
|
39 |
#' otherwise color will be assigned by \code{\link[grDevices]{hcl.colors}}
|
|
40 |
#' \item \code{marker_col_legend} a string to be displayed as marker color legend title, default is `NULL`
|
|
41 |
#' (no legend title is displayed)
|
|
42 |
#' }
|
|
43 |
#' @param marker_shape_list a list may contain \cr
|
|
44 |
#' \itemize{
|
|
45 |
#' \item \code{marker_shape} factor vector to specify shape for markers,
|
|
46 |
#' default is `NULL` (no shape marker is specified)
|
|
47 |
#' \item \code{marker_shape_opt} aesthetic values to map shape values (named vector to map shape values to each name).
|
|
48 |
#' If not `NULL`, please make sure this contains all possible values for \code{marker_shape} values,
|
|
49 |
#' otherwise shape will be assigned by \code{ggplot} default
|
|
50 |
#' \item \code{marker_shape_legend} string to be displayed as marker shape legend title, default is `NULL`
|
|
51 |
#' (no legend title is displayed)
|
|
52 |
#' }
|
|
53 |
#' @param show_days_label boolean value for showing y-axis label, default is \code{TRUE}
|
|
54 |
#' @param xlim numeric vector for x-axis limit, default is
|
|
55 |
#' \code{xlim = c(-28, max(marker_pos) + 5)}
|
|
56 |
#' @param xlab string to be shown as x-axis label, default is \code{"Study Day"}
|
|
57 |
#' @param show_title boolean value for showing title of the plot, default is \code{TRUE}
|
|
58 |
#' @param title string to be shown as title of the plot, default is `NULL` (no plot title is displayed)
|
|
59 |
#'
|
|
60 |
#' @author Xuefeng Hou (houx14) \email{houx14@gene.com}
|
|
61 |
#' @author Tina Cho (chot) \email{tina.cho@roche.com}
|
|
62 |
#' @author Molly He (hey59) \email{hey59@gene.com}
|
|
63 |
#' @template author_qit3
|
|
64 |
#'
|
|
65 |
#' @return plot object
|
|
66 |
#'
|
|
67 |
#' @export
|
|
68 |
#'
|
|
69 |
#' @examples
|
|
70 |
#' library(dplyr)
|
|
71 |
#'
|
|
72 |
#' # ADSL
|
|
73 |
#' ADSL <- osprey::rADSL %>%
|
|
74 |
#' filter(USUBJID == rADSL$USUBJID[1]) %>%
|
|
75 |
#' mutate(
|
|
76 |
#' TRTSDT = as.Date(TRTSDTM),
|
|
77 |
#' max_date = max(as.Date(LSTALVDT), as.Date(DTHDT), na.rm = TRUE),
|
|
78 |
#' max_day = as.numeric(as.Date(max_date) - as.Date(TRTSDT)) + 1
|
|
79 |
#' ) %>%
|
|
80 |
#' select(USUBJID, STUDYID, TRTSDT, max_day)
|
|
81 |
#'
|
|
82 |
#'
|
|
83 |
#'
|
|
84 |
#' # Example 1 Exposure "ADEX"
|
|
85 |
#' ADEX <- osprey::rADEX %>%
|
|
86 |
#' select(USUBJID, STUDYID, ASTDTM, PARCAT2, AVAL, AVALU, PARAMCD)
|
|
87 |
#' ADEX <- left_join(ADSL, ADEX, by = c("USUBJID", "STUDYID"))
|
|
88 |
#' ADEX <- ADEX %>%
|
|
89 |
#' filter(PARAMCD == "DOSE") %>%
|
|
90 |
#' arrange(PARCAT2, PARAMCD) %>%
|
|
91 |
#' mutate(diff = c(0, diff(AVAL, lag = 1))) %>%
|
|
92 |
#' mutate(
|
|
93 |
#' Modification = case_when(
|
|
94 |
#' diff < 0 ~ "Decrease",
|
|
95 |
#' diff > 0 ~ "Increase",
|
|
96 |
#' diff == 0 ~ "None"
|
|
97 |
#' )
|
|
98 |
#' ) %>%
|
|
99 |
#' mutate(
|
|
100 |
#' ASTDT_dur = as.numeric(
|
|
101 |
#' as.Date(
|
|
102 |
#' substr(as.character(ASTDTM), 1, 10)
|
|
103 |
#' ) - as.Date(TRTSDT) + 1
|
|
104 |
#' )
|
|
105 |
#' )
|
|
106 |
#'
|
|
107 |
#' p1 <- patient_domain_profile(
|
|
108 |
#' domain = "Exposure (ADEX)",
|
|
109 |
#' var_names = ADEX$PARCAT2,
|
|
110 |
#' marker_pos = ADEX$ASTDT_dur,
|
|
111 |
#' arrow_end = ADSL$max_day,
|
|
112 |
#' xtick_at = waiver(),
|
|
113 |
#' line_col_list = NULL,
|
|
114 |
#' line_width = 1,
|
|
115 |
#' arrow_size = 0.1,
|
|
116 |
#' no_enddate_extention = 0,
|
|
117 |
#' marker_col_list = list(
|
|
118 |
#' marker_col = factor(ADEX$Modification),
|
|
119 |
#' marker_col_opt = c("Increase" = "red", "Decrease" = "green", "None" = "blue"),
|
|
120 |
#' marker_col_legend = NULL
|
|
121 |
#' ),
|
|
122 |
#' marker_shape_list = list(
|
|
123 |
#' marker_shape = factor(ADEX$Modification),
|
|
124 |
#' marker_shape_opt = c("Increase" = 24, "Decrease" = 25, "None" = 23),
|
|
125 |
#' marker_shape_legend = "Dose Modification"
|
|
126 |
#' ),
|
|
127 |
#' show_days_label = TRUE,
|
|
128 |
#' xlim = c(-28, ADSL$max_day),
|
|
129 |
#' xlab = "Study Day",
|
|
130 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
131 |
#' )
|
|
132 |
#' p1
|
|
133 |
#'
|
|
134 |
#' # Example 2 Adverse Event "ADAE"
|
|
135 |
#' # Note that ASTDY is represented by a circle and AENDY is represented by a square.
|
|
136 |
#' # If AENDY and ASTDY occur on the same day only AENDY will be shown.
|
|
137 |
#'
|
|
138 |
#' # Adverse Event ADAE
|
|
139 |
#' ADAE <- osprey::rADAE %>%
|
|
140 |
#' select(USUBJID, STUDYID, AESOC, AEDECOD, AESER, AETOXGR, AEREL, ASTDY, AENDY)
|
|
141 |
#' ADAE <- left_join(ADSL, ADAE, by = c("USUBJID", "STUDYID"))
|
|
142 |
#'
|
|
143 |
#' p2 <- patient_domain_profile(
|
|
144 |
#' domain = "Adverse Event (ADAE)",
|
|
145 |
#' var_names = ADAE$AEDECOD,
|
|
146 |
#' marker_pos = ADAE[, c("ASTDY", "AENDY")],
|
|
147 |
#' arrow_end = ADSL$max_day,
|
|
148 |
#' xtick_at = waiver(),
|
|
149 |
#' line_col_list = list(
|
|
150 |
#' line_col = ADAE$AESER,
|
|
151 |
#' line_col_legend = "Serious",
|
|
152 |
#' line_col_opt = c("blue", "green")
|
|
153 |
#' ),
|
|
154 |
#' line_width = 1,
|
|
155 |
#' arrow_size = 0.1,
|
|
156 |
#' no_enddate_extention = 0,
|
|
157 |
#' marker_col_list = list(
|
|
158 |
#' marker_col = factor(ADAE$AETOXGR),
|
|
159 |
#' marker_col_opt = c("3" = "yellow", "4" = "red"),
|
|
160 |
#' marker_col_legend = NULL
|
|
161 |
#' ),
|
|
162 |
#' marker_shape_list = list(
|
|
163 |
#' marker_shape = NULL,
|
|
164 |
#' marker_shape_opt = NULL,
|
|
165 |
#' marker_shape_legend = "Grade"
|
|
166 |
#' ),
|
|
167 |
#' show_days_label = TRUE,
|
|
168 |
#' xlim = c(-28, ADSL$max_day),
|
|
169 |
#' xlab = "Study Day",
|
|
170 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
171 |
#' )
|
|
172 |
#' p2
|
|
173 |
#'
|
|
174 |
#' # Example 3 Tumor Response "ADRS"
|
|
175 |
#' ADRS <- osprey::rADRS %>%
|
|
176 |
#' select(USUBJID, STUDYID, PARAMCD, PARAM, AVALC, AVAL, ADY, ADTM)
|
|
177 |
#' ADRS <- left_join(ADSL, ADRS, by = c("USUBJID", "STUDYID"))
|
|
178 |
#' p3 <- patient_domain_profile(
|
|
179 |
#' domain = "Tumor Response (ADRS)",
|
|
180 |
#' var_names = ADRS$PARAMCD,
|
|
181 |
#' marker_pos = ADRS$ADY,
|
|
182 |
#' arrow_end = ADSL$max_day,
|
|
183 |
#' xtick_at = waiver(),
|
|
184 |
#' line_col_list = NULL,
|
|
185 |
#' line_width = 1,
|
|
186 |
#' arrow_size = 0.1,
|
|
187 |
#' no_enddate_extention = 0,
|
|
188 |
#' marker_col_list = list(
|
|
189 |
#' marker_col = factor(ADRS$AVALC),
|
|
190 |
#' marker_col_opt = c(
|
|
191 |
#' "CR" = "green", "PR" = "blue",
|
|
192 |
#' "SD" = "yellow", "PD" = "red", "NE" = "pink",
|
|
193 |
#' "Y" = "lightblue", "N" = "darkred"
|
|
194 |
#' ),
|
|
195 |
#' marker_col_legend = NULL
|
|
196 |
#' ),
|
|
197 |
#' marker_shape_list = list(
|
|
198 |
#' marker_shape = factor(ADRS$AVALC),
|
|
199 |
#' marker_shape_opt = c(
|
|
200 |
#' "CR" = 21, "PR" = 24,
|
|
201 |
#' "SD" = 23, "PD" = 22, "NE" = 14,
|
|
202 |
#' "Y" = 11, "N" = 8
|
|
203 |
#' ),
|
|
204 |
#' marker_shape_legend = "Response"
|
|
205 |
#' ),
|
|
206 |
#' show_days_label = TRUE,
|
|
207 |
#' xlim = c(-28, ADSL$max_day),
|
|
208 |
#' xlab = "Study Day",
|
|
209 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
210 |
#' )
|
|
211 |
#' p3
|
|
212 |
#'
|
|
213 |
#' # Example 4 Concomitant Med "ADCM"
|
|
214 |
#' ADCM <- osprey::rADCM %>%
|
|
215 |
#' select(USUBJID, STUDYID, ASTDTM, AENDTM, CMDECOD, ASTDY, AENDY)
|
|
216 |
#' ADCM <- left_join(ADSL, ADCM, by = c("USUBJID", "STUDYID"))
|
|
217 |
#' p4 <- patient_domain_profile(
|
|
218 |
#' domain = "Concomitant Med (ADCM)",
|
|
219 |
#' var_names = ADCM$CMDECOD,
|
|
220 |
#' marker_pos = ADCM[, c("ASTDY", "AENDY")],
|
|
221 |
#' arrow_end = ADSL$max_day,
|
|
222 |
#' xtick_at = waiver(),
|
|
223 |
#' line_col_list = list(line_col_opt = "orange"),
|
|
224 |
#' line_width = 1,
|
|
225 |
#' arrow_size = 0.1,
|
|
226 |
#' no_enddate_extention = 50,
|
|
227 |
#' marker_col_list = list(marker_col_opt = "orange"),
|
|
228 |
#' marker_shape_list = NULL,
|
|
229 |
#' show_days_label = TRUE,
|
|
230 |
#' xlim = c(-28, ADSL$max_day),
|
|
231 |
#' xlab = "Study Day",
|
|
232 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
233 |
#' )
|
|
234 |
#' p4
|
|
235 |
#'
|
|
236 |
#' # Example 5 Laboratory "ADLB"
|
|
237 |
#' ADLB <- osprey::rADLB %>%
|
|
238 |
#' select(
|
|
239 |
#' USUBJID, STUDYID, LBSEQ, PARAMCD, BASETYPE,
|
|
240 |
#' ADTM, ADY, ATPTN, AVISITN, LBTESTCD, ANRIND
|
|
241 |
#' )
|
|
242 |
#' ADLB <- left_join(ADSL, ADLB, by = c("USUBJID", "STUDYID"))
|
|
243 |
#'
|
|
244 |
#' ADLB <- ADLB %>%
|
|
245 |
#' group_by(USUBJID) %>%
|
|
246 |
#' mutate(ANRIND = factor(ANRIND, levels = c("LOW", "NORMAL", "HIGH")))
|
|
247 |
#'
|
|
248 |
#' p5 <- patient_domain_profile(
|
|
249 |
#' domain = "Laboratory (ADLB)",
|
|
250 |
#' var_names = ADLB$LBTESTCD,
|
|
251 |
#' marker_pos = ADLB$ADY,
|
|
252 |
#' arrow_end = ADSL$max_day,
|
|
253 |
#' xtick_at = waiver(),
|
|
254 |
#' line_col_list = NULL,
|
|
255 |
#' line_width = 1,
|
|
256 |
#' arrow_size = 0.1,
|
|
257 |
#' no_enddate_extention = 0,
|
|
258 |
#' marker_col_list = list(
|
|
259 |
#' marker_col = factor(ADLB$ANRIND),
|
|
260 |
#' marker_col_opt = c(
|
|
261 |
#' "HIGH" = "red", "LOW" = "blue",
|
|
262 |
#' "NORMAL" = "green", "NA" = "green"
|
|
263 |
#' )
|
|
264 |
#' ),
|
|
265 |
#' marker_shape_list = list(
|
|
266 |
#' marker_shape = factor(ADLB$ANRIND),
|
|
267 |
#' marker_shape_opt = c(
|
|
268 |
#' "HIGH" = 24, "LOW" = 25,
|
|
269 |
#' "NORMAL" = 23, "NA" = 23
|
|
270 |
#' ),
|
|
271 |
#' marker_shape_legend = "Labs Abnormality"
|
|
272 |
#' ),
|
|
273 |
#' show_days_label = TRUE,
|
|
274 |
#' xlim = c(-30, ADSL$max_day),
|
|
275 |
#' xlab = "Study Day",
|
|
276 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
277 |
#' )
|
|
278 |
#' p5
|
|
279 |
patient_domain_profile <- function(domain = NULL, |
|
280 |
var_names,
|
|
281 |
marker_pos,
|
|
282 |
arrow_end,
|
|
283 |
xtick_at = waiver(), |
|
284 |
line_col_list = NULL, |
|
285 |
line_width = 1, |
|
286 |
arrow_size = 0.1, |
|
287 |
no_enddate_extention = 0, |
|
288 |
marker_col_list = NULL, |
|
289 |
marker_shape_list = NULL, |
|
290 |
show_days_label = TRUE, |
|
291 |
xlim = c(-28, max(marker_pos) + 5), |
|
292 |
xlab = NULL, |
|
293 |
show_title = TRUE, |
|
294 |
title = NULL) { |
|
295 | ! |
line_col <- line_col_list[["line_col"]] |
296 | ! |
line_col_opt <- line_col_list[["line_col_opt"]] |
297 | ! |
line_col_legend <- line_col_list[["line_col_legend"]] |
298 | ||
299 | ! |
marker_col <- marker_col_list[["marker_col"]] |
300 | ! |
marker_col_opt <- marker_col_list[["marker_col_opt"]] |
301 | ! |
marker_col_legend <- marker_col_list[["marker_col_legend"]] |
302 | ||
303 | ! |
marker_shape <- marker_shape_list[["marker_shape"]] |
304 | ! |
marker_shape_opt <- marker_shape_list[["marker_shape_opt"]] |
305 | ! |
marker_shape_legend <- marker_shape_list[["marker_shape_legend"]] |
306 | ||
307 |
# check user input
|
|
308 | ! |
stopifnot( |
309 | ! |
"invalid arguments: check that the length of input arguments are identical" = |
310 | ! |
length(unique(nrow(data.frame(var_names)), nrow(data.frame(marker_pos)), nrow(data.frame(arrow_end)))) == 1 |
311 |
)
|
|
312 | ! |
stopifnot( |
313 | ! |
"invalid argument: check that marker_pos is either a vector or a data frame with two columns" = |
314 | ! |
ncol(data.frame(marker_pos)) <= 2 |
315 |
)
|
|
316 | ! |
stopifnot( |
317 | ! |
"invalid arguments: check that the length of line_col is equal as other inputs" = |
318 | ! |
is.null(line_col) || length(line_col) == length(var_names) |
319 |
)
|
|
320 | ! |
stopifnot( |
321 | ! |
"invalid arguments: check that the length of marker_col is equal as other inputs" = |
322 | ! |
is.null(marker_col) || length(marker_col) == length(var_names) |
323 |
)
|
|
324 | ! |
stopifnot( |
325 | ! |
"invalid arguments: check that the length of marker_shape is equal as other inputs" = |
326 | ! |
is.null(marker_shape) || length(marker_shape) == length(var_names) |
327 |
)
|
|
328 | ! |
checkmate::assert_numeric(xlim, len = 2) |
329 | ||
330 | ! |
marker_data <- data.frame( |
331 | ! |
var_names,
|
332 | ! |
marker_pos = if (is.null(marker_pos)) to_n("x", length(var_names)) else marker_pos, |
333 | ! |
marker_shape = if (is.null(marker_shape)) to_n("x", length(var_names)) else marker_shape, |
334 | ! |
marker_col = if (is.null(marker_col)) to_n("x", length(var_names)) else marker_col |
335 |
)
|
|
336 | ||
337 |
# plot lines
|
|
338 | ! |
if (length(dim(marker_pos)) == 2) { |
339 | ! |
line_data <- data.frame( |
340 | ! |
var_names,
|
341 | ! |
line_col = if (is.null(line_col)) to_n("x", length(var_names)) else line_col, |
342 | ! |
line_start = unname(dplyr::pull(marker_pos, 1)), |
343 | ! |
line_end = unname(dplyr::pull(marker_pos, 2)), |
344 | ! |
line_min = rep(xlim[1], length(var_names)), |
345 | ! |
line_max = rep(arrow_end + no_enddate_extention, length(var_names)) |
346 |
)
|
|
347 | ! |
names(line_data) <- c("var_names", "line_col", "line_start", "line_end", "line_min", "line_max") |
348 | ||
349 | ! |
p <- ggplot() + |
350 | ! |
geom_segment( |
351 | ! |
data = line_data[!is.na(line_data$line_end), ], |
352 | ! |
aes( |
353 | ! |
x = var_names, |
354 | ! |
y = line_start, |
355 | ! |
xend = var_names, |
356 | ! |
yend = line_end, |
357 | ! |
color = line_col |
358 |
),
|
|
359 | ! |
lineend = "round", |
360 | ! |
linejoin = "round", |
361 | ! |
linewidth = line_width, |
362 | ! |
arrow = NULL, |
363 | ! |
show.legend = NA, |
364 | ! |
na.rm = TRUE |
365 |
) + |
|
366 | ! |
scale_y_continuous(limits = xlim, breaks = xtick_at, expand = c(0, 0)) + |
367 | ! |
coord_flip(xlim = c(1, length(unique(var_names)))) + |
368 | ! |
geom_segment( |
369 | ! |
data = line_data[is.na(line_data$line_end) == TRUE, ], |
370 | ! |
aes( |
371 | ! |
x = var_names, |
372 | ! |
y = pmax(line_start, line_min, na.rm = TRUE), |
373 | ! |
xend = var_names, |
374 | ! |
yend = line_max, |
375 | ! |
color = line_col |
376 |
),
|
|
377 | ! |
lineend = "round", |
378 | ! |
linejoin = "round", |
379 | ! |
linewidth = line_width, |
380 | ! |
show.legend = FALSE, |
381 | ! |
arrow = arrow(length = grid::unit(arrow_size, "inches")), |
382 | ! |
na.rm = TRUE |
383 |
)
|
|
384 | ||
385 | ! |
if (is.null(line_col_opt)) { |
386 | ! |
line_col_opt <- if (!is.null(getOption("ggplot2.discrete.colour"))) { |
387 | ! |
getOption("ggplot2.discrete.colour") |
388 |
} else { |
|
389 | ! |
grDevices::hcl.colors(length(levels(line_data$line_col))) |
390 |
}
|
|
391 |
}
|
|
392 | ||
393 | ! |
p <- p + |
394 | ! |
scale_color_manual( |
395 | ! |
breaks = line_data$line_col, |
396 | ! |
values = line_col_opt, |
397 | ! |
limits = levels(line_data$line_col) |
398 |
)
|
|
399 | ||
400 | ! |
if (!is.null(line_col)) { |
401 | ! |
p <- p + guides(color = guide_legend(line_col_legend, order = 1)) |
402 |
} else { |
|
403 | ! |
p <- p + guides(color = "none") |
404 |
}
|
|
405 | ||
406 |
# plot markers
|
|
407 | ! |
p <- p + |
408 | ! |
geom_point( |
409 | ! |
data = marker_data, |
410 | ! |
aes(x = var_names, y = marker_data[, 2], fill = factor(marker_col)), |
411 | ! |
shape = 21, |
412 | ! |
size = 5, |
413 | ! |
na.rm = TRUE |
414 |
) + |
|
415 | ! |
geom_point( |
416 | ! |
data = marker_data, |
417 | ! |
aes(x = var_names, y = marker_data[, 3], fill = factor(marker_col)), |
418 | ! |
shape = 22, |
419 | ! |
size = 3, |
420 | ! |
na.rm = TRUE |
421 |
)
|
|
422 | ||
423 | ! |
if (is.null(marker_col_opt)) { |
424 | ! |
marker_col_opt <- if (!is.null(getOption("ggplot2.discrete.colour"))) { |
425 | ! |
getOption("ggplot2.discrete.colour") |
426 |
} else { |
|
427 | ! |
grDevices::hcl.colors(length(levels(marker_data$marker_col))) |
428 |
}
|
|
429 |
}
|
|
430 | ! |
p <- p + |
431 | ! |
scale_fill_manual( |
432 | ! |
breaks = marker_data$marker_col, |
433 | ! |
values = marker_col_opt |
434 |
)
|
|
435 | ||
436 | ||
437 | ! |
p <- p + theme_bw() + |
438 | ! |
theme( |
439 | ! |
panel.background = element_blank(), |
440 | ! |
panel.grid = element_blank(), |
441 | ! |
axis.line = element_line(colour = "black") |
442 |
) + |
|
443 | ! |
ylab(xlab) + xlab(domain) |
444 | ||
445 | ! |
if (!is.null(marker_col)) { |
446 | ! |
p <- p + guides(fill = guide_legend(marker_col_legend, order = 2)) |
447 |
} else { |
|
448 | ! |
p <- p + guides(fill = "none") |
449 |
}
|
|
450 | ||
451 | ! |
p <- p + guides(shape = guide_legend("Shape", order = 3)) |
452 | ||
453 | ! |
p <- p + scale_shape_manual(values = c(21, 22)) + |
454 | ! |
guides(shape = guide_legend(title = "Shape", override.aes = list(label = c("Start", "End")), order = 3)) |
455 | ||
456 | ! |
if (!is.null(marker_shape)) { |
457 | ! |
p <- p + guides(shape = guide_legend(marker_shape_legend, order = 3)) |
458 |
} else { |
|
459 | ! |
p <- p + guides(shape = "none") |
460 |
}
|
|
461 |
} else { |
|
462 | ! |
p <- ggplot() + |
463 | ! |
geom_point( |
464 | ! |
data = marker_data, |
465 | ! |
aes( |
466 | ! |
x = var_names, |
467 | ! |
y = marker_pos, |
468 | ! |
shape = marker_shape, |
469 | ! |
fill = marker_col |
470 |
),
|
|
471 | ! |
size = 3, na.rm = TRUE |
472 |
) + |
|
473 | ! |
scale_y_continuous(limits = xlim, breaks = xtick_at, expand = c(0, 0)) + |
474 | ! |
coord_flip(xlim = c(1, length(unique(var_names)))) + |
475 | ! |
theme_bw() + |
476 | ! |
theme( |
477 | ! |
panel.background = element_blank(), |
478 | ! |
panel.grid = element_blank(), |
479 | ! |
axis.line = element_line(colour = "black") |
480 |
) + |
|
481 | ! |
ylab(xlab) + |
482 | ! |
xlab(domain) |
483 | ||
484 | ! |
if (is.null(marker_col_legend)) { |
485 | ! |
if (length(setdiff(marker_col, marker_shape)) == 0) { |
486 | ! |
marker_col_legend <- marker_shape_legend |
487 |
}
|
|
488 |
}
|
|
489 | ||
490 | ! |
if (is.null(marker_shape_legend)) { |
491 | ! |
if (length(setdiff(marker_col, marker_shape)) == 0) { |
492 | ! |
marker_shape_legend <- marker_col_legend |
493 |
}
|
|
494 |
}
|
|
495 | ||
496 | ! |
if (is.null(marker_col_opt)) { |
497 | ! |
if (!is.null(getOption("ggplot2.discrete.colour"))) { |
498 | ! |
marker_col_opt <- getOption("ggplot2.discrete.colour") |
499 |
} else { |
|
500 | ! |
marker_col_opt <- grDevices::hcl.colors(length(levels(marker_data$marker_col))) |
501 |
}
|
|
502 |
}
|
|
503 | ! |
p <- p + |
504 | ! |
scale_fill_manual( |
505 | ! |
name = marker_col_legend, |
506 | ! |
breaks = marker_data$marker_col, |
507 | ! |
values = marker_col_opt |
508 |
)
|
|
509 | ||
510 | ! |
if (is.null(marker_shape_opt)) marker_shape_opt <- 1:25 |
511 | ! |
p <- p + scale_shape_manual( |
512 | ! |
name = marker_shape_legend, |
513 | ! |
breaks = marker_data$marker_shape, |
514 | ! |
values = marker_shape_opt |
515 |
)
|
|
516 |
}
|
|
517 | ||
518 |
# plot title and labels
|
|
519 | ! |
if (show_title) { |
520 | ! |
p <- p + |
521 | ! |
labs(title = title) + |
522 | ! |
theme(plot.title = element_text(face = "bold")) |
523 |
}
|
|
524 | ||
525 |
# Plot y axis label
|
|
526 | ! |
if (show_days_label == FALSE) { |
527 | ! |
p <- p + theme_bw() + |
528 | ! |
theme( |
529 | ! |
panel.background = element_blank(), |
530 | ! |
panel.grid = element_blank(), |
531 | ! |
axis.line = element_line(colour = "black"), |
532 | ! |
axis.text.x = element_blank(), |
533 | ! |
axis.title.x = element_blank(), |
534 | ! |
axis.title.y = element_text(size = rel(0.8)), |
535 | ! |
legend.title = element_text(size = 10), |
536 | ! |
legend.spacing.y = grid::unit(0, "cm"), |
537 | ! |
legend.key.height = grid::unit(1, "line"), |
538 | ! |
legend.margin = margin(t = 0, b = 0, r = 0.5, l = 0, unit = "cm"), |
539 | ! |
plot.margin = margin(t = 0, b = 0, r = 0.5, l = 0.5, unit = "cm") |
540 |
)
|
|
541 |
} else { |
|
542 | ! |
p <- p + theme_bw() + |
543 | ! |
theme( |
544 | ! |
panel.background = element_blank(), |
545 | ! |
panel.grid = element_blank(), |
546 | ! |
axis.line = element_line(colour = "black"), |
547 | ! |
legend.title = element_text(size = 10), |
548 | ! |
legend.spacing.y = grid::unit(0, "cm"), |
549 | ! |
legend.key.height = grid::unit(1, "line"), |
550 | ! |
legend.margin = margin(t = 0, b = 0, r = 0.5, l = 0, unit = "cm"), |
551 | ! |
plot.margin = margin(t = 0, b = 0, l = 0.5, r = 0.5, unit = "cm") |
552 |
)
|
|
553 |
}
|
|
554 | ||
555 | ! |
p
|
556 |
}
|
|
557 | ||
558 |
#' Patient Profile Plot
|
|
559 |
#'
|
|
560 |
#' Patient profile plot provides detailed information for a specific subject participating in the study.
|
|
561 |
#' The plot includes relevant data for one subject that can help correlate adverse events, response,
|
|
562 |
#' concomitant medications, exposure, and laboratory. The plotting of patient profile is modularized, with
|
|
563 |
#' each domain plot generated by function \code{\link{patient_domain_profile}}. This \code{\link{g_patient_profile}}
|
|
564 |
#' function assembles all requested domain plots into one patient profile.
|
|
565 |
#' \code{ADSL}, \code{ADEX}, \code{ADAE}, \code{ADRS}, \code{ADCM} and \code{ADLB} data must be provided.
|
|
566 |
#' The plot output will not include domains with data unspecified
|
|
567 |
#'
|
|
568 |
#' @param ex list may contain
|
|
569 |
#' \itemize{
|
|
570 |
#' \item \code{data} dataframe for \code{ADEX} domain dataset
|
|
571 |
#' \item \code{var} vector to identify each lane of \code{ADEX} domain plot
|
|
572 |
#' }
|
|
573 |
#' @param ae list may contain
|
|
574 |
#' \itemize{
|
|
575 |
#' \item \code{data} dataframe for \code{ADAE} domain dataset
|
|
576 |
#' \item \code{var} vector to identify each lane of \code{ADAE} plot
|
|
577 |
#' \item \code{line_col} factor vector to specify color for segments of \code{ADAE} plot
|
|
578 |
#' \item \code{line_col_legend} string to be displayed as line color legend title of \code{ADAE} plot
|
|
579 |
#' \item \code{line_col_opt} aesthetic values to map line color values of \code{ADAE} plot
|
|
580 |
#' (named vector to map color values to each name).
|
|
581 |
#' If not `NULL`, please make sure this contains all possible values for \code{line_col} values,
|
|
582 |
#' otherwise color will be assigned by \code{ggplot} default, please note that `NULL` needs to be
|
|
583 |
#' specified
|
|
584 |
#' }
|
|
585 |
#' @param rs list may contain
|
|
586 |
#' \itemize{
|
|
587 |
#' \item \code{data} dataframe for \code{ADRS} domain dataset
|
|
588 |
#' \item \code{var} vector to identify each lane of \code{ADRS} domain plot
|
|
589 |
#' }
|
|
590 |
#' @param cm list may contain
|
|
591 |
#' \itemize{
|
|
592 |
#' \item \code{data} dataframe for \code{ADCM} domain dataset
|
|
593 |
#' \item \code{var} vector to identify each lane of \code{ADCM} domain plot
|
|
594 |
#' }
|
|
595 |
#' @param lb list may contain
|
|
596 |
#' \itemize{
|
|
597 |
#' \item \code{data} dataframe for \code{ADLB} domain dataset
|
|
598 |
#' \item \code{var} vector to identify each lane of \code{ADLB} domain plot
|
|
599 |
#' }
|
|
600 |
#' @param arrow_end_day numeric value indicates the end of arrow when arrows are requested
|
|
601 |
#' @param xlim numeric vector for x-axis limit that will be shared by all domain plots, default is
|
|
602 |
#' \code{xlim = c(-28, 365)}
|
|
603 |
#' @param xlab string to be shown as x-axis label, default is \code{"Study Day"}
|
|
604 |
#' @param title string to be shown as title of the plot, default is \code{"Patient Profile"}
|
|
605 |
#'
|
|
606 |
#' @author Xuefeng Hou (houx14) \email{houx14@gene.com}
|
|
607 |
#' @author Molly He (hey59) \email{hey59@gene.com}
|
|
608 |
#' @template author_qit3
|
|
609 |
#'
|
|
610 |
#' @return plot object
|
|
611 |
#'
|
|
612 |
#' @export
|
|
613 |
#'
|
|
614 |
#' @seealso \code{\link{patient_domain_profile}}
|
|
615 |
#'
|
|
616 |
#' @examplesIf require("nestcolor")
|
|
617 |
#' library(dplyr)
|
|
618 |
#' library(nestcolor)
|
|
619 |
#'
|
|
620 |
#' # ADSL
|
|
621 |
#' ADSL <- osprey::rADSL %>%
|
|
622 |
#' filter(USUBJID == rADSL$USUBJID[1]) %>%
|
|
623 |
#' mutate(
|
|
624 |
#' TRTSDT = as.Date(TRTSDTM),
|
|
625 |
#' max_date = max(as.Date(LSTALVDT), as.Date(DTHDT), na.rm = TRUE),
|
|
626 |
#' max_day = as.numeric(as.Date(max_date) - as.Date(TRTSDT)) + 1
|
|
627 |
#' ) %>%
|
|
628 |
#' select(USUBJID, STUDYID, TRTSDT, max_day)
|
|
629 |
#'
|
|
630 |
#'
|
|
631 |
#' # ADEX
|
|
632 |
#' ADEX <- osprey::rADEX %>%
|
|
633 |
#' select(USUBJID, STUDYID, ASTDTM, PARCAT2, AVAL, AVALU, PARAMCD)
|
|
634 |
#' ADEX <- left_join(ADSL, ADEX, by = c("USUBJID", "STUDYID"))
|
|
635 |
#'
|
|
636 |
#' ADEX <- ADEX %>%
|
|
637 |
#' filter(PARAMCD == "DOSE") %>%
|
|
638 |
#' arrange(PARCAT2, PARAMCD) %>%
|
|
639 |
#' mutate(diff = c(0, diff(AVAL, lag = 1))) %>%
|
|
640 |
#' mutate(Modification = case_when(
|
|
641 |
#' diff < 0 ~ "Decrease",
|
|
642 |
#' diff > 0 ~ "Increase",
|
|
643 |
#' diff == 0 ~ "None"
|
|
644 |
#' )) %>%
|
|
645 |
#' mutate(ASTDT_dur = as.numeric(
|
|
646 |
#' as.Date(substr(as.character(ASTDTM), 1, 10)) -
|
|
647 |
#' as.Date(TRTSDT) + 1
|
|
648 |
#' ))
|
|
649 |
#'
|
|
650 |
#' # ADAE
|
|
651 |
#' ADAE <- osprey::rADAE %>%
|
|
652 |
#' select(USUBJID, STUDYID, AESOC, AEDECOD, AESER, AETOXGR, AEREL, ASTDY, AENDY)
|
|
653 |
#' ADAE <- left_join(ADSL, ADAE, by = c("USUBJID", "STUDYID"))
|
|
654 |
#'
|
|
655 |
#' # ADRS
|
|
656 |
#' ADRS <- osprey::rADRS %>%
|
|
657 |
#' select(USUBJID, STUDYID, PARAMCD, PARAM, AVALC, AVAL, ADY, ADTM)
|
|
658 |
#' ADRS <- left_join(ADSL, ADRS, by = c("USUBJID", "STUDYID"))
|
|
659 |
#'
|
|
660 |
#' # ADCM
|
|
661 |
#' ADCM <- osprey::rADCM %>%
|
|
662 |
#' select(USUBJID, STUDYID, ASTDTM, AENDTM, CMDECOD, ASTDY, AENDY)
|
|
663 |
#' ADCM <- left_join(ADSL, ADCM, by = c("USUBJID", "STUDYID"))
|
|
664 |
#'
|
|
665 |
#' # ADLB
|
|
666 |
#' ADLB <- osprey::rADLB %>%
|
|
667 |
#' select(
|
|
668 |
#' USUBJID, STUDYID, LBSEQ, PARAMCD, BASETYPE, ADTM,
|
|
669 |
#' ADY, ATPTN, AVISITN, LBTESTCD, ANRIND
|
|
670 |
#' )
|
|
671 |
#' ADLB <- left_join(ADSL, ADLB, by = c("USUBJID", "STUDYID"))
|
|
672 |
#'
|
|
673 |
#' ADLB <- ADLB %>%
|
|
674 |
#' group_by(USUBJID) %>%
|
|
675 |
#' mutate(ANRIND = factor(ANRIND, levels = c("LOW", "NORMAL", "HIGH")))
|
|
676 |
#'
|
|
677 |
#' # Example Patient Profile plot 5 domains
|
|
678 |
#' g_patient_profile(
|
|
679 |
#' ex = list(
|
|
680 |
#' data = ADEX,
|
|
681 |
#' var = ADEX$PARCAT2
|
|
682 |
#' ),
|
|
683 |
#' ae = list(
|
|
684 |
#' data = ADAE,
|
|
685 |
#' var = ADAE$AEDECOD,
|
|
686 |
#' line_col = factor(ADAE$AESER),
|
|
687 |
#' line_col_legend = "Serious",
|
|
688 |
#' line_col_opt = c("Y" = "red", "N" = "blue")
|
|
689 |
#' ),
|
|
690 |
#' rs = list(
|
|
691 |
#' data = ADRS,
|
|
692 |
#' var = ADRS$PARAMCD
|
|
693 |
#' ),
|
|
694 |
#' cm = list(
|
|
695 |
#' data = ADCM,
|
|
696 |
#' var = ADCM$CMDECOD
|
|
697 |
#' ),
|
|
698 |
#' lb = list(
|
|
699 |
#' data = ADLB,
|
|
700 |
#' var = ADLB$LBTESTCD
|
|
701 |
#' ),
|
|
702 |
#' arrow_end_day = ADSL$max_day,
|
|
703 |
#' xlim = c(-28, ADSL$max_day),
|
|
704 |
#' xlab = "Study Day",
|
|
705 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
706 |
#' )
|
|
707 |
#'
|
|
708 |
#' # Example Patient Profile plot without ADCM and ADLB
|
|
709 |
#' g_patient_profile(
|
|
710 |
#' ex = list(
|
|
711 |
#' data = ADEX,
|
|
712 |
#' var = ADEX$PARCAT2
|
|
713 |
#' ),
|
|
714 |
#' ae = list(
|
|
715 |
#' data = ADAE,
|
|
716 |
#' var = ADAE$AEDECOD,
|
|
717 |
#' line_col = factor(ADAE$AESER),
|
|
718 |
#' line_col_legend = "Serious",
|
|
719 |
#' line_col_opt = c("Y" = "red", "N" = "blue")
|
|
720 |
#' ),
|
|
721 |
#' rs = list(
|
|
722 |
#' data = ADRS,
|
|
723 |
#' var = ADRS$PARAMCD
|
|
724 |
#' ),
|
|
725 |
#' arrow_end_day = ADSL$max_day,
|
|
726 |
#' xlim = c(-28, ADSL$max_day),
|
|
727 |
#' xlab = "Study Day",
|
|
728 |
#' title = paste("Patient Profile: ", ADSL$USUBJID)
|
|
729 |
#' )
|
|
730 |
g_patient_profile <- function(ex = NULL, |
|
731 |
ae = NULL, |
|
732 |
rs = NULL, |
|
733 |
cm = NULL, |
|
734 |
lb = NULL, |
|
735 |
arrow_end_day,
|
|
736 |
xlim = c(-28, 365), |
|
737 |
xlab = "Study Day", |
|
738 |
title = "Patient Profile") { |
|
739 | ! |
domains <- list(ex = ex, ae = ae, rs = rs, cm = cm, lb = lb) |
740 | ! |
select <- mapply(domain_check, domains, names(domains)) |
741 | ! |
names(select) <- names(domains) |
742 | ||
743 | ! |
show_days_label <- c(FALSE, FALSE, FALSE, FALSE, FALSE) |
744 | ! |
show_days_label[max(which(select == TRUE))] <- TRUE |
745 | ||
746 | ! |
show_title <- c(FALSE, FALSE, FALSE, FALSE, FALSE) |
747 | ! |
show_title[min(which(select == TRUE))] <- TRUE |
748 | ||
749 | ! |
if (!is.null(getOption("ggplot2.discrete.colour"))) { |
750 | ! |
cols <- getOption("ggplot2.discrete.colour") |
751 |
} else { |
|
752 | ! |
cols <- NULL |
753 |
}
|
|
754 | ||
755 |
# Domain "ADEX"
|
|
756 | ! |
if (select["ex"]) { |
757 | ! |
p1 <- patient_domain_profile( |
758 | ! |
domain = "Exposure (ADEX)", |
759 | ! |
var_names = ex$var, |
760 | ! |
marker_pos = ex$data$ASTDT_dur, |
761 | ! |
arrow_end = arrow_end_day, |
762 | ! |
xtick_at = waiver(), |
763 | ! |
line_col_list = NULL, |
764 | ! |
line_width = 1, |
765 | ! |
arrow_size = 0.1, |
766 | ! |
no_enddate_extention = 0, |
767 | ! |
marker_col_list = list( |
768 | ! |
marker_col = factor(ex$data$Modification), |
769 | ! |
marker_col_opt = if (!is.null(cols)) cols else c("Increase" = "red", "Decrease" = "green", "None" = "blue") |
770 |
),
|
|
771 | ! |
marker_shape_list = list( |
772 | ! |
marker_shape = factor(ex$data$Modification), |
773 | ! |
marker_shape_opt = c("Increase" = 24, "Decrease" = 25, "None" = 23), |
774 | ! |
marker_shape_legend = "Dose Modification" |
775 |
),
|
|
776 | ! |
show_days_label = show_days_label[1], |
777 | ! |
xlim = xlim, |
778 | ! |
xlab = xlab, |
779 | ! |
show_title = show_title[1], |
780 | ! |
title = title |
781 |
)
|
|
782 |
} else { |
|
783 | ! |
p1 <- NULL |
784 |
}
|
|
785 |
# Domain "ADAE"
|
|
786 | ! |
if (select["ae"]) { |
787 | ! |
p2 <- patient_domain_profile( |
788 | ! |
domain = "Adverse Event (ADAE)", |
789 | ! |
var_names = ae$var, |
790 | ! |
marker_pos = ae$data[, c("ASTDY", "AENDY")], |
791 | ! |
arrow_end = arrow_end_day, |
792 | ! |
xtick_at = waiver(), |
793 | ! |
line_col_list = list( |
794 | ! |
line_col = ae$line_col, |
795 | ! |
line_col_legend = ae$line_col_legend, |
796 | ! |
line_col_opt = ae$line_col_opt |
797 |
),
|
|
798 | ! |
line_width = 1, |
799 | ! |
arrow_size = 0.1, |
800 | ! |
no_enddate_extention = 0.1, |
801 | ! |
marker_col_list = list( |
802 | ! |
marker_col = factor(ae$data$AETOXGR), |
803 | ! |
marker_col_opt = if (!is.null(cols)) { |
804 | ! |
cols
|
805 |
} else { |
|
806 | ! |
c( |
807 | ! |
"1" = "green", "2" = "blue", "3" = "yellow", "4" = "orange", "5" = "red" |
808 |
)
|
|
809 |
},
|
|
810 | ! |
marker_col_legend = "Grade" |
811 |
),
|
|
812 | ! |
marker_shape_list = NULL, |
813 | ! |
show_days_label = show_days_label[2], |
814 | ! |
xlim = xlim, |
815 | ! |
xlab = xlab, |
816 | ! |
show_title = show_title[2], |
817 | ! |
title = title |
818 |
)
|
|
819 |
} else { |
|
820 | ! |
p2 <- NULL |
821 |
}
|
|
822 | ||
823 | ||
824 |
# Domain "ADRS"
|
|
825 | ! |
if (select["rs"]) { |
826 | ! |
p3 <- patient_domain_profile( |
827 | ! |
domain = "Response (ADRS)", |
828 | ! |
var_names = rs$var, |
829 | ! |
marker_pos = rs$data$ADY, |
830 | ! |
arrow_end = arrow_end_day, |
831 | ! |
xtick_at = waiver(), |
832 | ! |
line_col_list = NULL, |
833 | ! |
line_width = 1, |
834 | ! |
arrow_size = 0.1, |
835 | ! |
no_enddate_extention = 0, |
836 | ! |
marker_col_list = list( |
837 | ! |
marker_col = factor(rs$data$AVALC), |
838 | ! |
marker_col_opt = if (!is.null(cols)) { |
839 | ! |
cols
|
840 |
} else { |
|
841 | ! |
c( |
842 | ! |
"CR" = "green", "PR" = "blue", "SD" = "yellow", "PD" = "red", |
843 | ! |
"NE" = "pink", "Y" = "lightblue", "N" = "darkred" |
844 |
)
|
|
845 |
}
|
|
846 |
),
|
|
847 | ! |
marker_shape_list = list( |
848 | ! |
marker_shape = factor(rs$data$AVALC), |
849 | ! |
marker_shape_opt = c("CR" = 21, "PR" = 24, "SD" = 23, "PD" = 22, "NE" = 14, "Y" = 11, "N" = 8), |
850 | ! |
marker_shape_legend = "Response" |
851 |
),
|
|
852 | ! |
show_days_label = show_days_label[3], |
853 | ! |
xlim = xlim, |
854 | ! |
xlab = xlab, |
855 | ! |
show_title = show_title[3], |
856 | ! |
title = title |
857 |
)
|
|
858 |
} else { |
|
859 | ! |
p3 <- NULL |
860 |
}
|
|
861 | ||
862 | ||
863 |
# Domain "ADCM"
|
|
864 | ! |
if (select["cm"]) { |
865 | ! |
p4 <- patient_domain_profile( |
866 | ! |
domain = "Conmed (ADCM)", |
867 | ! |
var_names = cm$var, |
868 | ! |
marker_pos = cm$data[, c("ASTDY", "AENDY")], |
869 | ! |
arrow_end = arrow_end_day, |
870 | ! |
xtick_at = waiver(), |
871 | ! |
line_col_list = list(line_col_opt = if (!is.null(cols)) cols else "orange"), |
872 | ! |
line_width = 1, |
873 | ! |
arrow_size = 0.1, |
874 | ! |
no_enddate_extention = 0.1, |
875 | ! |
marker_col_list = list(marker_col_opt = if (!is.null(cols)) cols else "orange"), |
876 | ! |
marker_shape_list = NULL, |
877 | ! |
show_days_label = show_days_label[4], |
878 | ! |
xlim = xlim, |
879 | ! |
xlab = xlab, |
880 | ! |
show_title = show_title[4], |
881 | ! |
title = title |
882 |
)
|
|
883 |
} else { |
|
884 | ! |
p4 <- NULL |
885 |
}
|
|
886 | ||
887 |
# Domain "ADLB"
|
|
888 | ! |
if (select["lb"]) { |
889 | ! |
p5 <- patient_domain_profile( |
890 | ! |
domain = "Laboratory (ADLB)", |
891 | ! |
var_names = lb$var, |
892 | ! |
marker_pos = lb$data$ADY, |
893 | ! |
arrow_end = arrow_end_day, |
894 | ! |
xtick_at = waiver(), |
895 | ! |
line_col_list = NULL, |
896 | ! |
line_width = 1, |
897 | ! |
arrow_size = 0.1, |
898 | ! |
no_enddate_extention = 0, |
899 | ! |
marker_col_list = list( |
900 | ! |
marker_col = factor(lb$data$ANRIND), |
901 | ! |
marker_col_opt = if (!is.null(cols)) cols else c("HIGH" = "red", "LOW" = "blue", "NORMAL" = "green") |
902 |
),
|
|
903 | ! |
marker_shape_list = list( |
904 | ! |
marker_shape = factor(lb$data$ANRIND), |
905 | ! |
marker_shape_opt = c("HIGH" = 24, "LOW" = 25, "NORMAL" = 23), |
906 | ! |
marker_shape_legend = "Labs Abnormality" |
907 |
),
|
|
908 | ! |
show_days_label = show_days_label[5], |
909 | ! |
xlim = xlim, |
910 | ! |
xlab = xlab, |
911 | ! |
show_title = show_title[5], |
912 | ! |
title = title |
913 |
)
|
|
914 |
} else { |
|
915 | ! |
p5 <- NULL |
916 |
}
|
|
917 | ||
918 |
# Assemble domain plots into patient profile plot
|
|
919 | ! |
plot_list <- list(p1, p2, p3, p4, p5) |
920 | ||
921 | ! |
plot_list <- plot_list[select] |
922 |
# distribute space by number of levels in each domain
|
|
923 | ! |
var_list <- list(ex$var, ae$var, rs$var, cm$var, lb$var) |
924 | ! |
var_list <- var_list %>% |
925 | ! |
lapply(unique) %>% |
926 | ! |
lapply(length) %>% |
927 | ! |
unlist() %>% |
928 | ! |
cbind(.data, select) %>% |
929 | ! |
as.data.frame() %>% |
930 |
# keep the selected domains
|
|
931 | ! |
dplyr::filter(select == TRUE) %>% |
932 | ! |
dplyr::mutate(nline_dat = ifelse(. <= 10 & . > 0, 10, .)) %>% |
933 |
# relative height
|
|
934 | ! |
dplyr::mutate(sbplt_ht = unlist(nline_dat) / sum(unlist(nline_dat))) |
935 | ||
936 | ! |
cowplot::plot_grid( |
937 | ! |
plotlist = plot_list, |
938 | ! |
nrow = nrow(var_list), |
939 | ! |
align = "v", |
940 | ! |
axis = "lr", |
941 | ! |
rel_heights = var_list$sbplt_ht |
942 |
)
|
|
943 |
}
|
|
944 | ||
945 |
#' a helper function for g_patient_profile to check whether the domain has data available
|
|
946 |
#' @param domain domain input from g_patient_profile
|
|
947 |
#' @param name domain names
|
|
948 |
#' @keywords internal
|
|
949 |
domain_check <- function(domain, name) { |
|
950 | ! |
if (is.null(domain)) { |
951 | ! |
select <- FALSE |
952 |
} else { |
|
953 | ! |
select <- TRUE |
954 | ! |
if (dim(domain$data)[1] == 0 || is.null(domain$data)) { |
955 | ! |
warning(paste("No", name, "data for this subject")) |
956 | ! |
select <- FALSE |
957 |
}
|
|
958 |
}
|
|
959 | ! |
return(select) |
960 |
}
|
1 |
#' Heatmap by Grade
|
|
2 |
#'
|
|
3 |
#' This function plots heatmap
|
|
4 |
#'
|
|
5 |
#' @param id_var (`character`)\cr name of the column that contains the unique subject identifier shared by all data
|
|
6 |
#' Usually it is \code{"USUBJID"}.
|
|
7 |
#' @param exp_data (`data.frame`)\cr exposure data. Usually it is \code{ADEX}.
|
|
8 |
#' @param visit_var (`character`)\cr name of the column that contains the analysis visit. Usually it is \code{"AVISIT"}
|
|
9 |
#' @param ongo_var (`character`)\cr name of the column in \code{exp_data} that contains the logical variable
|
|
10 |
#' indicating whether the treatment is still ongoing.
|
|
11 |
#' Usually it can be derived from \code{EOSSTT}
|
|
12 |
#' @param anno_data (`data.frame`)\cr annotation data that contains subject level characteristics.
|
|
13 |
#' Usually it is \code{ADSL}
|
|
14 |
#' @param anno_var (`character`) a vector of columns name(s) to include for the annotation
|
|
15 |
#' @param heat_data (`data.frame`)\cr data frame that contains the information needed for the text over heatmap
|
|
16 |
#' Usually it is \code{ADCM}.
|
|
17 |
#' @param heat_color_var (`character`)\cr name of the column that contains the heat grade
|
|
18 |
#' @param heat_color_opt optional, (`character`)\cr a named vector that maps the names to heat colors
|
|
19 |
#' @param conmed_data optional, (`data.frame`)\cr concomitant medicine data. Usually it is \code{ADCM}
|
|
20 |
#' default is `NULL` (no `conmed` plotted)
|
|
21 |
#' @param conmed_var optional, (`character`)\cr concomitant medicine variable name. Must be a column name in
|
|
22 |
#' `conmed_data` when `conmed_data` is provided. default is `NULL` (no `conmed` plotted)
|
|
23 |
#' @param conmed_color_opt optional, (`character`)\cr vector of color name(s) to `conmed_data`
|
|
24 |
#' @param xlab optional, (`character`)\cr string to be shown as x-axis label, default is \code{"Visit"}
|
|
25 |
#' @param title (`character`)\cr string to be shown as title of the plot.
|
|
26 |
#' default is `NULL` (no plot title is displayed)
|
|
27 |
#'
|
|
28 |
#' @export
|
|
29 |
#'
|
|
30 |
#' @author Nina Qi (qit3) \email{qit3@gene.com}
|
|
31 |
#' @author Molly He (hey59) \email{hey59@gene.com}
|
|
32 |
#'
|
|
33 |
#' @examples
|
|
34 |
#' library(dplyr)
|
|
35 |
#'
|
|
36 |
#' ADSL <- osprey::rADSL %>% slice(1:30)
|
|
37 |
#' ADEX <- osprey::rADEX %>% filter(USUBJID %in% ADSL$USUBJID)
|
|
38 |
#' ADAE <- osprey::rADAE %>% filter(USUBJID %in% ADSL$USUBJID)
|
|
39 |
#' ADCM <- osprey::rADCM %>% filter(USUBJID %in% ADSL$USUBJID)
|
|
40 |
#' # function to derive AVISIT from ADEX
|
|
41 |
#' add_visit <- function(data_need_visit) {
|
|
42 |
#' visit_dates <- ADEX %>%
|
|
43 |
#' filter(PARAMCD == "DOSE") %>%
|
|
44 |
#' distinct(USUBJID, AVISIT, ASTDTM) %>%
|
|
45 |
#' group_by(USUBJID) %>%
|
|
46 |
#' arrange(ASTDTM) %>%
|
|
47 |
#' mutate(next_vis = lead(ASTDTM), is_last = ifelse(is.na(next_vis), TRUE, FALSE)) %>%
|
|
48 |
#' rename(this_vis = ASTDTM)
|
|
49 |
#' data_visit <- data_need_visit %>%
|
|
50 |
#' select(USUBJID, ASTDTM) %>%
|
|
51 |
#' left_join(visit_dates, by = "USUBJID", relationship = "many-to-many") %>%
|
|
52 |
#' filter(ASTDTM > this_vis & (ASTDTM < next_vis | is_last == TRUE)) %>%
|
|
53 |
#' left_join(data_need_visit, relationship = "many-to-many")
|
|
54 |
#' return(data_visit)
|
|
55 |
#' }
|
|
56 |
#' # add AVISIT in ADAE and ADCM
|
|
57 |
#' ADAE <- add_visit(ADAE)
|
|
58 |
#' ADCM <- add_visit(ADCM)
|
|
59 |
#' exp_data <- ADEX %>%
|
|
60 |
#' filter(PARCAT1 == "INDIVIDUAL") %>%
|
|
61 |
#' group_by(USUBJID) %>%
|
|
62 |
#' # create a shorter subject identifier
|
|
63 |
#' mutate(SUBJ = utils::tail(strsplit(USUBJID, "-")[[1]], n = 1)) %>%
|
|
64 |
#' mutate(ongo_var = (EOSSTT == "ONGOING")) %>%
|
|
65 |
#' ungroup()
|
|
66 |
#' anno_data <- ADSL %>%
|
|
67 |
#' select(SEX, COUNTRY, USUBJID) %>%
|
|
68 |
#' group_by(USUBJID) %>%
|
|
69 |
#' mutate(SUBJ = utils::tail(strsplit(USUBJID, "-")[[1]], n = 1)) %>%
|
|
70 |
#' ungroup() %>%
|
|
71 |
#' select(-USUBJID)
|
|
72 |
#' heat_data <- ADAE %>%
|
|
73 |
#' select(USUBJID, AVISIT, AETOXGR) %>%
|
|
74 |
#' group_by(USUBJID) %>%
|
|
75 |
#' mutate(SUBJ = utils::tail(strsplit(USUBJID, "-")[[1]], n = 1)) %>%
|
|
76 |
#' ungroup() %>%
|
|
77 |
#' select(-USUBJID)
|
|
78 |
#' heat_color_opt <- c(
|
|
79 |
#' "No Event" = "gray90",
|
|
80 |
#' "1" = "lightsteelblue1",
|
|
81 |
#' "2" = "steelblue1",
|
|
82 |
#' "3" = "steelblue4",
|
|
83 |
#' "4" = "maroon",
|
|
84 |
#' "5" = "brown4"
|
|
85 |
#' )
|
|
86 |
#' cmdecod_label <- attr(ADCM[["CMDECOD"]], "label")
|
|
87 |
#' ADCM <- ADCM %>%
|
|
88 |
#' filter(
|
|
89 |
#' CMDECOD == "medname A_1/3" | CMDECOD == "medname A_2/3" | CMDECOD == "medname A_3/3"
|
|
90 |
#' ) %>%
|
|
91 |
#' mutate(CMDECOD = factor(CMDECOD, levels = unique(CMDECOD)))
|
|
92 |
#' attr(ADCM[["CMDECOD"]], "label") <- cmdecod_label
|
|
93 |
#' conmed_data <- ADCM %>%
|
|
94 |
#' group_by(USUBJID) %>%
|
|
95 |
#' mutate(SUBJ = utils::tail(strsplit(USUBJID, "-")[[1]], n = 1))
|
|
96 |
#' # example plotting conmed
|
|
97 |
#' g_heat_bygrade(
|
|
98 |
#' id_var = "SUBJ",
|
|
99 |
#' exp_data,
|
|
100 |
#' visit_var = "AVISIT",
|
|
101 |
#' ongo_var = "ongo_var",
|
|
102 |
#' anno_data,
|
|
103 |
#' anno_var = c("SEX", "COUNTRY"),
|
|
104 |
#' heat_data,
|
|
105 |
#' heat_color_var = "AETOXGR",
|
|
106 |
#' heat_color_opt,
|
|
107 |
#' conmed_data,
|
|
108 |
#' conmed_var = "CMDECOD",
|
|
109 |
#' conmed_color_opt = c("green", "green3", "green4")
|
|
110 |
#' )
|
|
111 |
#' # example not plotting conmed
|
|
112 |
#' g_heat_bygrade(
|
|
113 |
#' id_var = "SUBJ",
|
|
114 |
#' exp_data,
|
|
115 |
#' visit_var = "AVISIT",
|
|
116 |
#' ongo_var = "ongo_var",
|
|
117 |
#' anno_data,
|
|
118 |
#' anno_var = c("SEX", "COUNTRY"),
|
|
119 |
#' heat_data,
|
|
120 |
#' heat_color_var = "AETOXGR",
|
|
121 |
#' heat_color_opt
|
|
122 |
#' )
|
|
123 |
#'
|
|
124 |
g_heat_bygrade <- function(id_var, |
|
125 |
exp_data,
|
|
126 |
visit_var,
|
|
127 |
ongo_var,
|
|
128 |
anno_data,
|
|
129 |
anno_var,
|
|
130 |
heat_data,
|
|
131 |
heat_color_var,
|
|
132 |
heat_color_opt = NULL, |
|
133 |
conmed_data = NULL, |
|
134 |
conmed_var = NULL, |
|
135 |
conmed_color_opt = NULL, |
|
136 |
xlab = "Visit", |
|
137 |
title = NULL) { |
|
138 |
# check if all PARCAT1 in exp_data is "individual"
|
|
139 | ! |
checkmate::assert_string(id_var) |
140 | ! |
checkmate::assert_data_frame(exp_data) |
141 | ! |
stopifnot( |
142 | ! |
"invalid argument: please only include 'INDIVIDUAL' record in exp_data" = !is.na(exp_data[[visit_var]]) |
143 |
)
|
|
144 | ! |
checkmate::assert_string(visit_var) |
145 | ! |
checkmate::assert_string(ongo_var) |
146 | ! |
checkmate::assert( |
147 | ! |
combine = "and", |
148 | ! |
checkmate::check_choice(ongo_var, names(exp_data)), |
149 | ! |
checkmate::check_logical(exp_data[[ongo_var]]) |
150 |
)
|
|
151 | ! |
checkmate::assert_data_frame(anno_data) |
152 | ! |
stopifnot( |
153 | ! |
"invalid argument: anno_data can only contain 3 or less columns including subject ID" = length(anno_var) <= 2 |
154 |
)
|
|
155 | ! |
checkmate::assert_data_frame(heat_data) |
156 | ! |
checkmate::assert_choice(heat_color_var, names(heat_data)) |
157 | ! |
stopifnot( |
158 | ! |
"invalid argument: need to provide conmed_data and conmed_var" = |
159 | ! |
any(is.null(conmed_data), is.null(conmed_data) == is.null(conmed_var)) |
160 |
)
|
|
161 | ! |
checkmate::assert_choice(conmed_var, names(conmed_data), null.ok = TRUE) |
162 | ! |
stopifnot( |
163 | ! |
"invalid argument: please only include no more than three conmeds for plotting" = |
164 | ! |
is.null(conmed_var) || length(levels(conmed_data[[conmed_var]])) <= 3 |
165 |
)
|
|
166 | ! |
stopifnot( |
167 | ! |
"invalid argument: please specify conmed_color_opt for all unique conmed_var" = |
168 | ! |
is.null(conmed_data) || |
169 | ! |
is.null(conmed_color_opt) || # nolint indentation_linter |
170 | ! |
length(conmed_color_opt) == length(unique(conmed_data[[conmed_var]])) |
171 |
)
|
|
172 | ||
173 |
if ( |
|
174 |
!( |
|
175 | ! |
(is.null(conmed_data) || id_var %in% names(conmed_data)) && |
176 | ! |
id_var %in% names(exp_data) && id_var %in% names(anno_data) && id_var %in% names(heat_data) |
177 |
)
|
|
178 |
) { |
|
179 | ! |
stop( |
180 | ! |
paste( |
181 | ! |
"exp_data, anno_data, heat_data, and conmed_data (if plotting conmed) must include a column named",
|
182 | ! |
id_var,
|
183 | ! |
sep = " " |
184 |
)
|
|
185 |
)
|
|
186 |
}
|
|
187 |
if ( |
|
188 |
!( |
|
189 | ! |
(is.null(conmed_data) || visit_var %in% names(conmed_data)) && |
190 | ! |
visit_var %in% names(exp_data) && visit_var %in% names(heat_data) |
191 |
)
|
|
192 |
) { |
|
193 | ! |
stop( |
194 | ! |
paste( |
195 | ! |
"exp_data, heat_data, and conmed_data (if plotting conmed) must include a column named",
|
196 | ! |
visit_var,
|
197 | ! |
sep = " " |
198 |
)
|
|
199 |
)
|
|
200 |
}
|
|
201 | ||
202 | ! |
anl_data <- exp_data %>% |
203 | ! |
select(!!id_var, !!sym(visit_var)) %>% |
204 | ! |
left_join(heat_data, by = c(id_var, visit_var)) %>% |
205 | ! |
distinct() %>% |
206 | ! |
mutate(heat_color_num = tidyr::replace_na(as.numeric(.data[[heat_color_var]]), 0)) %>% |
207 | ! |
group_by(!!sym(id_var), !!sym(visit_var)) %>% |
208 | ! |
arrange(!!sym(visit_var)) %>% |
209 | ! |
mutate(heat_color_max = factor(max(.data$heat_color_num), levels = 0:5)) %>% |
210 | ! |
select(-(!!heat_color_var), -"heat_color_num") %>% # nolint |
211 | ! |
distinct() %>% |
212 | ! |
left_join(anno_data, by = id_var) |
213 | ||
214 |
# dose reduction data
|
|
215 | ! |
ex_red <- exp_data %>% |
216 | ! |
filter(.data$PARAMCD == "DOSE") %>% |
217 | ! |
group_by(!!sym(id_var)) %>% |
218 | ! |
arrange(.data$ASTDTM) %>% |
219 | ! |
mutate( |
220 | ! |
RANK = order(.data$ASTDTM), |
221 | ! |
LASTDOSE = lag(.data$AVAL), |
222 | ! |
DOSERED = ifelse(.data$RANK != 1 & .data$AVAL < .data$LASTDOSE, TRUE, FALSE) |
223 |
) %>% |
|
224 | ! |
select(!!sym(id_var), !!sym(visit_var), "RANK", "AVAL", "LASTDOSE", "DOSERED") %>% |
225 | ! |
filter(.data$DOSERED == TRUE) |
226 |
# does ongoing data
|
|
227 | ! |
exp_lst <- exp_data %>% |
228 | ! |
filter(.data$PARAMCD == "DOSE") %>% |
229 | ! |
filter(!!sym(ongo_var) == TRUE) %>% |
230 | ! |
group_by(!!sym(id_var)) %>% |
231 | ! |
arrange(!!sym(visit_var)) %>% |
232 | ! |
slice_tail() %>% |
233 | ! |
select(!!sym(id_var), !!sym(visit_var)) |
234 | ! |
visit_levels <- unique(anl_data[[visit_var]]) |
235 | ! |
if (!is.null(conmed_data) && !is.null(conmed_var)) { |
236 | ! |
conmed_data <- conmed_data %>% |
237 | ! |
left_join(anl_data, by = c(id_var, visit_var)) %>% |
238 | ! |
ungroup() %>% |
239 | ! |
mutate( |
240 | ! |
conmed_num = as.numeric(.data[[conmed_var]]), |
241 | ! |
conmed_num_m = stats::median(unique(.data$conmed_num), na.rm = TRUE) |
242 |
) %>% |
|
243 | ! |
mutate( |
244 | ! |
distance = (ifelse( |
245 | ! |
.data$conmed_num <= .data$conmed_num_m, |
246 | ! |
.data$conmed_num - 1, |
247 | ! |
.data$conmed_num + 1 |
248 | ! |
) - .data$conmed_num_m) / 5, |
249 | ! |
conmed_x = as.numeric(!!sym(visit_var)) + .data$distance |
250 |
)
|
|
251 |
}
|
|
252 | ! |
subj_levels <- unique(anl_data[[id_var]]) |
253 | ! |
levels(anl_data$heat_color_max)[levels(anl_data$heat_color_max) == "0"] <- "No Event" |
254 | ! |
p <- ggplot( |
255 | ! |
data = anl_data, |
256 | ! |
aes(x = !!sym(visit_var), y = factor(!!sym(id_var), levels = c(rev(subj_levels), ""))) |
257 |
) + |
|
258 | ! |
geom_tile(aes(fill = .data$heat_color_max)) + |
259 | ! |
scale_y_discrete(drop = FALSE) + |
260 | ! |
scale_fill_discrete( |
261 | ! |
name = "Highest grade of\nindividual events", |
262 | ! |
type = if (!is.null(heat_color_opt)) { |
263 | ! |
heat_color_opt
|
264 |
} else { |
|
265 | ! |
if (!is.null(getOption("ggplot2.discrete.colour"))) { |
266 | ! |
rev(grDevices::hcl.colors(6, palette = "peach")) |
267 |
} else { |
|
268 | ! |
rev(grDevices::terrain.colors(6)) |
269 |
}
|
|
270 |
}
|
|
271 |
) + |
|
272 |
# plot dose reduction
|
|
273 | ! |
geom_segment( |
274 | ! |
data = ex_red, |
275 | ! |
aes( |
276 | ! |
y = as.numeric(factor(!!sym(id_var), levels = rev(subj_levels))) + 0.3, |
277 | ! |
x = as.numeric(factor(!!sym(visit_var), levels = visit_levels)), |
278 | ! |
yend = as.numeric(factor(!!sym(id_var), levels = rev(subj_levels))) - 0.3, |
279 | ! |
xend = as.numeric(factor(!!sym(visit_var), levels = visit_levels)) |
280 |
),
|
|
281 | ! |
arrow = arrow(length = grid::unit(0.1, "cm")), |
282 | ! |
linewidth = .5, |
283 | ! |
color = "black" |
284 |
) + |
|
285 |
# plot ongoing
|
|
286 | ! |
geom_segment( |
287 | ! |
data = exp_lst, |
288 | ! |
aes( |
289 | ! |
x = as.numeric(factor(!!sym(visit_var), levels = visit_levels)) + 0.5, |
290 | ! |
xend = as.numeric(factor(!!sym(visit_var), levels = visit_levels)) + 0.65, |
291 | ! |
y = as.numeric(factor(!!sym(id_var), levels = rev(subj_levels))), |
292 | ! |
yend = as.numeric(factor(!!sym(id_var), levels = rev(subj_levels))) |
293 |
),
|
|
294 | ! |
arrow = arrow(length = grid::unit(0.1, "cm")), |
295 | ! |
linewidth = .5, |
296 | ! |
color = "black" |
297 |
)
|
|
298 | ! |
if (!is.null(conmed_data) && !is.null(conmed_var)) { |
299 | ! |
p <- p + |
300 | ! |
geom_point( |
301 | ! |
data = conmed_data, |
302 | ! |
aes( |
303 | ! |
x = .data$conmed_x, |
304 | ! |
y = as.numeric(factor(!!sym(id_var), levels = rev(subj_levels))), |
305 | ! |
shape = .data[[conmed_var]], |
306 | ! |
color = .data[[conmed_var]] |
307 |
),
|
|
308 | ! |
size = 2 |
309 |
) + |
|
310 | ! |
scale_colour_manual( |
311 | ! |
name = attr(conmed_data[[conmed_var]], "label"), |
312 | ! |
values = if (!is.null(conmed_color_opt)) { |
313 | ! |
conmed_color_opt
|
314 |
} else { |
|
315 | ! |
if (!is.null(getOption("ggplot2.discrete.colour"))) { |
316 | ! |
getOption("ggplot2.discrete.colour")[-2] |
317 |
} else { |
|
318 | ! |
rep("black", 5) |
319 |
}
|
|
320 |
}
|
|
321 |
) + |
|
322 | ! |
scale_shape_manual( |
323 | ! |
name = attr(conmed_data[[conmed_var]], "label"), |
324 | ! |
values = c(15:17) |
325 |
)
|
|
326 |
}
|
|
327 | ||
328 | ! |
p <- p + |
329 | ! |
theme_bw() + |
330 | ! |
theme( |
331 | ! |
panel.background = element_blank(), |
332 | ! |
panel.grid = element_blank(), |
333 | ! |
axis.line = element_line(colour = "black"), |
334 | ! |
axis.text.x = element_text(angle = 90), |
335 | ! |
axis.text.y = element_blank(), |
336 | ! |
axis.title.y = element_blank() |
337 |
) + |
|
338 | ! |
labs(x = xlab, y = "ylab") |
339 |
# plot title and labels
|
|
340 | ! |
if (!is.null(title)) { |
341 | ! |
p <- p + |
342 | ! |
labs(title = title) + |
343 | ! |
theme(plot.title = element_text(face = "bold")) |
344 |
}
|
|
345 | ||
346 |
# plot left legend
|
|
347 | ! |
t <- anl_data %>% |
348 | ! |
as.data.frame() %>% |
349 | ! |
select((!!anno_var), (!!id_var)) %>% |
350 | ! |
distinct() |
351 | ! |
my_theme <- gridExtra::ttheme_default( |
352 | ! |
core = list( |
353 | ! |
bg_params = list(fill = NA, col = NA), |
354 | ! |
fg_params = list(cex = 0.8) |
355 |
),
|
|
356 | ! |
colhead = list( |
357 | ! |
bg_params = list(fill = NA, col = NA), |
358 | ! |
fg_params = list(cex = 0.8) |
359 |
)
|
|
360 |
)
|
|
361 | ! |
tb <- gridExtra::tableGrob(t, rows = NULL, theme = my_theme) |
362 | ! |
tb$heights <- grid::unit(rep(1 / nrow(tb), nrow(tb)), "null") |
363 | ||
364 |
# grab plot and table as one plot
|
|
365 | ! |
g0 <- ggplotGrob(p) |
366 | ! |
g1 <- gtable::gtable_add_cols(g0, sum(tb$widths), 0) |
367 | ! |
g <- gtable::gtable_add_grob(g1, tb, t = g1$layout[g1$layout$name == "panel", 1], l = 1) |
368 | ||
369 | ! |
grid::grid.newpage() |
370 | ! |
grid::grid.draw(g) |
371 | ! |
invisible(g) |
372 |
}
|
1 |
#' Spider Plot
|
|
2 |
#'
|
|
3 |
#' Spider plot is often used in Early Development (ED) and displays individual
|
|
4 |
#' patient plot of an endpoint over time by group.
|
|
5 |
#'
|
|
6 |
#'
|
|
7 |
#' @param marker_x vector of x values (must be in sorted order)
|
|
8 |
#' @param marker_id vector to group the points together (default
|
|
9 |
#' should be `USUBJID`)
|
|
10 |
#' @param marker_y vector of y values
|
|
11 |
#' @param line_colby vector defines by what variable plot is color coded,
|
|
12 |
#' default here is `NULL`
|
|
13 |
#' @param line_color_opt vector defines line color, default here is `NULL`
|
|
14 |
#' @param marker_size size of markers in plot, default here is `NULL`
|
|
15 |
#' @param marker_shape vector defines by what variable points are shape coded,
|
|
16 |
#' , default here is `NULL`
|
|
17 |
#' @param marker_shape_opt vector defines marker shape code, default here is `NULL`
|
|
18 |
#' @param datalabel_txt list defines text (at last time point) and flag for an arrow annotation:
|
|
19 |
#' - (per defined variable) elements must be labeled `txt_ann`/`mrkr_all`/`mrkr_ann`.
|
|
20 |
#' - `txt_ann` text annotation next to final data point (for text annotation)
|
|
21 |
#' - `mrkr_all` vector of ID's (for annotation marker)
|
|
22 |
#' - `mrkr_ann` vector of ID's (subset of `mrkr_all`) where arrow is desired to
|
|
23 |
#' indicate any study interim points. Default here is `NULL`.
|
|
24 |
#' @param facet_rows dataframe defines what variable is used to split the
|
|
25 |
#' plot into rows, default here is `NULL`.
|
|
26 |
#' @param facet_columns dataframe defines what variable is used to split the
|
|
27 |
#' plot into columns, default here is `NULL`.
|
|
28 |
#' @param vref_line value defines vertical line overlay
|
|
29 |
#' (can be a vector), default here is `NULL`.
|
|
30 |
#' @param href_line value defines horizontal line overlay
|
|
31 |
#' (can be a vector), default here is `NULL`.
|
|
32 |
#' @param x_label string of text for x axis label, default is time.
|
|
33 |
#' @param y_label string of text for y axis label, default is % change.
|
|
34 |
#' @param show_legend boolean of whether marker legend is included, default here is `FALSE`.
|
|
35 |
#'
|
|
36 |
#' @return `ggplot` object
|
|
37 |
#'
|
|
38 |
#' @details there is no equivalent STREAM output
|
|
39 |
#'
|
|
40 |
#' @export
|
|
41 |
#'
|
|
42 |
#' @template author_zhanc107
|
|
43 |
#'
|
|
44 |
#' @examplesIf require("nestcolor")
|
|
45 |
#' # simple example
|
|
46 |
#' library(dplyr)
|
|
47 |
#' library(nestcolor)
|
|
48 |
#'
|
|
49 |
#' ADTR <- osprey::rADTR %>% select(STUDYID, USUBJID, ADY, AVISIT, CHG, PCHG, PARAMCD)
|
|
50 |
#' ADSL <- osprey::rADSL %>% select(STUDYID, USUBJID, RACE, SEX, ARM)
|
|
51 |
#' ANL <- left_join(ADTR, ADSL, by = c("STUDYID", "USUBJID"))
|
|
52 |
#' ANL <- ANL %>%
|
|
53 |
#' dplyr::filter(PARAMCD == "SLDINV" & AVISIT != "POST-BASELINE MINIMUM") %>%
|
|
54 |
#' dplyr::filter(RACE %in% c("WHITE", "ASIAN")) %>%
|
|
55 |
#' group_by(USUBJID) %>%
|
|
56 |
#' dplyr::arrange(ADY) %>%
|
|
57 |
#' dplyr::mutate(
|
|
58 |
#' CHG = ifelse(AVISIT == "Screening", 0, CHG),
|
|
59 |
#' PCHG = ifelse(AVISIT == "Screening", 0, PCHG)
|
|
60 |
#' )
|
|
61 |
#' ANL$USUBJID <- substr(ANL$USUBJID, 14, 18)
|
|
62 |
#'
|
|
63 |
#' # Plot 1 - default color and shape mapping
|
|
64 |
#' g_spiderplot(
|
|
65 |
#' marker_x = ANL$ADY,
|
|
66 |
#' marker_id = ANL$USUBJID,
|
|
67 |
#' marker_y = ANL$PCHG,
|
|
68 |
#' line_colby = ANL$USUBJID,
|
|
69 |
#' marker_shape = ANL$USUBJID,
|
|
70 |
#' # marker_size = 5,
|
|
71 |
#' datalabel_txt = list(txt_ann = ANL$USUBJID),
|
|
72 |
#' # facet_rows = data.frame(sex = ANL$SEX),
|
|
73 |
#' # facet_columns = data.frame(arm = ANL$ARM),
|
|
74 |
#' vref_line = c(42, 86),
|
|
75 |
#' href_line = c(-20, 20),
|
|
76 |
#' x_label = "Time (Days)",
|
|
77 |
#' y_label = "Change (%) from Baseline",
|
|
78 |
#' show_legend = TRUE
|
|
79 |
#' )
|
|
80 |
#'
|
|
81 |
#' # Plot 2 - with line color mapping
|
|
82 |
#' g_spiderplot(
|
|
83 |
#' marker_x = ANL$AVISIT,
|
|
84 |
#' marker_id = ANL$USUBJID,
|
|
85 |
#' marker_y = ANL$CHG,
|
|
86 |
#' line_colby = ANL$RACE,
|
|
87 |
#' line_color_opt = c("WHITE" = "red", "ASIAN" = "blue"),
|
|
88 |
#' marker_shape = ANL$USUBJID,
|
|
89 |
#' x_label = "Visit",
|
|
90 |
#' y_label = "Change from Baseline",
|
|
91 |
#' show_legend = TRUE
|
|
92 |
#' )
|
|
93 |
g_spiderplot <- function(marker_x, |
|
94 |
marker_id,
|
|
95 |
marker_y,
|
|
96 |
line_colby = NULL, |
|
97 |
line_color_opt = NULL, |
|
98 |
marker_shape = NULL, |
|
99 |
marker_shape_opt = NULL, |
|
100 |
marker_size = 3, |
|
101 |
datalabel_txt = NULL, # USUBJID default |
|
102 |
facet_rows = NULL, |
|
103 |
facet_columns = NULL, |
|
104 |
vref_line = NULL, |
|
105 |
href_line = NULL, |
|
106 |
x_label = "Time (Days)", |
|
107 |
y_label = "Change (%) from Baseline", |
|
108 |
show_legend = FALSE) { |
|
109 | ! |
check_input_length <- c(nrow(data.frame(marker_x)), nrow(data.frame(marker_id)), nrow(data.frame(marker_y))) |
110 | ||
111 | ! |
if (length(unique(check_input_length)) > 1) { |
112 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
113 |
}
|
|
114 | ! |
if (any(check_input_length == 0)) { |
115 | ! |
stop("invalid arguments: check that inputs are not null") |
116 |
}
|
|
117 | ||
118 |
# set up data-------
|
|
119 | ! |
dat <- data.frame(x = marker_x, y = marker_y, group = marker_id) |
120 | ||
121 | ! |
if (!is.null(marker_shape)) { |
122 | ! |
if (length(unique(c(nrow(data.frame(marker_shape)), check_input_length))) != 1) { |
123 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
124 |
}
|
|
125 | ! |
dat$sh <- marker_shape |
126 |
}
|
|
127 | ! |
if (!is.null(facet_rows)) { |
128 | ! |
if (length(unique(c(nrow(facet_rows), check_input_length))) != 1) { |
129 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
130 |
}
|
|
131 | ! |
dat$f_rows <- interaction(facet_rows) |
132 |
}
|
|
133 | ! |
if (!is.null(facet_columns)) { |
134 | ! |
if (length(unique(c(nrow(facet_columns), check_input_length))) != 1) { |
135 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
136 |
}
|
|
137 | ! |
dat$f_columns <- interaction(facet_columns) |
138 |
}
|
|
139 | ! |
if (!is.null(line_colby)) { |
140 | ! |
if (length(unique(c(nrow(data.frame(line_colby)), check_input_length))) != 1) { |
141 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
142 |
}
|
|
143 | ! |
dat$l_col <- line_colby |
144 |
}
|
|
145 | ! |
if (!is.null(datalabel_txt$txt_ann)) { |
146 | ! |
dat$lbl_all <- datalabel_txt$txt_ann |
147 | ||
148 | ! |
dat <- dat %>% |
149 | ! |
group_by(.data$lbl_all) %>% |
150 | ! |
dplyr::mutate(lab = ifelse(.data$x == last(.data$x), as.character(.data$lbl_all), " ")) |
151 |
}
|
|
152 | ! |
if (!is.null(datalabel_txt$mrkr_all) && !is.null(datalabel_txt$mrkr_ann)) { |
153 | ! |
if (length(unique(c(nrow(datalabel_txt$mrkr_all), check_input_length))) != 1) { |
154 | ! |
stop("invalid arguments: check that the length of input arguments are identical") |
155 |
}
|
|
156 | ! |
dat$id <- datalabel_txt$mrkr_all |
157 |
}
|
|
158 | ||
159 | ! |
dat <- dat %>% as.data.frame() |
160 | ||
161 |
# plot spider plot----------------- this section can be condensed later
|
|
162 | ! |
pl <- ggplot(data = dat, aes(x = .data$x, y = .data$y, group = .data$group)) + |
163 | ! |
xlab(x_label) + |
164 | ! |
ylab(y_label) + |
165 | ! |
theme(legend.position = "top", legend.title = element_blank()) |
166 | ||
167 | ! |
pl <- pl + geom_hline(yintercept = 0, linetype = "solid", color = "gray", linewidth = 1) |
168 | ||
169 | ||
170 | ! |
pl <- pl + |
171 | ! |
geom_line( |
172 | ! |
mapping = if (!is.null(line_colby)) { |
173 | ! |
aes(color = .data$l_col) |
174 |
} else { |
|
175 | ! |
NULL
|
176 |
},
|
|
177 | ! |
linewidth = 1, |
178 | ! |
alpha = 0.5, |
179 | ! |
show.legend = show_legend |
180 |
)
|
|
181 | ||
182 |
# marker shape------------ this section can be condensed later
|
|
183 | ! |
if (!is.null(marker_shape)) { |
184 | ! |
pl <- pl + |
185 | ! |
geom_point(mapping = if (!is.null(line_colby)) { |
186 | ! |
aes(shape = .data$sh, color = .data$l_col) |
187 |
} else { |
|
188 | ! |
aes(shape = .data$sh) |
189 | ! |
}, size = marker_size, show.legend = show_legend) |
190 | ! |
} else if (is.null(marker_shape)) { |
191 | ! |
pl <- pl + |
192 | ! |
geom_point(mapping = if (!is.null(line_colby)) { |
193 | ! |
aes(color = .data$l_col) |
194 |
} else { |
|
195 | ! |
NULL
|
196 | ! |
}, size = 3, show.legend = show_legend) |
197 |
}
|
|
198 | ||
199 |
# label at last data point---------
|
|
200 | ! |
if (!is.null(datalabel_txt)) { |
201 | ! |
if (!is.null(datalabel_txt$txt_ann) && is.null(datalabel_txt$mrkr_all) && is.null(datalabel_txt$mrkr_ann)) { |
202 | ! |
pl <- pl + |
203 | ! |
geom_text( |
204 | ! |
data = dat, |
205 | ! |
aes(x = .data$x, y = .data$y, label = .data$lab), hjust = -0.3, |
206 | ! |
size = 4, |
207 | ! |
show.legend = FALSE |
208 |
)
|
|
209 | ! |
} else if (is.null(datalabel_txt$txt_ann) && !is.null(datalabel_txt$mrkr_all) && !is.null(datalabel_txt$mrkr_ann)) { |
210 | ! |
dat_arrow <- dat %>% |
211 | ! |
dplyr::filter(id %in% datalabel_txt$mrkr_ann) %>% |
212 | ! |
group_by(.data$id) %>% |
213 | ! |
dplyr::filter(.data$x == last(.data$x)) |
214 | ! |
pl <- pl + |
215 | ! |
geom_segment( |
216 | ! |
data = dat_arrow, |
217 | ! |
mapping = aes(x = .data$x, y = .data$y, xend = .data$x, yend = .data$y), |
218 | ! |
arrow = arrow(length = grid::unit(0.15, "inches"), ends = "first", type = "closed"), |
219 | ! |
linewidth = 0.4, |
220 | ! |
color = "black", |
221 | ! |
show.legend = FALSE |
222 |
)
|
|
223 |
} else if ( |
|
224 | ! |
!is.null(datalabel_txt$txt_ann) && |
225 | ! |
!is.null(datalabel_txt$mrkr_all) && |
226 | ! |
!is.null(datalabel_txt$mrkr_ann) |
227 |
) { |
|
228 | ! |
pl <- pl + |
229 | ! |
geom_text( |
230 | ! |
data = dat, |
231 | ! |
aes(x = .data$x, y = .data$y, label = .data$lab), |
232 | ! |
hjust = -0.45, |
233 | ! |
size = 4, |
234 | ! |
show.legend = FALSE |
235 |
)
|
|
236 | ||
237 | ! |
dat_arrow <- dat %>% |
238 | ! |
dplyr::filter(id %in% datalabel_txt$mrkr_ann) %>% |
239 | ! |
group_by(.data$id) %>% |
240 | ! |
dplyr::filter(.data$x == last(.data$x)) |
241 | ||
242 | ! |
pl <- pl + |
243 | ! |
geom_segment( |
244 | ! |
data = dat_arrow, |
245 | ! |
mapping = aes(x = .data$x, y = .data$y, xend = .data$x, yend = .data$y), |
246 | ! |
arrow = arrow(length = grid::unit(0.15, "inches"), ends = "first", type = "closed"), |
247 | ! |
linewidth = 0.4, |
248 | ! |
color = "black", |
249 | ! |
show.legend = FALSE |
250 |
)
|
|
251 |
}
|
|
252 |
}
|
|
253 | ||
254 |
# vertical and horizontal reference lines
|
|
255 | ! |
if (!is.null(href_line)) { |
256 | ! |
pl <- pl + geom_hline(yintercept = href_line, linetype = "dotted", color = "black") |
257 |
}
|
|
258 | ||
259 | ! |
if (!is.null(vref_line)) { |
260 | ! |
for (i in seq_along(vref_line)) { |
261 | ! |
pl <- pl + |
262 | ! |
annotate("segment", |
263 | ! |
x = vref_line[i], |
264 | ! |
y = -Inf, |
265 | ! |
xend = vref_line[i], |
266 | ! |
yend = Inf, |
267 | ! |
linetype = "dotted", |
268 | ! |
color = "black" |
269 |
)
|
|
270 |
}
|
|
271 |
}
|
|
272 | ||
273 |
# facets---------------
|
|
274 | ! |
if (is.null(facet_rows) && is.null(facet_columns)) { |
275 | ! |
pl
|
276 | ! |
} else if (is.null(facet_rows) && !is.null(facet_columns)) { |
277 | ! |
pl <- pl + facet_grid(. ~ f_columns) |
278 | ! |
} else if (is.null(facet_columns) && !is.null(facet_rows)) { |
279 | ! |
pl <- pl + facet_grid(f_rows ~ .) |
280 |
} else { |
|
281 | ! |
pl <- pl + facet_grid(f_rows ~ f_columns) |
282 |
}
|
|
283 | ||
284 |
# simple function to call a vector of color values
|
|
285 | ! |
call_color <- function(len) { |
286 | ! |
dat_col <- data.frame(color_opt = grDevices::colors()) |
287 | ! |
dat_col <- dat_col %>% |
288 | ! |
dplyr::filter(!grepl("white", .data$color_opt)) %>% |
289 | ! |
droplevels() |
290 | ||
291 | ! |
return(dat_col[1:len, 1]) |
292 |
}
|
|
293 | ||
294 |
# remove marker from color legend
|
|
295 | ! |
if (!is.null(line_colby)) { |
296 | ! |
if (!is.null(line_color_opt)) { |
297 | ! |
pl <- pl + scale_color_manual( |
298 | ! |
name = "Color", |
299 | ! |
breaks = dat$l_col, |
300 | ! |
values = line_color_opt |
301 |
)
|
|
302 |
}
|
|
303 |
}
|
|
304 | ||
305 | ! |
if (!is.null(marker_shape) && is.null(marker_shape_opt)) { |
306 | ! |
symbol_val <- c(15:18, 1:14) |
307 | ! |
len <- length(unique(dat$sh)) |
308 | ! |
symbol_val <- rep(symbol_val, ceiling(len / 26)) |
309 | ||
310 | ! |
pl <- pl + scale_shape_manual( |
311 | ! |
values = symbol_val[1:len] |
312 |
)
|
|
313 |
}
|
|
314 | ||
315 | ! |
if (!is.null(marker_shape_opt)) { |
316 | ! |
pl <- pl + scale_shape_manual( |
317 | ! |
name = "Shape", |
318 | ! |
breaks = dat$sh, |
319 | ! |
values = marker_shape_opt |
320 |
)
|
|
321 |
}
|
|
322 | ||
323 |
# modify background color
|
|
324 | ! |
pl <- pl + annotate("segment", x = -Inf, xend = Inf, y = -Inf, yend = -Inf) + |
325 | ! |
annotate("segment", x = -Inf, xend = -Inf, y = -Inf, yend = Inf) + |
326 | ! |
theme_bw() + |
327 | ! |
theme( |
328 | ! |
strip.background = element_rect(linetype = "blank", fill = "white"), |
329 | ! |
text = element_text(size = 16), |
330 | ! |
axis.text = element_text(color = "black"), |
331 | ! |
legend.text = element_text(size = 7), |
332 | ! |
legend.title = element_text(size = 7) |
333 |
) + |
|
334 | ! |
labs(shape = "Shape", color = "Color") # + |
335 | ||
336 | ! |
if (is.numeric(marker_x)) { |
337 | ! |
pl <- pl + xlim(min(marker_x), max(marker_x) * 1.3) |
338 |
} else { |
|
339 | ! |
pl <- pl + |
340 | ! |
scale_x_discrete(expand = c(0.3, 0)) + |
341 | ! |
theme(axis.text.x = element_text(angle = 90)) |
342 |
}
|
|
343 | ||
344 | ! |
grid::grid.draw(pl) |
345 | ! |
invisible(pl) |
346 |
}
|
1 |
#' Hy's Law Plot
|
|
2 |
#'
|
|
3 |
#' A scatter plot typically used to display maximum total bilirubin values (`TBL`)
|
|
4 |
#' versus maximum alanine transaminase (`ALT`) values.
|
|
5 |
#'
|
|
6 |
#' @param id unique subject identifier.
|
|
7 |
#' @param term the term of the observation.
|
|
8 |
#' @param aval analysis value.
|
|
9 |
#' @param arm treatment arm. Used as fill color in the plot.
|
|
10 |
#' @param term_selected string vector of length 2 - the two terms selected to be
|
|
11 |
#' used in the plot. First value corresponds to parameter plotted on the
|
|
12 |
#' x-axis. Second value corresponds to that plotted on the y-axis.
|
|
13 |
#' @param anrhi the high limit of normal range.
|
|
14 |
#' @param folds numeric vector of length two. Indicates the position of the
|
|
15 |
#' reference lines to be drawn. Default c(3, 2) corresponds to a line at
|
|
16 |
#' position 3 on the x-axis and 2 on the y-axis.
|
|
17 |
#' @param text string vector of length four with the label to be shown on each
|
|
18 |
#' quadrant. First value corresponds to label shown in the bottom left
|
|
19 |
#' quadrant. Subsequent values move through the graph clockwise.
|
|
20 |
#' @param caption string of text for footnote. Details of methodology can be
|
|
21 |
#' shown here.
|
|
22 |
#' @param title string of text for plot title.
|
|
23 |
#' @param xlab string of text for x axis label.
|
|
24 |
#' @param ylab string of text for y axis label.
|
|
25 |
#'
|
|
26 |
#' @details
|
|
27 |
#' This graphic is based upon the `eDISH` (evaluation of Drug Induced Serious
|
|
28 |
#' Hepatotoxicity) plot of Watkins et. al. in a 2008 publication from Hepatology.
|
|
29 |
#' Maximum values are defined as the maximum post-baseline value at any time
|
|
30 |
#' during the entire length of the observation period. Both axes are in log
|
|
31 |
#' scale to control for the dispersion of the data. The values are plotted in
|
|
32 |
#' 'times upper limit of normal' where a value of 1 would mean that the result
|
|
33 |
#' was normal. Any value above or below 1 would be considered above the upper
|
|
34 |
#' limit or normal or below the upper limit of normal respectively. For
|
|
35 |
#' instance, a value of 3 would be read as '3 times the upper limit of normal'.
|
|
36 |
#' Reference lines are included to determine various states, based upon clinical
|
|
37 |
#' interpretation of the values and includes the following:
|
|
38 |
#'
|
|
39 |
#' * Hyperbilirubinemia `TBL` at least 2 `xULN` and `ALT` less than 3 `xULN`
|
|
40 |
#' * Normal Range `TBL` <= 1 `xULN` and `ALT` <= 1 `xULN`
|
|
41 |
#' * Temple’s Corollary `TBL` <= 1 `xULN` and `ALT` at least 3 `xULN`
|
|
42 |
#' * Possible Hy's Law `TBL` at least 2 `xULN` and `ALT` at least 3 `xULN`
|
|
43 |
#'
|
|
44 |
#' This plot can easily be adjusted for other lab parameters and reference
|
|
45 |
#' ranges as needed. Consultation with a clinical expert to determine which
|
|
46 |
#' associations would be clinically meaningful and how to interpret those
|
|
47 |
#' associations is recommended.
|
|
48 |
#'
|
|
49 |
#' There is no equivalent STREAM output.
|
|
50 |
#'
|
|
51 |
#' @return plot object
|
|
52 |
#'
|
|
53 |
#' @export
|
|
54 |
#'
|
|
55 |
#' @template author_withycok
|
|
56 |
#' @template author_frankla4
|
|
57 |
#' @template author_holmesw
|
|
58 |
#'
|
|
59 |
#' @examplesIf require("nestcolor")
|
|
60 |
#' library(dplyr)
|
|
61 |
#' library(nestcolor)
|
|
62 |
#'
|
|
63 |
#' # Note: CRP is being used in place of Bilirubin here because this is the only available data
|
|
64 |
#' adsl <- osprey::rADSL
|
|
65 |
#' adlb <- osprey::rADLB %>% mutate(ANRHI = 50)
|
|
66 |
#'
|
|
67 |
#' # Example 1, - Hy's law template (3 and 2 X ULN)
|
|
68 |
#' g_hy_law(
|
|
69 |
#' id = adlb$USUBJID,
|
|
70 |
#' term = adlb$PARAMCD,
|
|
71 |
#' aval = adlb$AVAL,
|
|
72 |
#' arm = adlb$ARM,
|
|
73 |
#' term_selected = c("ALT", "CRP"),
|
|
74 |
#' anrhi = adlb$ANRHI,
|
|
75 |
#' folds = c(3, 2),
|
|
76 |
#' text = c("Normal Range", "Hyperbilirubinemia", "Possible Hy's Law Range", "Temple's Corollary"),
|
|
77 |
#' caption = paste(
|
|
78 |
#' "Maximum values are those maximum values that occur",
|
|
79 |
#' "post-baseline (no time constraints and not necessarily concurrent events)."
|
|
80 |
#' ),
|
|
81 |
#' title = "Max. Total Bilirubin vs. Max. Alanine Aminotransferase",
|
|
82 |
#' xlab = "Maximum Alanine Aminotransferase (/ULN)",
|
|
83 |
#' ylab = "Maximum Total Bilirubin (/ULN)"
|
|
84 |
#' )
|
|
85 |
#'
|
|
86 |
#' # Example 2, - change the quadrant lines and labels
|
|
87 |
#' g_hy_law(
|
|
88 |
#' id = adlb$USUBJID,
|
|
89 |
#' term = adlb$PARAMCD,
|
|
90 |
#' aval = adlb$AVAL,
|
|
91 |
#' arm = adlb$ARM,
|
|
92 |
#' term_selected = c("ALT", "CRP"),
|
|
93 |
#' anrhi = adlb$ANRHI,
|
|
94 |
#' folds = c(10, 15),
|
|
95 |
#' text = c("Quadrant 1", "Quadrant 2", "Quadrant 3", "Quadrant 4"),
|
|
96 |
#' caption = paste(
|
|
97 |
#' "Maximum values are those maximum values that occur",
|
|
98 |
#' "post-baseline (no time constraints and not necessarily concurrent events)."
|
|
99 |
#' ),
|
|
100 |
#' title = "Max. Total Bilirubin vs. Max. Alanine Aminotransferase",
|
|
101 |
#' xlab = "Maximum Alanine Aminotransferase (/ULN)",
|
|
102 |
#' ylab = "Maximum Total Bilirubin (/ULN)"
|
|
103 |
#' )
|
|
104 |
g_hy_law <- function(id, |
|
105 |
term,
|
|
106 |
aval,
|
|
107 |
arm,
|
|
108 |
term_selected,
|
|
109 |
anrhi,
|
|
110 |
folds = c(3, 2), |
|
111 |
text = c("Normal Range", "Hyperbilirubinemia", "Possible Hy's Law Range", "Temple's Corollary"), |
|
112 |
caption = paste( |
|
113 |
"Maximum values are those maximum values that occur post-baseline",
|
|
114 |
"(no time constraints and not necessarily concurrent events)."
|
|
115 |
),
|
|
116 |
title = "Max. Total Bilirubin vs. Max. Alanine Aminotransferase", |
|
117 |
xlab = "Maximum Alanine Aminotransferase (/ULN)", |
|
118 |
ylab = "Maximum Total Bilirubin (/ULN)") { |
|
119 | ! |
checkmate::assert_character(term_selected, len = 2, any.missing = FALSE) |
120 | ! |
checkmate::assert_numeric(folds, len = 2, any.missing = FALSE) |
121 | ! |
checkmate::assert_character(text, len = 4, any.missing = FALSE) |
122 | ! |
checkmate::assert_string(title) |
123 | ! |
checkmate::assert_string(caption) |
124 | ! |
checkmate::assert_string(xlab) |
125 | ! |
checkmate::assert_string(ylab) |
126 | ||
127 | ! |
anl <- data.frame(id, term, aval, arm, anrhi) |
128 | ||
129 | ! |
anl <- anl %>% |
130 | ! |
dplyr::filter(term %in% term_selected) %>% |
131 | ! |
dplyr::group_by(id, term) %>% |
132 | ! |
dplyr::mutate(MAX = max(aval)) %>% |
133 | ! |
dplyr::slice(1) %>% |
134 | ! |
dplyr::mutate(ULN = .data$MAX / anrhi) %>% |
135 | ! |
tidyr::pivot_wider(id_cols = c(id, arm), names_from = term, values_from = "ULN") |
136 | ||
137 | ! |
p <- ggplot(data = anl) + |
138 | ! |
scale_x_continuous( |
139 | ! |
name = xlab, |
140 | ! |
breaks = log10(c(seq(0.1, 1, 0.1), seq(2, 10, 1), seq(20, 100, 10))), |
141 | ! |
limits = c(-1, 2), |
142 | ! |
labels = c(0.1, rep(" ", 8), 1, rep(" ", 8), 10, rep(" ", 8), 100), |
143 | ! |
expand = c(0.01, 0.01) |
144 |
) + |
|
145 | ! |
scale_y_continuous( |
146 | ! |
name = ylab, |
147 | ! |
breaks = log10(c(seq(0.1, 1, 0.1), seq(2, 10, 1), seq(20, 100, 10))), |
148 | ! |
limits = c(-1, 2), |
149 | ! |
labels = c(0.1, rep(" ", 8), 1, rep(" ", 8), 10, rep(" ", 8), 100), |
150 | ! |
expand = c(0.01, 0.01) |
151 |
) + |
|
152 | ! |
labs(title = title, caption = caption) + |
153 | ! |
theme_bw(base_size = 14) + |
154 | ! |
theme( |
155 | ! |
plot.title = element_text(hjust = 0.5), |
156 | ! |
plot.title.position = "plot", |
157 | ! |
plot.caption = element_text(hjust = 0), |
158 | ! |
plot.caption.position = "plot", |
159 | ! |
legend.title = element_blank(), |
160 | ! |
panel.grid = element_blank() |
161 |
) + |
|
162 | ! |
geom_segment( |
163 | ! |
aes(x = log10(folds[1]), y = log10(0), xend = log10(folds[1]), yend = log10(75)), |
164 | ! |
linewidth = 0.25, |
165 | ! |
color = "grey" |
166 |
) + |
|
167 | ! |
geom_segment( |
168 | ! |
aes(x = log10(0), y = log10(folds[2]), xend = log10(65), yend = log10(folds[2])), |
169 | ! |
linewidth = 0.25, |
170 | ! |
color = "grey" |
171 |
) + |
|
172 | ! |
geom_segment( |
173 | ! |
aes(x = log10(1), y = log10(0), xend = log10(1), yend = log10(1)), |
174 | ! |
linewidth = 0.25, |
175 | ! |
color = "black" |
176 |
) + |
|
177 | ! |
geom_segment( |
178 | ! |
aes(x = log10(0), y = log10(1), xend = log10(1), yend = log10(1)), |
179 | ! |
linewidth = 0.25, |
180 | ! |
color = "black" |
181 |
) + |
|
182 | ! |
annotate("text", label = paste0(folds[1], "XULN"), x = log10(folds[1]), y = log10(90)) + |
183 | ! |
annotate("text", label = paste0(folds[2], "XULN"), x = log10(85), y = log10(folds[2])) + |
184 | ! |
annotate("text", label = text[1], x = log10(0.2), y = log10(0.12)) + |
185 | ! |
annotate("text", label = text[2], x = log10(0.2), y = log10(80)) + |
186 | ! |
annotate("text", label = text[3], x = log10(40), y = log10(80)) + |
187 | ! |
annotate("text", label = text[4], x = log10(40), y = log10(0.12)) + |
188 | ! |
geom_point(aes( |
189 | ! |
x = log10(.data[[term_selected[1]]]), |
190 | ! |
y = log10(.data[[term_selected[2]]]), |
191 | ! |
shape = arm, |
192 | ! |
color = arm |
193 |
)) + |
|
194 | ! |
scale_shape_manual(values = c(1:n_distinct(arm))) |
195 | ||
196 | ! |
g <- ggplotGrob(p) |
197 | ! |
grid::grid.newpage() |
198 | ! |
grid::grid.draw(g) |
199 | ! |
invisible(g) |
200 |
}
|
1 |
#' Simple spider plot
|
|
2 |
#'
|
|
3 |
#' Description of this plot
|
|
4 |
#'
|
|
5 |
#' @param anl The analysis data frame
|
|
6 |
#' @param byvar Analysis dataset
|
|
7 |
#' @param days Variable with time in days
|
|
8 |
#' @param mes_value Variable with measurement
|
|
9 |
#' @param group_col Variable to color the individual lines and id in plot
|
|
10 |
#' @param baseday Numeric Value, points with only smaller values will be cut out
|
|
11 |
#'
|
|
12 |
#'
|
|
13 |
#' @return `ggplot` object
|
|
14 |
#'
|
|
15 |
#' @export
|
|
16 |
#'
|
|
17 |
#' @author Mika Maekinen
|
|
18 |
#'
|
|
19 |
#' @examplesIf require("nestcolor")
|
|
20 |
#' library(dplyr)
|
|
21 |
#' library(nestcolor)
|
|
22 |
#'
|
|
23 |
#' ADSL <- osprey::rADSL[1:15, ]
|
|
24 |
#' ADTR <- osprey::rADTR
|
|
25 |
#' ANL <- left_join(ADSL, ADTR)
|
|
26 |
#'
|
|
27 |
#' ANL %>%
|
|
28 |
#' dplyr::filter(ANL01FL == "Y" & PARAMCD == "SLDINV") %>%
|
|
29 |
#' spiderplot_simple(group_col = "SEX", days = "ADY", mes_value = "AVAL")
|
|
30 |
spiderplot_simple <- function(anl, |
|
31 |
byvar = "USUBJID", |
|
32 |
days = "TRTDURD", |
|
33 |
mes_value = "PARAM", |
|
34 |
group_col = "USUBJID", |
|
35 |
baseday = 0) { |
|
36 |
### remove patients without post baseline measurement
|
|
37 | ! |
anl <- anl %>% |
38 | ! |
group_by(!!byvar) %>% |
39 | ! |
dplyr::mutate(morebase = ifelse(max(!!days, na.rm = TRUE) > baseday, TRUE, FALSE)) %>% |
40 | ! |
dplyr::filter(.data$morebase == TRUE) %>% |
41 | ! |
ungroup() |
42 |
### find the last measurement
|
|
43 | ! |
last_obs <- anl %>% |
44 | ! |
group_by(!!as.symbol(byvar)) %>% |
45 | ! |
slice(which.max(!!as.symbol(days))) |
46 | ||
47 |
# plotr
|
|
48 | ! |
ggplot( |
49 | ! |
data = anl, |
50 | ! |
mapping = aes(x = !!days, y = !!mes_value, group = !!byvar, colour = !!group_col), |
51 | ! |
size = 2, |
52 | ! |
alpha = 1 |
53 |
) + |
|
54 | ! |
geom_point(size = 3) + |
55 | ! |
geom_line(linewidth = 2, alpha = 0.7) + |
56 | ! |
geom_text(aes(x = !!days, y = !!mes_value, label = !!byvar), data = last_obs, hjust = 0) + |
57 | ! |
geom_hline(aes(yintercept = 0), linetype = "dotted", color = "black") + |
58 | ! |
xlab("Time (Days)") + |
59 | ! |
ylab("Change(%) from Baseline") |
60 |
}
|