| 1 |
setOldClass(c("listing_df", "tbl_df", "tbl", "data.frame"))
|
|
| 2 |
setOldClass(c("MatrixPrintForm", "list"))
|
|
| 3 | ||
| 4 |
#' Create a listing from a `data.frame` or `tibble` |
|
| 5 |
#' |
|
| 6 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 7 |
#' |
|
| 8 |
#' Create listings displaying `key_cols` and `disp_cols` to produce a compact and |
|
| 9 |
#' elegant representation of the input `data.frame` or `tibble`. |
|
| 10 |
#' |
|
| 11 |
#' @param df (`data.frame` or `listing_df`)\cr the `data.frame` to be converted to a listing or |
|
| 12 |
#' `listing_df` to be modified. |
|
| 13 |
#' @param key_cols (`character`)\cr vector of names of columns which should be treated as *key columns* |
|
| 14 |
#' when rendering the listing. Key columns allow you to group repeat occurrences. |
|
| 15 |
#' @param disp_cols (`character` or `NULL`)\cr vector of names of non-key columns which should be |
|
| 16 |
#' displayed when the listing is rendered. Defaults to all columns of `df` not named in `key_cols` or |
|
| 17 |
#' `non_disp_cols`. |
|
| 18 |
#' @param non_disp_cols (`character` or `NULL`)\cr vector of names of non-key columns to be excluded as display |
|
| 19 |
#' columns. All other non-key columns are treated as display columns. Ignored if `disp_cols` is non-`NULL`. |
|
| 20 |
#' @param sort_cols (`character` or `NULL`)\cr vector of names of columns (in order) which should be used to sort the |
|
| 21 |
#' listing. Defaults to `key_cols`. If `NULL`, no sorting will be performed. |
|
| 22 |
#' @param unique_rows (`flag`)\cr whether only unique rows should be included in the listing. Defaults to `FALSE`. |
|
| 23 |
#' @param default_formatting (`list`)\cr a named list of default column format configurations to apply when rendering |
|
| 24 |
#' the listing. Each name-value pair consists of a name corresponding to a data class (or "numeric" for all |
|
| 25 |
#' unspecified numeric classes) and a value of type `fmt_config` with the format configuration that should be |
|
| 26 |
#' implemented for columns of that class. If named element "all" is included in the list, this configuration will be |
|
| 27 |
#' used for all data classes not specified. Objects of type `fmt_config` can take 3 arguments: `format`, `na_str`, |
|
| 28 |
#' and `align`. |
|
| 29 |
#' @param col_formatting (`list`)\cr a named list of custom column formatting configurations to apply to specific |
|
| 30 |
#' columns when rendering the listing. Each name-value pair consists of a name corresponding to a column name and a |
|
| 31 |
#' value of type `fmt_config` with the formatting configuration that should be implemented for that column. Objects |
|
| 32 |
#' of type `fmt_config` can take 3 arguments: `format`, `na_str`, and `align`. Defaults to `NULL`. |
|
| 33 |
#' @param add_trailing_sep (`character` or `numeric` or `NULL`)\cr If it is assigned to one or more column names, |
|
| 34 |
#' a trailing separator will be added between groups with identical values for that column. Numeric option allows |
|
| 35 |
#' the user to specify in which rows it can be added. Defaults to `NULL`. |
|
| 36 |
#' @param trailing_sep (`character(1)`)\cr The separator to be added between groups. The character will be repeated to |
|
| 37 |
#' fill the row. |
|
| 38 |
#' @param main_title (`string` or `NULL`)\cr the main title for the listing, or `NULL` (the default). |
|
| 39 |
#' @param subtitles (`character` or `NULL`)\cr a vector of subtitles for the listing, or `NULL` (the default). |
|
| 40 |
#' @param main_footer (`character` or `NULL`)\cr a vector of main footer lines for the listing, or `NULL` (the default). |
|
| 41 |
#' @param prov_footer (`character` or `NULL`)\cr a vector of provenance footer lines for the listing, or `NULL` |
|
| 42 |
#' (the default). Each string element is placed on a new line. |
|
| 43 |
#' @param split_into_pages_by_var (`character` or `NULL`)\cr the name of a variable for on the listing should be split |
|
| 44 |
#' into pages, with each page corresponding to one unique value/level of the variable. See |
|
| 45 |
#' [split_into_pages_by_var()] for more details. |
|
| 46 |
#' @param vec (`string`)\cr name of a column vector from a `listing_df` object to be annotated as a key column. |
|
| 47 |
#' |
|
| 48 |
#' @return A `listing_df` object, sorted by its key columns. |
|
| 49 |
#' |
|
| 50 |
#' @details |
|
| 51 |
#' At its core, a `listing_df` object is a `tbl_df` object with a customized |
|
| 52 |
#' print method and support for the formatting and pagination machinery provided by |
|
| 53 |
#' the `formatters` package. |
|
| 54 |
#' |
|
| 55 |
#' `listing_df` objects have two 'special' types of columns: key columns and display columns. |
|
| 56 |
#' |
|
| 57 |
#' Key columns act as indexes, which means a number of things in practice. |
|
| 58 |
#' |
|
| 59 |
#' All key columns are also display columns. |
|
| 60 |
#' |
|
| 61 |
#' `listing_df` objects are always sorted by their set of key columns at creation time. |
|
| 62 |
#' Any `listing_df` object which is not sorted by its full set of key columns (e.g., |
|
| 63 |
#' one whose rows have been reordered explicitly during creation) is invalid and the behavior |
|
| 64 |
#' when rendering or paginating that object is undefined. |
|
| 65 |
#' |
|
| 66 |
#' Each value of a key column is printed only once per page and per unique combination of |
|
| 67 |
#' values for all higher-priority (i.e., to the left of it) key columns. Locations |
|
| 68 |
#' where a repeated value would have been printed within a key column for the same |
|
| 69 |
#' higher-priority-key combination on the same page are rendered as empty space. |
|
| 70 |
#' Note, determination of which elements to display within a key column at rendering is |
|
| 71 |
#' based on the underlying value; any non-default formatting applied to the column |
|
| 72 |
#' has no effect on this behavior. |
|
| 73 |
#' |
|
| 74 |
#' Display columns are columns which should be rendered, but are not key columns. By |
|
| 75 |
#' default this is all non-key columns in the incoming data, but in need not be. |
|
| 76 |
#' Columns in the underlying data which are neither key nor display columns remain |
|
| 77 |
#' within the object available for computations but *are not rendered during |
|
| 78 |
#' printing or export of the listing*. |
|
| 79 |
#' |
|
| 80 |
#' @examples |
|
| 81 |
#' dat <- ex_adae |
|
| 82 |
#' |
|
| 83 |
#' # This example demonstrates the listing with key_cols (values are grouped by USUBJID) and |
|
| 84 |
#' # multiple lines in prov_footer |
|
| 85 |
#' lsting <- as_listing(dat[1:25, ], |
|
| 86 |
#' key_cols = c("USUBJID", "AESOC"),
|
|
| 87 |
#' main_title = "Example Title for Listing", |
|
| 88 |
#' subtitles = "This is the subtitle for this Adverse Events Table", |
|
| 89 |
#' main_footer = "Main footer for the listing", |
|
| 90 |
#' prov_footer = c( |
|
| 91 |
#' "You can even add a subfooter", "Second element is place on a new line", |
|
| 92 |
#' "Third string" |
|
| 93 |
#' ) |
|
| 94 |
#' ) %>% |
|
| 95 |
#' add_listing_col("AETOXGR") %>%
|
|
| 96 |
#' add_listing_col("BMRKR1", format = "xx.x") %>%
|
|
| 97 |
#' add_listing_col("AESER / AREL", fun = function(df) paste(df$AESER, df$AREL, sep = " / "))
|
|
| 98 |
#' |
|
| 99 |
#' mat <- matrix_form(lsting) |
|
| 100 |
#' |
|
| 101 |
#' cat(toString(mat)) |
|
| 102 |
#' |
|
| 103 |
#' # This example demonstrates the listing table without key_cols |
|
| 104 |
#' # and specifying the cols with disp_cols. |
|
| 105 |
#' dat <- ex_adae |
|
| 106 |
#' lsting <- as_listing(dat[1:25, ], |
|
| 107 |
#' disp_cols = c("USUBJID", "AESOC", "RACE", "AETOXGR", "BMRKR1")
|
|
| 108 |
#' ) |
|
| 109 |
#' |
|
| 110 |
#' mat <- matrix_form(lsting) |
|
| 111 |
#' |
|
| 112 |
#' cat(toString(mat)) |
|
| 113 |
#' |
|
| 114 |
#' # This example demonstrates a listing with format configurations specified |
|
| 115 |
#' # via the default_formatting and col_formatting arguments |
|
| 116 |
#' dat <- ex_adae |
|
| 117 |
#' dat$AENDY[3:6] <- NA |
|
| 118 |
#' lsting <- as_listing(dat[1:25, ], |
|
| 119 |
#' key_cols = c("USUBJID", "AESOC"),
|
|
| 120 |
#' disp_cols = c("STUDYID", "SEX", "ASEQ", "RANDDT", "ASTDY", "AENDY"),
|
|
| 121 |
#' default_formatting = list( |
|
| 122 |
#' all = fmt_config(align = "left"), |
|
| 123 |
#' numeric = fmt_config( |
|
| 124 |
#' format = "xx.xx", |
|
| 125 |
#' na_str = "<No data>", |
|
| 126 |
#' align = "right" |
|
| 127 |
#' ) |
|
| 128 |
#' ) |
|
| 129 |
#' ) %>% |
|
| 130 |
#' add_listing_col("BMRKR1", format = "xx.x", align = "center")
|
|
| 131 |
#' |
|
| 132 |
#' mat <- matrix_form(lsting) |
|
| 133 |
#' |
|
| 134 |
#' cat(toString(mat)) |
|
| 135 |
#' |
|
| 136 |
#' @export |
|
| 137 |
#' @rdname listings |
|
| 138 |
as_listing <- function(df, |
|
| 139 |
key_cols = names(df)[1], |
|
| 140 |
disp_cols = NULL, |
|
| 141 |
non_disp_cols = NULL, |
|
| 142 |
sort_cols = key_cols, |
|
| 143 |
unique_rows = FALSE, |
|
| 144 |
default_formatting = list(all = fmt_config()), |
|
| 145 |
col_formatting = NULL, |
|
| 146 |
add_trailing_sep = NULL, |
|
| 147 |
trailing_sep = " ", |
|
| 148 |
main_title = NULL, |
|
| 149 |
subtitles = NULL, |
|
| 150 |
main_footer = NULL, |
|
| 151 |
prov_footer = NULL, |
|
| 152 |
split_into_pages_by_var = NULL) {
|
|
| 153 | 80x |
checkmate::assert_multi_class(add_trailing_sep, c("character", "numeric"), null.ok = TRUE)
|
| 154 | 80x |
checkmate::assert_string(trailing_sep, n.chars = 1) |
| 155 | ||
| 156 | 79x |
if (length(non_disp_cols) > 0 && length(intersect(key_cols, non_disp_cols)) > 0) {
|
| 157 | 1x |
stop( |
| 158 | 1x |
"Key column also listed in non_disp_cols. All key columns are by", |
| 159 | 1x |
" definition display columns." |
| 160 |
) |
|
| 161 |
} |
|
| 162 | 78x |
if (!is.null(disp_cols) && !is.null(non_disp_cols)) {
|
| 163 | 1x |
stop("Got non-null values for both disp_cols and non_disp_cols. This is not supported.")
|
| 164 | 77x |
} else if (is.null(disp_cols)) {
|
| 165 |
## non_disp_cols NULL is ok here |
|
| 166 | 32x |
cols <- setdiff(names(df), c(key_cols, non_disp_cols)) |
| 167 |
} else {
|
|
| 168 |
## disp_cols non-null, non_disp_cols NULL |
|
| 169 | 45x |
cols <- disp_cols |
| 170 |
} |
|
| 171 | 77x |
if (!all(sapply(default_formatting, is, class2 = "fmt_config"))) {
|
| 172 | 1x |
stop( |
| 173 | 1x |
"All format configurations supplied in `default_formatting`", |
| 174 | 1x |
" must be of type `fmt_config`." |
| 175 |
) |
|
| 176 |
} |
|
| 177 | 76x |
if (!(is.null(col_formatting) || all(sapply(col_formatting, is, class2 = "fmt_config")))) {
|
| 178 | 2x |
stop( |
| 179 | 2x |
"All format configurations supplied in `col_formatting`", |
| 180 | 2x |
" must be of type `fmt_config`." |
| 181 |
) |
|
| 182 |
} |
|
| 183 | ||
| 184 | 74x |
if (any(sapply(df, inherits, "difftime"))) {
|
| 185 | 1x |
stop("One or more variables in the dataframe have class 'difftime'. Please convert to factor or character.")
|
| 186 |
} |
|
| 187 | ||
| 188 | 73x |
df <- as_tibble(df) |
| 189 | 73x |
varlabs <- var_labels(df, fill = TRUE) |
| 190 | 73x |
if (!is.null(sort_cols)) {
|
| 191 | 71x |
sort_miss <- setdiff(sort_cols, names(df)) |
| 192 | 71x |
if (length(sort_miss) > 0) {
|
| 193 | 1x |
stop( |
| 194 | 1x |
"The following columns were specified as sorting columns (sort_cols) but are missing from df: ", |
| 195 | 1x |
paste0("`", sort_miss, "`", collapse = ", ")
|
| 196 |
) |
|
| 197 |
} |
|
| 198 | 70x |
o <- do.call(order, df[sort_cols]) |
| 199 | 70x |
if (is.unsorted(o)) {
|
| 200 | 38x |
if (interactive()) {
|
| 201 | ! |
message(paste( |
| 202 | ! |
"sorting incoming data by", |
| 203 | ! |
if (identical(sort_cols, key_cols)) {
|
| 204 | ! |
"key columns" |
| 205 |
} else {
|
|
| 206 | ! |
paste0("column", if (length(sort_cols) > 1) "s", " ", paste0("`", sort_cols, "`", collapse = ", "))
|
| 207 |
} |
|
| 208 |
)) |
|
| 209 |
} |
|
| 210 | 38x |
df <- df[o, ] |
| 211 |
} |
|
| 212 |
} |
|
| 213 | ||
| 214 |
## reorder the full set of cols to ensure key columns are first |
|
| 215 | 72x |
ordercols <- c(key_cols, setdiff(names(df), key_cols)) |
| 216 | 72x |
df <- df[, ordercols] |
| 217 | 72x |
var_labels(df) <- varlabs[ordercols] |
| 218 | ||
| 219 | 72x |
for (cnm in key_cols) {
|
| 220 | 110x |
df[[cnm]] <- as_keycol(df[[cnm]]) |
| 221 |
} |
|
| 222 | ||
| 223 |
## key cols must be leftmost cols |
|
| 224 | 72x |
cols <- c(key_cols, setdiff(cols, key_cols)) |
| 225 | ||
| 226 | 72x |
row_all_na <- apply(df[cols], 1, function(x) all(is.na(x))) |
| 227 | 72x |
if (any(row_all_na)) {
|
| 228 | 1x |
warning("rows that only contain NA values have been trimmed")
|
| 229 | 1x |
df <- df[!row_all_na, ] |
| 230 |
} |
|
| 231 | ||
| 232 |
# set col format configs |
|
| 233 | 72x |
df[cols] <- lapply(cols, function(col) {
|
| 234 | 427x |
col_class <- tail(class(df[[col]]), 1) |
| 235 | 427x |
col_fmt_class <- if (!col_class %in% names(default_formatting) && is.numeric(df[[col]])) "numeric" else col_class |
| 236 | 427x |
col_fmt <- if (col %in% names(col_formatting)) {
|
| 237 | 7x |
col_formatting[[col]] |
| 238 | 427x |
} else if (col_fmt_class %in% names(default_formatting)) {
|
| 239 | 6x |
default_formatting[[col_fmt_class]] |
| 240 |
} else {
|
|
| 241 | 414x |
if (!"all" %in% names(default_formatting)) {
|
| 242 | 1x |
stop( |
| 243 | 1x |
"Format configurations must be supplied for all listing columns. ", |
| 244 | 1x |
"To cover all remaining columns please add an 'all' configuration", |
| 245 | 1x |
" to `default_formatting`." |
| 246 |
) |
|
| 247 |
} |
|
| 248 | 413x |
default_formatting[["all"]] |
| 249 |
} |
|
| 250 |
# ANY attr <- fmt_config slot |
|
| 251 | 426x |
obj_format(df[[col]]) <- obj_format(col_fmt) |
| 252 | 426x |
obj_na_str(df[[col]]) <- if (is.null(obj_na_str(col_fmt))) "NA" else obj_na_str(col_fmt) |
| 253 | 426x |
obj_align(df[[col]]) <- if (is.null(obj_align(col_fmt))) "left" else obj_align(col_fmt) |
| 254 | 426x |
df[[col]] |
| 255 |
}) |
|
| 256 | ||
| 257 | 3x |
if (unique_rows) df <- df[!duplicated(df[, cols]), ] |
| 258 | ||
| 259 | 71x |
class(df) <- c("listing_df", class(df))
|
| 260 | ||
| 261 |
## these all work even when the value is NULL |
|
| 262 | 71x |
main_title(df) <- main_title |
| 263 | 71x |
main_footer(df) <- main_footer |
| 264 | 71x |
subtitles(df) <- subtitles |
| 265 | 71x |
prov_footer(df) <- prov_footer |
| 266 | 71x |
listing_dispcols(df) <- cols |
| 267 | ||
| 268 | 71x |
if (!is.null(split_into_pages_by_var)) {
|
| 269 | 2x |
df <- split_into_pages_by_var(df, split_into_pages_by_var) |
| 270 |
} |
|
| 271 | ||
| 272 |
# add trailing separators to the df object |
|
| 273 | 71x |
if (!is.null(add_trailing_sep)) {
|
| 274 | 8x |
if (class(df)[1] == "list") {
|
| 275 | 1x |
df <- lapply( |
| 276 | 1x |
df, .do_add_trailing_sep, |
| 277 | 1x |
add_trailing_sep = add_trailing_sep, |
| 278 | 1x |
trailing_sep = trailing_sep |
| 279 |
) |
|
| 280 |
} else {
|
|
| 281 | 7x |
df <- .do_add_trailing_sep(df, add_trailing_sep, trailing_sep) |
| 282 |
} |
|
| 283 |
} |
|
| 284 | ||
| 285 | 69x |
df |
| 286 |
} |
|
| 287 | ||
| 288 |
# Helper function to add trailing separators to the dataframe |
|
| 289 |
.do_add_trailing_sep <- function(df_tmp, add_trailing_sep, trailing_sep) {
|
|
| 290 | 11x |
if (is.character(add_trailing_sep)) {
|
| 291 | 8x |
if (!all(add_trailing_sep %in% names(df_tmp))) {
|
| 292 | 1x |
stop( |
| 293 | 1x |
"The column specified in `add_trailing_sep` does not exist in the dataframe." |
| 294 |
) |
|
| 295 |
} |
|
| 296 | 7x |
row_ind_for_trail_sep <- apply( |
| 297 | 7x |
apply(as.data.frame(df_tmp)[, add_trailing_sep, drop = FALSE], 2, function(col_i) {
|
| 298 | 8x |
diff(as.numeric(as.factor(col_i))) |
| 299 |
}), |
|
| 300 | 7x |
1, function(row_i) any(row_i != 0) |
| 301 |
) %>% |
|
| 302 | 7x |
which() |
| 303 | 7x |
listing_trailing_sep(df_tmp) <- list( |
| 304 | 7x |
"var_trailing_sep" = add_trailing_sep, |
| 305 | 7x |
"where_trailing_sep" = row_ind_for_trail_sep, |
| 306 | 7x |
"what_to_separe" = trailing_sep |
| 307 |
) |
|
| 308 | 3x |
} else if (is.numeric(add_trailing_sep)) {
|
| 309 | 3x |
if (any(!add_trailing_sep %in% seq_len(nrow(df_tmp)))) {
|
| 310 | 1x |
stop( |
| 311 | 1x |
"The row indices specified in `add_trailing_sep` are not valid." |
| 312 |
) |
|
| 313 |
} |
|
| 314 | 2x |
listing_trailing_sep(df_tmp) <- list( |
| 315 | 2x |
"var_trailing_sep" = NULL, # If numeric only |
| 316 | 2x |
"where_trailing_sep" = add_trailing_sep, |
| 317 | 2x |
"what_to_separe" = trailing_sep |
| 318 |
) |
|
| 319 |
} |
|
| 320 | ||
| 321 | 9x |
df_tmp |
| 322 |
} |
|
| 323 | ||
| 324 | ||
| 325 |
#' @export |
|
| 326 |
#' @rdname listings |
|
| 327 |
as_keycol <- function(vec) {
|
|
| 328 | 110x |
if (is.factor(vec)) {
|
| 329 | 23x |
lab <- obj_label(vec) |
| 330 | 23x |
vec <- as.character(vec) |
| 331 | 23x |
obj_label(vec) <- lab |
| 332 |
} |
|
| 333 | 110x |
class(vec) <- c("listing_keycol", class(vec))
|
| 334 | 110x |
vec |
| 335 |
} |
|
| 336 | ||
| 337 |
#' @export |
|
| 338 |
#' @rdname listings |
|
| 339 |
is_keycol <- function(vec) {
|
|
| 340 | 8435x |
inherits(vec, "listing_keycol") |
| 341 |
} |
|
| 342 | ||
| 343 |
#' @export |
|
| 344 |
#' @rdname listings |
|
| 345 |
get_keycols <- function(df) {
|
|
| 346 | 482x |
names(which(sapply(df, is_keycol))) |
| 347 |
} |
|
| 348 | ||
| 349 |
#' @inherit formatters::matrix_form |
|
| 350 |
#' @param indent_rownames (`flag`)\cr silently ignored, as listings do not have row names |
|
| 351 |
#' nor indenting structure. |
|
| 352 |
#' @param expand_newlines (`flag`)\cr this should always be `TRUE` for listings. We keep it |
|
| 353 |
#' for debugging reasons. |
|
| 354 |
#' |
|
| 355 |
#' @return a [formatters::MatrixPrintForm] object. |
|
| 356 |
#' |
|
| 357 |
#' @seealso [formatters::matrix_form()] |
|
| 358 |
#' |
|
| 359 |
#' @examples |
|
| 360 |
#' lsting <- as_listing(mtcars) |
|
| 361 |
#' mf <- matrix_form(lsting) |
|
| 362 |
#' |
|
| 363 |
#' @export |
|
| 364 |
setMethod( |
|
| 365 |
"matrix_form", "listing_df", |
|
| 366 |
rix_form <- function(obj, |
|
| 367 |
indent_rownames = FALSE, |
|
| 368 |
expand_newlines = TRUE, |
|
| 369 |
fontspec = font_spec, |
|
| 370 |
col_gap = 3L, |
|
| 371 |
round_type = c("iec", "sas")) {
|
|
| 372 |
## we intentionally silently ignore indent_rownames because listings have |
|
| 373 |
## no rownames, but formatters::vert_pag_indices calls matrix_form(obj, TRUE) |
|
| 374 |
## unconditionally. |
|
| 375 | 190x |
cols <- attr(obj, "listing_dispcols") |
| 376 | 190x |
listing <- obj[, cols] |
| 377 | 190x |
atts <- attributes(obj) |
| 378 | 190x |
atts$names <- cols |
| 379 | 190x |
attributes(listing) <- atts |
| 380 | 190x |
keycols <- get_keycols(listing) |
| 381 | ||
| 382 | 190x |
bodymat <- matrix("",
|
| 383 | 190x |
nrow = nrow(listing), |
| 384 | 190x |
ncol = ncol(listing) |
| 385 |
) |
|
| 386 | ||
| 387 | 190x |
colnames(bodymat) <- names(listing) |
| 388 | ||
| 389 | 190x |
curkey <- "" |
| 390 | 190x |
for (i in seq_along(keycols)) {
|
| 391 | 281x |
kcol <- keycols[i] |
| 392 | 281x |
kcolvec <- listing[[kcol]] |
| 393 | 281x |
kcolvec <- vapply(kcolvec, format_value, "", |
| 394 | 281x |
format = obj_format(kcolvec), |
| 395 | 281x |
na_str = obj_na_str(kcolvec), |
| 396 | 281x |
round_type = round_type |
| 397 |
) |
|
| 398 | 281x |
curkey <- paste0(curkey, kcolvec) |
| 399 | 281x |
disp <- c(TRUE, tail(curkey, -1) != head(curkey, -1)) |
| 400 | 281x |
bodymat[disp, kcol] <- kcolvec[disp] |
| 401 |
} |
|
| 402 | ||
| 403 | 190x |
nonkeycols <- setdiff(names(listing), keycols) |
| 404 | 190x |
if (length(nonkeycols) > 0) {
|
| 405 | 182x |
for (nonk in nonkeycols) {
|
| 406 | 483x |
vec <- listing[[nonk]] |
| 407 | 483x |
vec <- vapply(vec, format_value, "", |
| 408 | 483x |
format = obj_format(vec), |
| 409 | 483x |
na_str = obj_na_str(vec), |
| 410 | 483x |
round_type = round_type |
| 411 |
) |
|
| 412 | 483x |
bodymat[, nonk] <- vec |
| 413 |
} |
|
| 414 |
} |
|
| 415 | ||
| 416 | 190x |
fullmat <- rbind( |
| 417 | 190x |
var_labels(listing, fill = TRUE), |
| 418 | 190x |
bodymat |
| 419 |
) |
|
| 420 | ||
| 421 | 190x |
colaligns <- rbind( |
| 422 | 190x |
rep("center", length(cols)),
|
| 423 | 190x |
matrix(sapply(listing, obj_align), |
| 424 | 190x |
ncol = length(cols), |
| 425 | 190x |
nrow = nrow(fullmat) - 1, |
| 426 | 190x |
byrow = TRUE |
| 427 |
) |
|
| 428 |
) |
|
| 429 | ||
| 430 | 190x |
if (any(grepl("([{}])", fullmat))) {
|
| 431 | 1x |
stop( |
| 432 | 1x |
"Labels cannot contain { or } due to their use for indicating referential footnotes.\n",
|
| 433 | 1x |
"These are not supported at the moment in {rlistings}."
|
| 434 |
) |
|
| 435 |
} |
|
| 436 | ||
| 437 |
# trailing sep setting |
|
| 438 | 189x |
row_info <- make_row_df(obj, fontspec = fontspec) |
| 439 | 189x |
if (!is.null(listing_trailing_sep(obj))) {
|
| 440 | 5x |
lts <- listing_trailing_sep(obj) |
| 441 | ||
| 442 |
# We need to make sure that the trailing separator is not beyond the number of rows (cases like head()) |
|
| 443 | 5x |
lts$where_trailing_sep <- lts$where_trailing_sep[lts$where_trailing_sep <= nrow(row_info)] |
| 444 | 5x |
row_info$trailing_sep[lts$where_trailing_sep] <- lts$what_to_separe |
| 445 |
} |
|
| 446 | ||
| 447 | 189x |
MatrixPrintForm( |
| 448 | 189x |
strings = fullmat, |
| 449 | 189x |
spans = matrix(1, |
| 450 | 189x |
nrow = nrow(fullmat), |
| 451 | 189x |
ncol = ncol(fullmat) |
| 452 |
), |
|
| 453 | 189x |
ref_fnotes = list(), |
| 454 | 189x |
aligns = colaligns, |
| 455 | 189x |
formats = matrix(1, |
| 456 | 189x |
nrow = nrow(fullmat), |
| 457 | 189x |
ncol = ncol(fullmat) |
| 458 |
), |
|
| 459 | 189x |
listing_keycols = keycols, # It is always something |
| 460 | 189x |
row_info = row_info, |
| 461 | 189x |
nlines_header = 1, # We allow only one level of headers and nl expansion happens after |
| 462 | 189x |
nrow_header = 1, |
| 463 | 189x |
has_topleft = FALSE, |
| 464 | 189x |
has_rowlabs = FALSE, |
| 465 | 189x |
expand_newlines = expand_newlines, |
| 466 | 189x |
main_title = main_title(obj), |
| 467 | 189x |
subtitles = subtitles(obj), |
| 468 | 189x |
page_titles = page_titles(obj), |
| 469 | 189x |
main_footer = main_footer(obj), |
| 470 | 189x |
prov_footer = prov_footer(obj), |
| 471 | 189x |
col_gap = col_gap, |
| 472 | 189x |
fontspec = fontspec, |
| 473 | 189x |
rep_cols = length(keycols) |
| 474 |
) |
|
| 475 |
} |
|
| 476 |
) |
|
| 477 | ||
| 478 |
#' @export |
|
| 479 |
#' @rdname listings |
|
| 480 | 241x |
listing_dispcols <- function(df) attr(df, "listing_dispcols") %||% character() |
| 481 | ||
| 482 |
#' @param new (`character`)\cr vector of names of columns to be added to |
|
| 483 |
#' the set of display columns. |
|
| 484 |
#' |
|
| 485 |
#' @export |
|
| 486 |
#' @rdname listings |
|
| 487 |
add_listing_dispcol <- function(df, new) {
|
|
| 488 | 25x |
listing_dispcols(df) <- c(listing_dispcols(df), new) |
| 489 | 25x |
df |
| 490 |
} |
|
| 491 | ||
| 492 |
#' @param value (`string`)\cr new value. |
|
| 493 |
#' |
|
| 494 |
#' @export |
|
| 495 |
#' @rdname listings |
|
| 496 |
`listing_dispcols<-` <- function(df, value) {
|
|
| 497 | 96x |
if (!is.character(value)) {
|
| 498 | ! |
stop( |
| 499 | ! |
"dispcols must be a character vector of column names, got ", |
| 500 | ! |
"object of class: ", paste(class(value), collapse = ",") |
| 501 |
) |
|
| 502 |
} |
|
| 503 | 96x |
chk <- setdiff(value, names(df)) ## remember setdiff is not symmetrical |
| 504 | 96x |
if (length(chk) > 0) {
|
| 505 | ! |
stop( |
| 506 | ! |
"listing display columns must be columns in the underlying data. ", |
| 507 | ! |
"Column(s) ", paste(chk, collapse = ", "), " not present in the data." |
| 508 |
) |
|
| 509 |
} |
|
| 510 | 96x |
attr(df, "listing_dispcols") <- unique(value) |
| 511 | 96x |
df |
| 512 |
} |
|
| 513 |
#' @keywords internal |
|
| 514 | 206x |
listing_trailing_sep <- function(df) attr(df, "listing_trailing_sep") %||% NULL |
| 515 | ||
| 516 |
# xxx @param value (`list`)\cr List of names or rows to be separated and their separator. |
|
| 517 |
#' |
|
| 518 |
#' @keywords internal |
|
| 519 |
`listing_trailing_sep<-` <- function(df, value) {
|
|
| 520 | 11x |
checkmate::assert_list(value, len = 3, null.ok = TRUE) |
| 521 | 11x |
if (is.null(value)) {
|
| 522 | ! |
attr(df, "listing_trailing_sep") <- NULL |
| 523 | ! |
return(df) |
| 524 |
} |
|
| 525 | 11x |
checkmate::assert_set_equal( |
| 526 | 11x |
names(value), |
| 527 | 11x |
c("var_trailing_sep", "where_trailing_sep", "what_to_separe")
|
| 528 |
) |
|
| 529 | 11x |
attr(df, "listing_trailing_sep") <- value |
| 530 | 11x |
df |
| 531 |
} |
|
| 532 | ||
| 533 |
#' @inheritParams formatters::fmt_config |
|
| 534 |
#' @param name (`string`)\cr name of the existing or new column to be |
|
| 535 |
#' displayed when the listing is rendered. |
|
| 536 |
#' @param fun (`function` or `NULL`)\cr a function which accepts `df` and |
|
| 537 |
#' returns the vector for a new column, which is added to `df` as |
|
| 538 |
#' `name`, or `NULL` if marking an existing column as a listing column. |
|
| 539 |
#' |
|
| 540 |
#' @return `df` with `name` created (if necessary) and marked for |
|
| 541 |
#' display during rendering. |
|
| 542 |
#' |
|
| 543 |
#' @export |
|
| 544 |
#' @rdname listings |
|
| 545 |
add_listing_col <- function(df, |
|
| 546 |
name, |
|
| 547 |
fun = NULL, |
|
| 548 |
format = NULL, |
|
| 549 |
na_str = "NA", |
|
| 550 |
align = "left") {
|
|
| 551 | 26x |
if (class(df)[1] == "list") {
|
| 552 | 1x |
out <- lapply( |
| 553 | 1x |
df, add_listing_col, |
| 554 | 1x |
name = name, fun = fun, format = format, na_str = na_str, align = align |
| 555 |
) |
|
| 556 | 1x |
return(out) |
| 557 |
} |
|
| 558 | ||
| 559 | 25x |
if (!is.null(fun)) {
|
| 560 | 1x |
vec <- with_label(fun(df), name) |
| 561 | 24x |
} else if (name %in% names(df)) {
|
| 562 | 24x |
vec <- df[[name]] |
| 563 |
} else {
|
|
| 564 | ! |
stop( |
| 565 | ! |
"Column '", name, "' not found. name argument must specify an existing column when ", |
| 566 | ! |
"no generating function (fun argument) is specified." |
| 567 |
) |
|
| 568 |
} |
|
| 569 | ||
| 570 | 25x |
if (!is.null(format)) {
|
| 571 | 14x |
obj_format(vec) <- format |
| 572 |
} |
|
| 573 | ||
| 574 | 25x |
obj_na_str(vec) <- na_str |
| 575 | 25x |
obj_align(vec) <- align |
| 576 | ||
| 577 |
## this works for both new and existing columns |
|
| 578 | 25x |
df[[name]] <- vec |
| 579 | 25x |
df <- add_listing_dispcol(df, name) |
| 580 | 25x |
df |
| 581 |
} |
|
| 582 | ||
| 583 |
#' Split Listing by Values of a Variable |
|
| 584 |
#' |
|
| 585 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 586 |
#' |
|
| 587 |
#' Split is performed based on unique values of the given parameter present in the listing. |
|
| 588 |
#' Each listing can only be split by variable once. If this function is applied prior to |
|
| 589 |
#' pagination, parameter values will be separated by page. |
|
| 590 |
#' |
|
| 591 |
#' @param lsting (`listing_df`)\cr the listing to split. |
|
| 592 |
#' @param var (`string`)\cr name of the variable to split on. If the column is a factor, |
|
| 593 |
#' the resulting list follows the order of the levels. |
|
| 594 |
#' @param page_prefix (`string`)\cr prefix to be appended with the split value (`var` level), |
|
| 595 |
#' at the end of the subtitles, corresponding to each resulting list element (listing). |
|
| 596 |
#' |
|
| 597 |
#' @return A list of `lsting_df` objects each corresponding to a unique value of `var`. |
|
| 598 |
#' |
|
| 599 |
#' @note This function should only be used after the complete listing has been created. The |
|
| 600 |
#' listing cannot be modified further after applying this function. |
|
| 601 |
#' |
|
| 602 |
#' @examples |
|
| 603 |
#' dat <- ex_adae[1:20, ] |
|
| 604 |
#' |
|
| 605 |
#' lsting <- as_listing( |
|
| 606 |
#' dat, |
|
| 607 |
#' key_cols = c("USUBJID", "AGE"),
|
|
| 608 |
#' disp_cols = "SEX", |
|
| 609 |
#' main_title = "title", |
|
| 610 |
#' main_footer = "footer" |
|
| 611 |
#' ) %>% |
|
| 612 |
#' add_listing_col("BMRKR1", format = "xx.x") %>%
|
|
| 613 |
#' split_into_pages_by_var("SEX")
|
|
| 614 |
#' |
|
| 615 |
#' lsting |
|
| 616 |
#' |
|
| 617 |
#' @export |
|
| 618 |
split_into_pages_by_var <- function(lsting, var, page_prefix = var) {
|
|
| 619 | 9x |
checkmate::assert_class(lsting, "listing_df") |
| 620 | 8x |
checkmate::assert_choice(var, names(lsting)) |
| 621 | ||
| 622 |
# Pre-processing in case of factor variable |
|
| 623 | 8x |
levels_or_vals <- if (is.factor(lsting[[var]])) {
|
| 624 | 8x |
lvls <- levels(lsting[[var]]) |
| 625 | 8x |
lvls[lvls %in% unique(lsting[[var]])] # Filter out missing values |
| 626 |
} else {
|
|
| 627 | ! |
unique(lsting[[var]]) |
| 628 |
} |
|
| 629 | ||
| 630 |
# Main list creator (filters rows by var) |
|
| 631 | 8x |
lsting_by_var <- list() |
| 632 | 8x |
for (lvl in levels_or_vals) {
|
| 633 | 16x |
var_desc <- paste0(page_prefix, ": ", lvl) |
| 634 | 16x |
lsting_by_var[[lvl]] <- lsting[lsting[[var]] == lvl, ] |
| 635 | 16x |
subtitles(lsting_by_var[[lvl]]) <- c(subtitles(lsting), var_desc) |
| 636 |
} |
|
| 637 | ||
| 638 |
# Correction for cases with trailing separators |
|
| 639 | 8x |
if (!is.null(listing_trailing_sep(lsting))) {
|
| 640 | 2x |
trailing_sep_directives <- listing_trailing_sep(lsting) |
| 641 | 2x |
if (is.null(trailing_sep_directives$var_trailing_sep)) {
|
| 642 | 1x |
stop( |
| 643 | 1x |
"Current lsting did have add_trailing_sep directives with numeric indexes. ", |
| 644 | 1x |
"This is not supported for split_into_pages_by_var. Please use the <var> method." |
| 645 |
) |
|
| 646 |
} |
|
| 647 | 1x |
add_trailing_sep <- trailing_sep_directives$var_trailing_sep |
| 648 | 1x |
trailing_sep <- trailing_sep_directives$trailing_sep |
| 649 | 1x |
lsting_by_var <- lapply(lsting_by_var, .do_add_trailing_sep, add_trailing_sep, trailing_sep) |
| 650 |
} |
|
| 651 | ||
| 652 | 7x |
lsting_by_var |
| 653 |
} |
| 1 |
## XXX this historically has been 1, but it actually should be 1.2!!!!! |
|
| 2 |
dflt_courier <- font_spec("Courier", 9, 1)
|
|
| 3 | ||
| 4 |
#' Methods for `listing_df` objects |
|
| 5 |
#' |
|
| 6 |
#' See core documentation in [formatters::formatters-package] for descriptions of these functions. |
|
| 7 |
#' |
|
| 8 |
#' @inheritParams formatters::toString |
|
| 9 |
#' @param x (`listing_df`)\cr the listing. |
|
| 10 |
#' @param ... additional parameters passed to [formatters::toString()]. |
|
| 11 |
#' |
|
| 12 |
#' @method print listing_df |
|
| 13 |
#' |
|
| 14 |
#' @export |
|
| 15 |
#' @name listing_methods |
|
| 16 |
print.listing_df <- function(x, |
|
| 17 |
widths = NULL, |
|
| 18 |
tf_wrap = FALSE, |
|
| 19 |
max_width = NULL, |
|
| 20 |
fontspec = NULL, |
|
| 21 |
col_gap = 3L, |
|
| 22 |
round_type = c("iec", "sas"),
|
|
| 23 |
...) {
|
|
| 24 | 4x |
tryCatch({
|
| 25 | 4x |
cat( |
| 26 | 4x |
toString( |
| 27 | 4x |
matrix_form(x, fontspec = fontspec, col_gap = col_gap), |
| 28 | 4x |
widths = widths, |
| 29 | 4x |
tf_wrap = tf_wrap, |
| 30 | 4x |
max_width = max_width, |
| 31 | 4x |
fontspec = fontspec, |
| 32 | 4x |
col_gap = col_gap, |
| 33 | 4x |
round_type = round_type, |
| 34 |
... |
|
| 35 |
) |
|
| 36 |
) |
|
| 37 | 4x |
}, error = function(e) {
|
| 38 | ! |
if (nrow(x) == 0) {
|
| 39 | ! |
print("No observation in the listing object.")
|
| 40 |
} else {
|
|
| 41 | ! |
stop(e) |
| 42 |
} |
|
| 43 |
}) |
|
| 44 | 4x |
invisible(x) |
| 45 |
} |
|
| 46 | ||
| 47 |
#' @exportMethod toString |
|
| 48 |
#' @name listing_methods |
|
| 49 |
#' @aliases toString,listing_df-method |
|
| 50 |
setMethod("toString", "listing_df", function(x,
|
|
| 51 |
widths = NULL, |
|
| 52 |
fontspec = NULL, |
|
| 53 |
col_gap = 3L, |
|
| 54 |
round_type = c("iec", "sas"),
|
|
| 55 |
...) {
|
|
| 56 | 9x |
toString( |
| 57 | 9x |
matrix_form(x, fontspec = fontspec, col_gap = col_gap, round_type = round_type), |
| 58 | 9x |
fontspec = fontspec, |
| 59 | 9x |
col_gap = col_gap, |
| 60 | 9x |
widths = widths, |
| 61 | 9x |
round_type = round_type, |
| 62 |
... |
|
| 63 |
) |
|
| 64 |
}) |
|
| 65 | ||
| 66 |
## because rle in base is too much of a stickler for being atomic |
|
| 67 |
basic_run_lens <- function(x) {
|
|
| 68 | 189x |
n <- length(x) |
| 69 | 189x |
if (n == 0) {
|
| 70 | ! |
return(integer()) |
| 71 |
} |
|
| 72 | ||
| 73 | 189x |
y <- x[-1L] != x[-n] |
| 74 | 189x |
i <- c(which(y), n) |
| 75 | 189x |
diff(c(0L, i)) |
| 76 |
} |
|
| 77 | ||
| 78 |
#' @param df (`listing_df`)\cr the listing. |
|
| 79 |
#' @param colnm (`string`)\cr column name. |
|
| 80 |
#' @param colvec (`vector`)\cr column values based on `colnm`. |
|
| 81 |
#' |
|
| 82 |
#' @rdname vec_nlines |
|
| 83 |
#' @keywords internal |
|
| 84 |
format_colvector <- function(df, colnm, colvec = df[[colnm]], round_type = c("iec", "sas")) {
|
|
| 85 | 763x |
if (missing(colvec) && !(colnm %in% names(df))) {
|
| 86 | ! |
stop("column ", colnm, " not found")
|
| 87 |
} |
|
| 88 | 763x |
na_str <- obj_na_str(colvec) |
| 89 | 763x |
if (is.null(na_str) || all(is.na(na_str))) {
|
| 90 | ! |
na_str <- rep("-", max(1L, length(na_str)))
|
| 91 |
} |
|
| 92 | ||
| 93 | 763x |
strvec <- vapply(colvec, format_value, "", format = obj_format(colvec), na_str = na_str, round_type = round_type) |
| 94 | 763x |
strvec |
| 95 |
} |
|
| 96 | ||
| 97 |
#' Utilities for formatting a listing column |
|
| 98 |
#' |
|
| 99 |
#' For `vec_nlines`, calculate the number of lines each element of a column vector will |
|
| 100 |
#' take to render. For `format_colvector`, |
|
| 101 |
#' |
|
| 102 |
#' @param vec (`vector`)\cr a column vector to be rendered into ASCII. |
|
| 103 |
#' @param max_width (`numeric(1)` or `NULL`)\cr the width to render the column with. |
|
| 104 |
#' @return (`numeric`)\cr a vector of the number of lines element-wise that will be |
|
| 105 |
#' needed to render the elements of `vec` to width `max_width`. |
|
| 106 |
#' |
|
| 107 |
#' @keywords internal |
|
| 108 |
setGeneric("vec_nlines", function(vec, max_width = NULL, fontspec = dflt_courier, round_type = c("iec", "sas")) {
|
|
| 109 | 763x |
standardGeneric("vec_nlines")
|
| 110 |
}) |
|
| 111 | ||
| 112 |
#' @param vec (`vector`)\cr a vector. |
|
| 113 |
#' |
|
| 114 |
#' @rdname vec_nlines |
|
| 115 |
#' @keywords internal |
|
| 116 |
setMethod("vec_nlines", "ANY", function(vec, max_width = NULL, fontspec = dflt_courier, round_type = c("iec", "sas")) {
|
|
| 117 | 763x |
round_type <- match.arg(round_type) |
| 118 | 763x |
if (is.null(max_width)) {
|
| 119 | 763x |
max_width <- floor(0.9 * getOption("width")) # default of base::strwrap
|
| 120 |
# NB: flooring as it is used as <= (also in base::strwrap) |
|
| 121 |
} |
|
| 122 |
# in formatters for characters |
|
| 123 | 763x |
unlist(lapply(format_colvector(colvec = vec, round_type = round_type), nlines, |
| 124 | 763x |
max_width = max_width, fontspec = fontspec)) |
| 125 |
}) |
|
| 126 | ||
| 127 |
## setMethod("vec_nlines", "character", function(vec, max_width = NULL) {
|
|
| 128 |
## strvec <- wrap_txt(format_colvector(colvec = vec), width = max_width, collapse = "\n") |
|
| 129 |
## mtchs <- gregexpr("\n", strvec, fixed = TRUE)
|
|
| 130 |
## 1L + vapply(mtchs, function(vi) sum(vi > 0), 1L) |
|
| 131 |
## }) |
|
| 132 | ||
| 133 |
## setMethod("vec_nlines", "factor", function(vec, max_width = NULL) {
|
|
| 134 |
## lvl_nlines <- vec_nlines(levels(vec), max_width = max_width) |
|
| 135 |
## ret <- lvl_nlines[vec] |
|
| 136 |
## ret[is.na(ret)] <- format_value(NA_character |
|
| 137 |
## }) |
|
| 138 | ||
| 139 | ||
| 140 | ||
| 141 |
#' Make pagination data frame for a listing |
|
| 142 |
#' |
|
| 143 |
#' @inheritParams formatters::make_row_df |
|
| 144 |
#' @param tt (`listing_df`)\cr the listing to be rendered. |
|
| 145 |
#' @param visible_only (`flag`)\cr ignored, as listings do not have |
|
| 146 |
#' non-visible structural elements. |
|
| 147 |
#' |
|
| 148 |
#' @return a `data.frame` with pagination information. |
|
| 149 |
#' |
|
| 150 |
#' @seealso [formatters::make_row_df()] |
|
| 151 |
#' |
|
| 152 |
#' @examples |
|
| 153 |
#' lsting <- as_listing(mtcars) |
|
| 154 |
#' mf <- matrix_form(lsting) |
|
| 155 |
#' |
|
| 156 |
#' @export |
|
| 157 |
setMethod( |
|
| 158 |
"make_row_df", "listing_df", |
|
| 159 |
function(tt, colwidths = NULL, visible_only = TRUE, |
|
| 160 |
rownum = 0, |
|
| 161 |
indent = 0L, |
|
| 162 |
path = character(), |
|
| 163 |
incontent = FALSE, |
|
| 164 |
repr_ext = 0L, |
|
| 165 |
repr_inds = integer(), |
|
| 166 |
sibpos = NA_integer_, |
|
| 167 |
nsibs = NA_integer_, |
|
| 168 |
fontspec = dflt_courier, |
|
| 169 |
round_type = c("iec", "sas")) {
|
|
| 170 |
## assume sortedness by keycols |
|
| 171 | 190x |
keycols <- get_keycols(tt) |
| 172 | 190x |
dispcols <- listing_dispcols(tt) |
| 173 | 190x |
abs_rownumber <- seq_along(tt[[1]]) |
| 174 | 190x |
if (length(keycols) >= 1) {
|
| 175 | 189x |
runlens <- basic_run_lens(tt[[tail(keycols, 1)]]) |
| 176 |
} else {
|
|
| 177 | 1x |
runlens <- rep(1, NROW(tt)) |
| 178 |
} |
|
| 179 | 190x |
sibpos <- unlist(lapply(runlens, seq_len)) |
| 180 | 190x |
nsibs <- rep(runlens, times = runlens) |
| 181 | 190x |
extents <- rep(1L, nrow(tt)) |
| 182 | 190x |
if (length(colwidths) > 0 && length(colwidths) != length(dispcols)) {
|
| 183 | ! |
stop( |
| 184 | ! |
"Non-null colwidths vector must be the same length as the number of display columns.\n", |
| 185 | ! |
"Got: ", length(colwidths), "(", length(dispcols), " disp cols)."
|
| 186 |
) |
|
| 187 |
} |
|
| 188 | 190x |
if (length(colwidths) > 0) {
|
| 189 | ! |
names(colwidths) <- dispcols |
| 190 |
} |
|
| 191 |
## extents is a row-wise vector of extents, for each col, we update |
|
| 192 |
## if that column has any rows wider than the previously recorded extent. |
|
| 193 | 190x |
for (col in dispcols) {
|
| 194 |
## duplicated from matrix_form method, refactor! |
|
| 195 | 763x |
col_ext <- vec_nlines(tt[[col]], max_width = colwidths[col], fontspec = fontspec, round_type = round_type) |
| 196 | 763x |
extents <- ifelse(col_ext > extents, col_ext, extents) |
| 197 |
} |
|
| 198 | 190x |
ret <- data.frame( |
| 199 | 190x |
label = "", name = "", |
| 200 | 190x |
abs_rownumber = abs_rownumber, |
| 201 | 190x |
path = I(as.list(rep(NA_character_, NROW(tt)))), |
| 202 | 190x |
pos_in_siblings = sibpos, |
| 203 | 190x |
n_siblings = nsibs, |
| 204 | 190x |
self_extent = extents, |
| 205 | 190x |
par_extent = 0L, |
| 206 | 190x |
reprint_inds = I(replicate(NROW(tt), list(integer()))), |
| 207 | 190x |
node_class = "listing_df", |
| 208 | 190x |
indent = 0L, |
| 209 | 190x |
nrowrefs = 0L, ## XXX this doesn't support footnotes |
| 210 | 190x |
ncellrefs = 0L, ## XXX this doesn't support footnotes |
| 211 | 190x |
nreflines = 0L, ## XXX this doesn't support footnotes |
| 212 | 190x |
force_page = FALSE, |
| 213 | 190x |
page_title = NA_character_, |
| 214 | 190x |
trailing_sep = NA_character_ |
| 215 |
) |
|
| 216 | 190x |
stopifnot(identical( |
| 217 | 190x |
names(ret), |
| 218 | 190x |
names(pagdfrow( |
| 219 | 190x |
nm = "", lab = "", rnum = 1L, pth = NA_character_, extent = 1L, |
| 220 | 190x |
rclass = "" |
| 221 |
)) |
|
| 222 |
)) |
|
| 223 | 190x |
ret |
| 224 |
} |
|
| 225 |
) |
|
| 226 | ||
| 227 |
## tt$sibpos <- unlist(lapply( |
|
| 228 |
## ## don't support pathing for now |
|
| 229 |
## tt$path <- I(lapply(1:NROW(tt), |
|
| 230 |
## function(i) {
|
|
| 231 |
## retpath <- character(2*length(keycols)) |
|
| 232 |
## for(j in seq_along(keycols)) {
|
|
| 233 |
## retpath[2*j - 1] <- keycols[j] |
|
| 234 |
## retpath[2*j] <- tt[i, keycols[j], drop = TRUE] |
|
| 235 |
## } |
|
| 236 |
## retpath |
|
| 237 |
## })) |
|
| 238 |
## spl <- split(tt, tt[keycols]) |
|
| 239 |
## spl <- spl[vapply(spl, function(y) NROW(y) > 0, NA)] |
|
| 240 |
## dfs <- lapply(spl, function(df) {
|
|
| 241 |
## df <- df[order(df$abs_rownumber),] |
|
| 242 |
## ndf <- NROW(df) |
|
| 243 |
## lapply(1:ndf, function(i) {
|
|
| 244 |
## rw <- df[i,] |
|
| 245 |
## stopifnot(nrow(rw) == 1) |
|
| 246 |
## pagdfrow(nm = "", |
|
| 247 |
## lab = "", |
|
| 248 |
## rnum = rw$abs_rownumber, |
|
| 249 |
## pth = NA_character_, |
|
| 250 |
## sibpos = i, |
|
| 251 |
## nsibs = ndf, |
|
| 252 |
## extent = 1L, |
|
| 253 |
## rclass = "listing_df", |
|
| 254 |
## repind = integer()) |
|
| 255 |
## }) |
|
| 256 |
## }) |
|
| 257 |
## ret <- do.call(rbind, unlist(dfs, recursive = FALSE)) |
|
| 258 |
## ret <- ret[order(ret$abs_rownumber),] |
|
| 259 |
## ret |
|
| 260 |
## }) |
|
| 261 | ||
| 262 |
#' @inheritParams base::Extract |
|
| 263 |
#' @param x (`listing_df`)\cr the listing. |
|
| 264 |
#' @param i (`any`)\cr object passed to base `[` methods. |
|
| 265 |
#' @param j (`any`)\cr object passed to base `[` methods. |
|
| 266 |
#' |
|
| 267 |
#' @export |
|
| 268 |
#' @aliases [,listing_df-method |
|
| 269 |
#' @rdname listing_methods |
|
| 270 |
setMethod( |
|
| 271 |
"[", "listing_df", |
|
| 272 |
function(x, i, j, drop = FALSE) {
|
|
| 273 | ! |
xattr <- attributes(x) |
| 274 | ! |
xattr$names <- xattr$names[j] |
| 275 | ! |
res <- NextMethod() |
| 276 | ! |
if (!drop) {
|
| 277 | ! |
attributes(res) <- xattr |
| 278 |
} |
|
| 279 | ! |
res |
| 280 |
} |
|
| 281 |
) |
|
| 282 | ||
| 283 |
#' @rdname listing_methods |
|
| 284 |
#' @param obj (`listing_df`)\cr the listing. |
|
| 285 |
#' |
|
| 286 |
#' @return |
|
| 287 |
#' * Accessor methods return the value of the aspect of `obj`. |
|
| 288 |
#' * Setter methods return `obj` with the relevant element of the listing updated. |
|
| 289 |
#' |
|
| 290 |
#' @examples |
|
| 291 |
#' lsting <- as_listing(mtcars) |
|
| 292 |
#' main_title(lsting) <- "Hi there" |
|
| 293 |
#' |
|
| 294 |
#' main_title(lsting) |
|
| 295 |
#' |
|
| 296 |
#' @export |
|
| 297 |
setMethod( |
|
| 298 |
"main_title", "listing_df", |
|
| 299 | 192x |
function(obj) attr(obj, "main_title") %||% character() |
| 300 |
) |
|
| 301 | ||
| 302 |
#' @rdname listing_methods |
|
| 303 |
#' @export |
|
| 304 |
setMethod( |
|
| 305 |
"subtitles", "listing_df", |
|
| 306 | 208x |
function(obj) attr(obj, "subtitles") %||% character() |
| 307 |
) |
|
| 308 | ||
| 309 |
#' @rdname listing_methods |
|
| 310 |
#' @export |
|
| 311 |
setMethod( |
|
| 312 |
"main_footer", "listing_df", |
|
| 313 | 191x |
function(obj) attr(obj, "main_footer") %||% character() |
| 314 |
) |
|
| 315 | ||
| 316 |
#' @rdname listing_methods |
|
| 317 |
#' @export |
|
| 318 |
setMethod( |
|
| 319 |
"prov_footer", "listing_df", |
|
| 320 | 191x |
function(obj) attr(obj, "prov_footer") %||% character() |
| 321 |
) |
|
| 322 | ||
| 323 |
.chk_value <- function(val, fname, len_one = FALSE, null_ok = TRUE) {
|
|
| 324 | 316x |
if (null_ok && is.null(val)) {
|
| 325 | 261x |
return(TRUE) |
| 326 |
} |
|
| 327 | 55x |
if (!is.character(val)) {
|
| 328 | 4x |
stop("value for ", fname, " must be a character, got ",
|
| 329 | 4x |
"object of class: ", paste(class(val), collapse = ","), |
| 330 | 4x |
call. = FALSE |
| 331 |
) |
|
| 332 |
} |
|
| 333 | 51x |
if (len_one && length(val) > 1) {
|
| 334 | 1x |
stop( |
| 335 | 1x |
"value for ", fname, " must be length <= 1, got ", |
| 336 | 1x |
"vector of length ", length(val) |
| 337 |
) |
|
| 338 |
} |
|
| 339 | 50x |
TRUE |
| 340 |
} |
|
| 341 | ||
| 342 |
#' @rdname listing_methods |
|
| 343 |
#' @export |
|
| 344 |
setMethod( |
|
| 345 |
"main_title<-", "listing_df", |
|
| 346 |
function(obj, value) {
|
|
| 347 |
## length 1 restriction is to match rtables behavior |
|
| 348 |
## which currently enforces this (though incompletely) |
|
| 349 | 77x |
.chk_value(value, "main_title", len_one = TRUE) |
| 350 | 75x |
attr(obj, "main_title") <- value |
| 351 | 75x |
obj |
| 352 |
} |
|
| 353 |
) |
|
| 354 | ||
| 355 |
#' @rdname listing_methods |
|
| 356 |
#' @export |
|
| 357 |
setMethod( |
|
| 358 |
"subtitles<-", "listing_df", |
|
| 359 |
function(obj, value) {
|
|
| 360 | 90x |
.chk_value(value, "subtitles") |
| 361 | 89x |
attr(obj, "subtitles") <- value |
| 362 | 89x |
obj |
| 363 |
} |
|
| 364 |
) |
|
| 365 | ||
| 366 |
#' @rdname listing_methods |
|
| 367 |
#' @export |
|
| 368 |
setMethod( |
|
| 369 |
"main_footer<-", "listing_df", |
|
| 370 |
function(obj, value) {
|
|
| 371 | 75x |
.chk_value(value, "main_footer") |
| 372 | 74x |
attr(obj, "main_footer") <- value |
| 373 | 74x |
obj |
| 374 |
} |
|
| 375 |
) |
|
| 376 | ||
| 377 |
#' @rdname listing_methods |
|
| 378 |
#' @export |
|
| 379 |
setMethod( |
|
| 380 |
"prov_footer<-", "listing_df", |
|
| 381 |
function(obj, value) {
|
|
| 382 | 74x |
.chk_value(value, "prov_footer") |
| 383 | 73x |
attr(obj, "prov_footer") <- value |
| 384 | 73x |
obj |
| 385 |
} |
|
| 386 |
) |
|
| 387 | ||
| 388 |
#' @rdname listing_methods |
|
| 389 |
#' @export |
|
| 390 |
setMethod( |
|
| 391 |
"num_rep_cols", "listing_df", |
|
| 392 |
function(obj) {
|
|
| 393 | 101x |
length(get_keycols(obj)) |
| 394 |
} |
|
| 395 |
) |
| 1 |
#' Paginate listings |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("experimental")`
|
|
| 4 |
#' |
|
| 5 |
#' Pagination of a listing. This can be vertical for long listings with many |
|
| 6 |
#' rows and/or horizontal if there are many columns. This function is a wrapper of |
|
| 7 |
#' [formatters::paginate_to_mpfs()] and it is mainly meant for exploration and testing. |
|
| 8 |
#' |
|
| 9 |
#' @inheritParams formatters::pag_indices_inner |
|
| 10 |
#' @inheritParams formatters::vert_pag_indices |
|
| 11 |
#' @inheritParams formatters::page_lcpp |
|
| 12 |
#' @inheritParams formatters::toString |
|
| 13 |
#' @param lsting (`listing_df` or `list`)\cr the listing or list of listings to paginate. |
|
| 14 |
#' @param lpp (`numeric(1)` or `NULL`)\cr number of rows/lines (excluding titles and footers) |
|
| 15 |
#' to include per page. Standard is `70` while `NULL` disables vertical pagination. |
|
| 16 |
#' @param cpp (`numeric(1)` or `NULL`)\cr width (in characters) of the pages for horizontal |
|
| 17 |
#' pagination. `NULL` (the default) indicates no horizontal pagination should be done. |
|
| 18 |
#' @param print_pages (`flag`)\cr whether the paginated listing should be printed to the console |
|
| 19 |
#' (`cat(toString(x))`). |
|
| 20 |
#' |
|
| 21 |
#' @return A list of `listing_df` objects where each list element corresponds to a separate page. |
|
| 22 |
#' |
|
| 23 |
#' @examples |
|
| 24 |
#' dat <- ex_adae |
|
| 25 |
#' lsting <- as_listing(dat[1:25, ], disp_cols = c("USUBJID", "AESOC", "RACE", "AETOXGR", "BMRKR1"))
|
|
| 26 |
#' mat <- matrix_form(lsting) |
|
| 27 |
#' cat(toString(mat)) |
|
| 28 |
#' |
|
| 29 |
#' paginate_listing(lsting, lpp = 10) |
|
| 30 |
#' |
|
| 31 |
#' paginate_listing(lsting, cpp = 100, lpp = 40) |
|
| 32 |
#' |
|
| 33 |
#' paginate_listing(lsting, cpp = 80, lpp = 40, verbose = TRUE) |
|
| 34 |
#' |
|
| 35 |
#' @export |
|
| 36 |
#' @rdname paginate |
|
| 37 |
paginate_listing <- function(lsting, |
|
| 38 |
page_type = "letter", |
|
| 39 |
font_family = "Courier", |
|
| 40 |
font_size = 8, |
|
| 41 |
lineheight = 1, |
|
| 42 |
landscape = FALSE, |
|
| 43 |
pg_width = NULL, |
|
| 44 |
pg_height = NULL, |
|
| 45 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
| 46 |
lpp = NA_integer_, |
|
| 47 |
cpp = NA_integer_, |
|
| 48 |
colwidths = NULL, |
|
| 49 |
tf_wrap = !is.null(max_width), |
|
| 50 |
rep_cols = NULL, |
|
| 51 |
max_width = NULL, |
|
| 52 |
col_gap = 3, |
|
| 53 |
fontspec = font_spec(font_family, font_size, lineheight), |
|
| 54 |
verbose = FALSE, |
|
| 55 |
print_pages = TRUE) {
|
|
| 56 | 24x |
checkmate::assert_multi_class(lsting, c("listing_df", "list"))
|
| 57 | 24x |
checkmate::assert_numeric(colwidths, lower = 0, len = length(listing_dispcols(lsting)), null.ok = TRUE) |
| 58 | 23x |
checkmate::assert_flag(tf_wrap) |
| 59 | 23x |
checkmate::assert_count(max_width, null.ok = TRUE) |
| 60 | 23x |
checkmate::assert_flag(verbose) |
| 61 | 23x |
checkmate::assert_flag(print_pages) |
| 62 | ||
| 63 | 23x |
pages <- paginate_to_mpfs(lsting, |
| 64 | 23x |
page_type = page_type, |
| 65 | 23x |
fontspec = fontspec, |
| 66 | 23x |
landscape = landscape, |
| 67 | 23x |
pg_width = pg_width, |
| 68 | 23x |
pg_height = pg_height, |
| 69 | 23x |
margins = margins, |
| 70 | 23x |
lpp = lpp, |
| 71 | 23x |
cpp = cpp, |
| 72 | 23x |
colwidths = colwidths, |
| 73 | 23x |
tf_wrap = tf_wrap, |
| 74 | 23x |
max_width = max_width, |
| 75 | 23x |
rep_cols = rep_cols, |
| 76 | 23x |
col_gap = col_gap, |
| 77 | 23x |
verbose = verbose |
| 78 |
) |
|
| 79 | ||
| 80 | 23x |
if (print_pages) {
|
| 81 | 1x |
nothing <- lapply(seq_along(pages), function(pagi) {
|
| 82 | 2x |
cat("--- Page", paste0(pagi, "/", length(pages)), "---\n")
|
| 83 |
# It is NULL because paginate_mpfs takes care of it |
|
| 84 | 2x |
cat(toString(pages[[pagi]], widths = NULL, tf_wrap = tf_wrap, max_width = max_width, col_gap = col_gap)) |
| 85 | 2x |
cat("\n")
|
| 86 |
}) |
|
| 87 |
} |
|
| 88 | 23x |
invisible(pages) |
| 89 |
} |