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