1 |
setOldClass(c("listing_df", "tbl_df", "tbl", "data.frame")) |
|
2 |
setOldClass(c("MatrixPrintForm", "list")) |
|
3 | ||
4 |
#' @rdname listings |
|
5 |
#' @title Create a Listing from a `data.frame` or `tibble` |
|
6 |
#' |
|
7 |
#' @description `r lifecycle::badge("experimental")` |
|
8 |
#' |
|
9 |
#' Creates listings by using `cols` and `key_cols` to produce a compact and |
|
10 |
#' elegant representation of the `data.frame` or `tibble` in input. |
|
11 |
#' |
|
12 |
#' @param df data.frame or listing_df. The (non-listing) data.frame to be converted to a listing or |
|
13 |
#' the listing_df to be modified. |
|
14 |
#' @param key_cols character. Names of columns which should be treated as *key columns* |
|
15 |
#' when rendering the listing. Key columns allow you to group repeat occurrences. |
|
16 |
#' @param disp_cols character or NULL. Names of non-key columns which should be displayed when |
|
17 |
#' the listing is rendered. Defaults to all columns of `df` not named in `key_cols` or |
|
18 |
#' `non_disp_cols`. |
|
19 |
#' @param non_disp_cols character or NULL. Names of non-key columns to be excluded as display |
|
20 |
#' columns. All other non-key columns are then treated as display columns. Invalid if |
|
21 |
#' `disp_cols` is non-NULL. |
|
22 |
#' @param unique_rows logical(1). Should only unique rows be included in the listing. Defaults to `FALSE`. |
|
23 |
#' @param default_formatting list. A named list of default column format configurations to apply when rendering the |
|
24 |
#' listing. Each name-value pair consists of a name corresponding to a data class (or "numeric" for all unspecified |
|
25 |
#' numeric classes) and a value of type `fmt_config` with the format configuration that should be implemented for |
|
26 |
#' columns of that class. If named element "all" is included in the list, this configuration will be used for all |
|
27 |
#' data classes not specified. Objects of type `fmt_config` can take 3 arguments: `format`, `na_str`, and `align`. |
|
28 |
#' @param col_formatting list. A named list of custom column formatting configurations to apply to specific columns |
|
29 |
#' when rendering the listing. Each name-value pair consists of a name corresponding to a column name and a value of |
|
30 |
#' type `fmt_config` with the formatting configuration that should be implemented for that column. Objects of type |
|
31 |
#' `fmt_config` can take 3 arguments: `format`, `na_str`, and `align`. Defaults to `NULL`. |
|
32 |
#' @param main_title character(1) or NULL. The main title for the listing, or |
|
33 |
#' `NULL` (the default). Must be length 1 non-NULL. |
|
34 |
#' @param subtitles character or NULL. A vector of subtitle(s) for the |
|
35 |
#' listing, or `NULL` (the default). |
|
36 |
#' @param main_footer character or NULL. A vector of main footer lines |
|
37 |
#' for the listing, or `NULL` (the default). |
|
38 |
#' @param prov_footer character or NULL. A vector of provenance strings |
|
39 |
#' for the listing, or `NULL` (the default). Each string element is placed on a new line. |
|
40 |
#' @param vec any. A column vector from a `listing_df` to be annotated as a key column. |
|
41 |
#' |
|
42 |
#' @return A `listing_df` object, sorted by the key columns. |
|
43 |
#' |
|
44 |
#' @details At its core, a `listing_df` object is a `tbl_df` object with a customized |
|
45 |
#' print method and support for the formatting and pagination machinery provided by |
|
46 |
#' the `formatters` package. |
|
47 |
#' |
|
48 |
#' `listing_df` objects have two 'special' types of columns: key columns and display columns. |
|
49 |
#' |
|
50 |
#' Key columns act as indexes, which means a number of things in practice. |
|
51 |
#' |
|
52 |
#' All key columns are also display columns. |
|
53 |
#' |
|
54 |
#' `listing_df` objects are always sorted by their set of key_columns at creation time. |
|
55 |
#' Any `listing_df` object which is not sorted by its full set of key columns (e.g., |
|
56 |
#' one whose rows have been reordered explicitly creation) is invalid and the behavior |
|
57 |
#' when rendering or paginating that object is undefined. |
|
58 |
#' |
|
59 |
#' Each value of a key column is printed only once per page and per unique combination of |
|
60 |
#' values for all higher-priority (i.e., to the left of it) key columns. Locations |
|
61 |
#' where a repeated value would have been printed within a key column for the same |
|
62 |
#' higher-priority-key combination on the same page are rendered as empty space. |
|
63 |
#' Note, determination of which elements to display within a key column at rendering is |
|
64 |
#' based on the underlying value; any non-default formatting applied to the column |
|
65 |
#' has no effect on this behavior. |
|
66 |
#' |
|
67 |
#' Display columns are columns which should be rendered, but are not key columns. By |
|
68 |
#' default this is all non-key columns in the incoming data, but in need not be. |
|
69 |
#' Columns in the underlying data which are neither key nor display columns remain |
|
70 |
#' within the object available for computations but *are not rendered during |
|
71 |
#' printing or export of the listing*. |
|
72 |
#' |
|
73 |
#' |
|
74 |
#' @examples |
|
75 |
#' dat <- ex_adae |
|
76 |
#' |
|
77 |
#' # This example demonstrates the listing with key_cols (values are grouped by USUBJID) and |
|
78 |
#' # multiple lines in prov_footer |
|
79 |
#' lsting <- as_listing(dat[1:25, ], |
|
80 |
#' key_cols = c("USUBJID", "AESOC"), |
|
81 |
#' main_title = "Example Title for Listing", |
|
82 |
#' subtitles = "This is the subtitle for this Adverse Events Table", |
|
83 |
#' main_footer = "Main footer for the listing", |
|
84 |
#' prov_footer = c( |
|
85 |
#' "You can even add a subfooter", "Second element is place on a new line", |
|
86 |
#' "Third string" |
|
87 |
#' ) |
|
88 |
#' ) %>% |
|
89 |
#' add_listing_col("AETOXGR") %>% |
|
90 |
#' add_listing_col("BMRKR1", format = "xx.x") %>% |
|
91 |
#' add_listing_col("AESER / AREL", fun = function(df) paste(df$AESER, df$AREL, sep = " / ")) |
|
92 |
#' |
|
93 |
#' mat <- matrix_form(lsting) |
|
94 |
#' |
|
95 |
#' cat(toString(mat)) |
|
96 |
#' |
|
97 |
#' # This example demonstrates the listing table without key_cols |
|
98 |
#' # and specifying the cols with disp_cols. |
|
99 |
#' dat <- ex_adae |
|
100 |
#' lsting <- as_listing(dat[1:25, ], |
|
101 |
#' disp_cols = c("USUBJID", "AESOC", "RACE", "AETOXGR", "BMRKR1") |
|
102 |
#' ) |
|
103 |
#' |
|
104 |
#' mat <- matrix_form(lsting) |
|
105 |
#' |
|
106 |
#' cat(toString(mat)) |
|
107 |
#' |
|
108 |
#' # This example demonstrates a listing with format configurations specified |
|
109 |
#' # via the default_formatting and col_formatting arguments |
|
110 |
#' dat <- ex_adae |
|
111 |
#' dat$AENDY[3:6] <- NA |
|
112 |
#' lsting <- as_listing(dat[1:25, ], |
|
113 |
#' key_cols = c("USUBJID", "AESOC"), |
|
114 |
#' disp_cols = c("STUDYID", "SEX", "ASEQ", "RANDDT", "ASTDY", "AENDY"), |
|
115 |
#' default_formatting = list( |
|
116 |
#' all = fmt_config(align = "left"), |
|
117 |
#' numeric = fmt_config( |
|
118 |
#' format = "xx.xx", |
|
119 |
#' na_str = "<No data>", |
|
120 |
#' align = "right" |
|
121 |
#' ) |
|
122 |
#' ) |
|
123 |
#' ) %>% |
|
124 |
#' add_listing_col("BMRKR1", format = "xx.x", align = "center") |
|
125 |
#' |
|
126 |
#' mat <- matrix_form(lsting) |
|
127 |
#' |
|
128 |
#' cat(toString(mat)) |
|
129 |
#' |
|
130 |
#' @export |
|
131 |
as_listing <- function(df, |
|
132 |
key_cols = names(df)[1], |
|
133 |
disp_cols = NULL, |
|
134 |
non_disp_cols = NULL, |
|
135 |
unique_rows = FALSE, |
|
136 |
default_formatting = list(all = fmt_config()), |
|
137 |
col_formatting = NULL, |
|
138 |
main_title = NULL, |
|
139 |
subtitles = NULL, |
|
140 |
main_footer = NULL, |
|
141 |
prov_footer = NULL) { |
|
142 | 41x |
if (length(non_disp_cols) > 0 && length(intersect(key_cols, non_disp_cols)) > 0) { |
143 | 1x |
stop( |
144 | 1x |
"Key column also listed in non_disp_cols. All key columns are by", |
145 | 1x |
" definition display columns." |
146 |
) |
|
147 |
} |
|
148 | 40x |
if (!is.null(disp_cols) && !is.null(non_disp_cols)) { |
149 | 1x |
stop("Got non-null values for both disp_cols and non_disp_cols. This is not supported.") |
150 | 39x |
} else if (is.null(disp_cols)) { |
151 |
## non_disp_cols NULL is ok here |
|
152 | 15x |
cols <- setdiff(names(df), c(key_cols, non_disp_cols)) |
153 |
} else { |
|
154 |
## disp_cols non-null, non_disp_cols NULL |
|
155 | 24x |
cols <- disp_cols |
156 |
} |
|
157 | 39x |
if (!all(sapply(default_formatting, is, class2 = "fmt_config"))) { |
158 | ! |
stop( |
159 | ! |
"All format configurations supplied in `default_formatting`", |
160 | ! |
" must be of type `fmt_config`." |
161 |
) |
|
162 |
} |
|
163 | 39x |
if (!(is.null(col_formatting) || all(sapply(col_formatting, is, class2 = "fmt_config")))) { |
164 | ! |
stop( |
165 | ! |
"All format configurations supplied in `col_formatting`", |
166 | ! |
" must be of type `fmt_config`." |
167 |
) |
|
168 |
} |
|
169 | ||
170 | 39x |
df <- as_tibble(df) |
171 | 39x |
varlabs <- var_labels(df, fill = TRUE) |
172 | 39x |
o <- do.call(order, df[key_cols]) |
173 | 39x |
if (is.unsorted(o)) { |
174 | 21x |
message("sorting incoming data by key columns") |
175 | 21x |
df <- df[o, ] |
176 |
} |
|
177 | ||
178 |
## reorder the full set of cols to ensure key columns are first |
|
179 | 39x |
ordercols <- c(key_cols, setdiff(names(df), key_cols)) |
180 | 39x |
df <- df[, ordercols] |
181 | 39x |
var_labels(df) <- varlabs[ordercols] |
182 | ||
183 | 39x |
for (cnm in key_cols) { |
184 | 59x |
df[[cnm]] <- as_keycol(df[[cnm]]) |
185 |
} |
|
186 | ||
187 |
## key cols must be leftmost cols |
|
188 | 39x |
cols <- c(key_cols, setdiff(cols, key_cols)) |
189 | ||
190 | 39x |
row_all_na <- apply(df[cols], 1, function(x) all(is.na(x))) |
191 | 39x |
if (any(row_all_na)) { |
192 | 1x |
message("rows that only contain NA values have been trimmed") |
193 | 1x |
df <- df[!row_all_na, ] |
194 |
} |
|
195 | ||
196 |
# set col format configs |
|
197 | 39x |
df[cols] <- lapply(cols, function(col) { |
198 | 234x |
col_class <- tail(class(df[[col]]), 1) |
199 | 234x |
col_fmt_class <- if (!col_class %in% names(default_formatting) && is.numeric(df[[col]])) "numeric" else col_class |
200 | 234x |
col_fmt <- if (col %in% names(col_formatting)) { |
201 | 6x |
col_formatting[[col]] |
202 | 234x |
} else if (col_fmt_class %in% names(default_formatting)) { |
203 | 6x |
default_formatting[[col_fmt_class]] |
204 |
} else { |
|
205 | 222x |
if (!"all" %in% names(default_formatting)) { |
206 | ! |
stop( |
207 | ! |
"Format configurations must be supplied for all listing columns. ", |
208 | ! |
"To cover all remaining columns please add an 'all' configuration", |
209 | ! |
" to `default_formatting`." |
210 |
) |
|
211 |
} |
|
212 | 222x |
default_formatting[["all"]] |
213 |
} |
|
214 |
# ANY attr <- fmt_config slot |
|
215 | 234x |
obj_format(df[[col]]) <- obj_format(col_fmt) |
216 | 234x |
obj_na_str(df[[col]]) <- if (is.null(obj_na_str(col_fmt))) "NA" else obj_na_str(col_fmt) |
217 | 234x |
obj_align(df[[col]]) <- if (is.null(obj_align(col_fmt))) "left" else obj_align(col_fmt) |
218 | 234x |
df[[col]] |
219 |
}) |
|
220 | ||
221 | 2x |
if (unique_rows) df <- df[!duplicated(df[, cols]), ] |
222 | ||
223 | 39x |
class(df) <- c("listing_df", class(df)) |
224 |
## these all work even when the value is NULL |
|
225 | 39x |
main_title(df) <- main_title |
226 | 39x |
main_footer(df) <- main_footer |
227 | 39x |
subtitles(df) <- subtitles |
228 | 39x |
prov_footer(df) <- prov_footer |
229 | 39x |
listing_dispcols(df) <- cols |
230 | 39x |
df |
231 |
} |
|
232 | ||
233 | ||
234 |
#' @export |
|
235 |
#' @rdname listings |
|
236 |
as_keycol <- function(vec) { |
|
237 | 59x |
if (is.factor(vec)) { |
238 | 13x |
lab <- obj_label(vec) |
239 | 13x |
vec <- as.character(vec) |
240 | 13x |
obj_label(vec) <- lab |
241 |
} |
|
242 | 59x |
class(vec) <- c("listing_keycol", class(vec)) |
243 | 59x |
vec |
244 |
} |
|
245 | ||
246 | ||
247 |
#' @export |
|
248 |
#' @rdname listings |
|
249 |
is_keycol <- function(vec) { |
|
250 | 3989x |
inherits(vec, "listing_keycol") |
251 |
} |
|
252 | ||
253 | ||
254 | ||
255 |
#' @export |
|
256 |
#' @rdname listings |
|
257 |
get_keycols <- function(df) { |
|
258 | 249x |
names(which(sapply(df, is_keycol))) |
259 |
} |
|
260 | ||
261 |
#' @export |
|
262 |
#' @inherit formatters::matrix_form |
|
263 |
#' @seealso [formatters::matrix_form()] |
|
264 |
#' @param indent_rownames logical(1). Silently ignored, as listings do not have row names |
|
265 |
#' nor indenting structure. |
|
266 |
#' |
|
267 |
#' @examples |
|
268 |
#' |
|
269 |
#' lsting <- as_listing(mtcars) |
|
270 |
#' mf <- matrix_form(lsting) |
|
271 |
#' |
|
272 |
#' @return a `MatrixPrintForm` object |
|
273 |
setMethod( |
|
274 |
"matrix_form", "listing_df", |
|
275 |
rix_form <- function(obj, indent_rownames = FALSE) { |
|
276 |
## we intentionally silently ignore indent_rownames because listings have |
|
277 |
## no rownames, but formatters::vert_pag_indices calls matrix_form(obj, TRUE) |
|
278 |
## unconditionally. |
|
279 | 110x |
cols <- attr(obj, "listing_dispcols") |
280 | 110x |
listing <- obj[, cols] |
281 | 110x |
atts <- attributes(obj) |
282 | 110x |
atts$names <- cols |
283 | 110x |
attributes(listing) <- atts |
284 | 110x |
keycols <- get_keycols(listing) |
285 | ||
286 | ||
287 | 110x |
bodymat <- matrix("", |
288 | 110x |
nrow = nrow(listing), |
289 | 110x |
ncol = ncol(listing) |
290 |
) |
|
291 | ||
292 | 110x |
colnames(bodymat) <- names(listing) |
293 | ||
294 | ||
295 | 110x |
curkey <- "" |
296 | 110x |
for (i in seq_along(keycols)) { |
297 | 161x |
kcol <- keycols[i] |
298 | 161x |
kcolvec <- listing[[kcol]] |
299 | 161x |
kcolvec <- vapply(kcolvec, format_value, "", format = obj_format(kcolvec), na_str = obj_na_str(kcolvec)) |
300 | 161x |
curkey <- paste0(curkey, kcolvec) |
301 | 161x |
disp <- c(TRUE, tail(curkey, -1) != head(curkey, -1)) |
302 | 161x |
bodymat[disp, kcol] <- kcolvec[disp] |
303 |
} |
|
304 | ||
305 | 110x |
nonkeycols <- setdiff(names(listing), keycols) |
306 | 110x |
if (length(nonkeycols) > 0) { |
307 | 109x |
for (nonk in nonkeycols) { |
308 | 314x |
vec <- listing[[nonk]] |
309 | 314x |
vec <- vapply(vec, format_value, "", format = obj_format(vec), na_str = obj_na_str(vec)) |
310 | 314x |
bodymat[, nonk] <- vec |
311 |
} |
|
312 |
} |
|
313 | ||
314 | ||
315 | 110x |
fullmat <- rbind( |
316 | 110x |
var_labels(listing, fill = TRUE), |
317 | 110x |
bodymat |
318 |
) |
|
319 | ||
320 | 110x |
colaligns <- rbind( |
321 | 110x |
rep("center", length(cols)), |
322 | 110x |
matrix(sapply(listing, obj_align), |
323 | 110x |
ncol = length(cols), |
324 | 110x |
nrow = nrow(fullmat) - 1, |
325 | 110x |
byrow = TRUE |
326 |
) |
|
327 |
) |
|
328 | ||
329 | 110x |
MatrixPrintForm( |
330 | 110x |
strings = fullmat, |
331 | 110x |
spans = matrix(1, |
332 | 110x |
nrow = nrow(fullmat), |
333 | 110x |
ncol = ncol(fullmat) |
334 |
), |
|
335 | 110x |
ref_fnotes = list(), |
336 | 110x |
aligns = colaligns, |
337 | 110x |
formats = matrix(1, |
338 | 110x |
nrow = nrow(fullmat), |
339 | 110x |
ncol = ncol(fullmat) |
340 |
), |
|
341 | 110x |
row_info = make_row_df(obj), |
342 | 110x |
nlines_header = 1, ## XXX this is probably wrong!!! |
343 | 110x |
nrow_header = 1, |
344 | 110x |
has_topleft = FALSE, |
345 | 110x |
has_rowlabs = FALSE, |
346 | 110x |
expand_newlines = TRUE, |
347 | 110x |
main_title = main_title(obj), |
348 | 110x |
subtitles = subtitles(obj), |
349 | 110x |
page_titles = page_titles(obj), |
350 | 110x |
main_footer = main_footer(obj), |
351 | 110x |
prov_footer = prov_footer(obj) |
352 |
) |
|
353 |
} |
|
354 |
) |
|
355 | ||
356 | ||
357 |
#' @export |
|
358 |
#' @rdname listings |
|
359 | 172x |
listing_dispcols <- function(df) attr(df, "listing_dispcols") %||% character() |
360 | ||
361 |
#' @export |
|
362 |
#' @param new character. Names of columns to be added to |
|
363 |
#' the set of display columns. |
|
364 |
#' @rdname listings |
|
365 |
add_listing_dispcol <- function(df, new) { |
|
366 | 18x |
listing_dispcols(df) <- c(listing_dispcols(df), new) |
367 | 18x |
df |
368 |
} |
|
369 |
#' @export |
|
370 |
#' @param value character. New value. |
|
371 |
#' @rdname listings |
|
372 |
`listing_dispcols<-` <- function(df, value) { |
|
373 | 135x |
if (!is.character(value)) { |
374 | ! |
stop( |
375 | ! |
"dispcols must be a character vector of column names, got ", |
376 | ! |
"object of class: ", paste(class(value), collapse = ",") |
377 |
) |
|
378 |
} |
|
379 | 135x |
chk <- setdiff(value, names(df)) ## remember setdiff is not symmetrical |
380 | 135x |
if (length(chk) > 0) { |
381 | ! |
stop( |
382 | ! |
"listing display columns must be columns in the underlying data. ", |
383 | ! |
"Column(s) ", paste(chk, collapse = ", "), " not present in the data." |
384 |
) |
|
385 |
} |
|
386 | 135x |
attr(df, "listing_dispcols") <- unique(value) |
387 | 135x |
df |
388 |
} |
|
389 | ||
390 | ||
391 | ||
392 |
#' @rdname listings |
|
393 |
#' |
|
394 |
#' @param name character(1). Name of the existing or new column to be |
|
395 |
#' displayed when the listing is rendered. |
|
396 |
#' @param fun function or NULL. A function which accepts \code{df} and |
|
397 |
#' returns the vector for a new column, which is added to \code{df} as |
|
398 |
#' \code{name}, or NULL if marking an existing column as |
|
399 |
#' a listing column. |
|
400 |
#' @inheritParams formatters::fmt_config |
|
401 |
#' |
|
402 |
#' @return `df`, with `name` created (if necessary) and marked for |
|
403 |
#' display during rendering. |
|
404 |
#' |
|
405 |
#' @export |
|
406 |
add_listing_col <- function(df, |
|
407 |
name, |
|
408 |
fun = NULL, |
|
409 |
format = NULL, |
|
410 |
na_str = "NA", |
|
411 |
align = "left") { |
|
412 | 18x |
if (!is.null(fun)) { |
413 | 1x |
vec <- with_label(fun(df), name) |
414 | 17x |
} else if (name %in% names(df)) { |
415 | 17x |
vec <- df[[name]] |
416 |
} else { |
|
417 | ! |
stop( |
418 | ! |
"Column '", name, "' not found. name argument must specify an existing column when ", |
419 | ! |
"no generating function (fun argument) is specified." |
420 |
) |
|
421 |
} |
|
422 | ||
423 | 18x |
if (!is.null(format)) { |
424 | 8x |
obj_format(vec) <- format |
425 |
} |
|
426 | ||
427 | 18x |
obj_na_str(vec) <- na_str |
428 | 18x |
obj_align(vec) <- align |
429 | ||
430 |
## this works for both new and existing columns |
|
431 | 18x |
df[[name]] <- vec |
432 | 18x |
df <- add_listing_dispcol(df, name) |
433 | 18x |
df |
434 |
} |
1 |
## #' Print a listing to the terminal |
|
2 |
## #' @param x listing_df. the listing |
|
3 |
## #' @param ... ANY. unused |
|
4 |
## #' @return prints the listing object to the screen and silently returns the object |
|
5 |
## #' @export |
|
6 |
## setMethod("print", "listing_df", |
|
7 |
## function(x, ...) { |
|
8 |
## cat(toString(listing_matrix_form(x))) |
|
9 |
## invisible(x) |
|
10 |
## }) |
|
11 | ||
12 |
#' Methods for `listing_df` objects |
|
13 |
#' |
|
14 |
#' See core documentation in \code{formatters} for descriptions |
|
15 |
#' of these functions. |
|
16 |
#' |
|
17 |
#' @export |
|
18 |
#' @inheritParams formatters::toString |
|
19 |
#' @param x listing_df. The listing. |
|
20 |
#' @param ... dots. See `toString` method in \code{formatters} for all parameters. |
|
21 |
#' @method print listing_df |
|
22 |
#' @name listing_methods |
|
23 |
print.listing_df <- function(x, widths = NULL, tf_wrap = FALSE, max_width = NULL, ...) { |
|
24 | 2x |
cat(toString(matrix_form(x), widths = widths, tf_wrap = tf_wrap, max_width = max_width, ...)) |
25 | 2x |
invisible(x) |
26 |
} |
|
27 | ||
28 |
#' @exportMethod toString |
|
29 |
#' @name listing_methods |
|
30 |
#' @aliases toString,listing_df-method |
|
31 |
setMethod("toString", "listing_df", function(x, ...) { |
|
32 | 4x |
toString(matrix_form(x), ...) |
33 |
}) |
|
34 | ||
35 |
## because rle in base is too much of a stickler for being atomic |
|
36 |
basic_run_lens <- function(x) { |
|
37 | 110x |
n <- length(x) |
38 | 110x |
if (n == 0) { |
39 | ! |
return(integer()) |
40 |
} |
|
41 | ||
42 | 110x |
y <- x[-1L] != x[-n] |
43 | 110x |
i <- c(which(y), n) |
44 | 110x |
diff(c(0L, i)) |
45 |
} |
|
46 | ||
47 | ||
48 |
#' @rdname vec_nlines |
|
49 |
#' @param df listing_df. The listing. |
|
50 |
#' @param colnm Column name |
|
51 |
#' @param colvec Column values based on colnm |
|
52 |
format_colvector <- function(df, colnm, colvec = df[[colnm]]) { |
|
53 | 477x |
if (missing(colvec) && !(colnm %in% names(df))) { |
54 | ! |
stop("column ", colnm, " not found") |
55 |
} |
|
56 | 477x |
na_str <- obj_na_str(colvec) |
57 | 477x |
if (is.null(na_str) || all(is.na(na_str))) { |
58 | ! |
na_str <- rep("-", max(1L, length(na_str))) |
59 |
} |
|
60 | ||
61 | 477x |
strvec <- vapply(colvec, format_value, "", format = obj_format(colvec), na_str = na_str) |
62 | 477x |
strvec |
63 |
} |
|
64 | ||
65 |
#' Utilities for formatting a listing column |
|
66 |
#' |
|
67 |
#' For `vec_nlines`, calculate the number of lines each element of a column vector will |
|
68 |
#' take to render. For `format_colvector`, |
|
69 |
#' |
|
70 |
#' @param vec any vector. A column vector to be rendered into ASCII. |
|
71 |
#' @param max_width numeric (or NULL). The width the column will be |
|
72 |
#' rendered in. |
|
73 |
#' @return a numeric vector of the number of lines elementwise that |
|
74 |
#' will be needed to render the elements of \code{vec} to width |
|
75 |
#' \code{max_width}. |
|
76 |
#' @keywords internal |
|
77 | 477x |
setGeneric("vec_nlines", function(vec, max_width = NULL) standardGeneric("vec_nlines")) |
78 | ||
79 |
#' @rdname vec_nlines |
|
80 |
#' @param vec A vector. |
|
81 |
#' @keywords internal |
|
82 |
setMethod("vec_nlines", "ANY", function(vec, max_width = NULL) { |
|
83 | 477x |
if (is.null(max_width)) { |
84 | 477x |
max_width <- floor(0.9 * getOption("width")) # default of base::strwrap |
85 |
# NB: flooring as it is used as <= (also in base::strwrap) |
|
86 |
} |
|
87 |
# in formatters for characters |
|
88 | 477x |
unlist(lapply(format_colvector(colvec = vec), nlines, max_width = max_width)) |
89 |
}) |
|
90 | ||
91 |
## setMethod("vec_nlines", "character", function(vec, max_width = NULL) { |
|
92 |
## strvec <- wrap_txt(format_colvector(colvec = vec), width = max_width, collapse = "\n") |
|
93 |
## mtchs <- gregexpr("\n", strvec, fixed = TRUE) |
|
94 |
## 1L + vapply(mtchs, function(vi) sum(vi > 0), 1L) |
|
95 |
## }) |
|
96 | ||
97 |
## setMethod("vec_nlines", "factor", function(vec, max_width = NULL) { |
|
98 |
## lvl_nlines <- vec_nlines(levels(vec), max_width = max_width) |
|
99 |
## ret <- lvl_nlines[vec] |
|
100 |
## ret[is.na(ret)] <- format_value(NA_character |
|
101 |
## }) |
|
102 | ||
103 |
#' Make pagination dataframe for a listing |
|
104 |
#' @export |
|
105 |
#' @inheritParams formatters::make_row_df |
|
106 |
#' @param tt listing_df. The listing to be rendered |
|
107 |
#' @param visible_only logical(1). Ignored, as listings |
|
108 |
#' do not have non-visible structural elements. |
|
109 |
#' |
|
110 |
#' @examples |
|
111 |
#' lsting <- as_listing(mtcars) |
|
112 |
#' mf <- matrix_form(lsting) |
|
113 |
#' |
|
114 |
#' @return a data.frame with pagination information. |
|
115 |
#' @seealso \code{\link[formatters]{make_row_df}} |
|
116 |
setMethod( |
|
117 |
"make_row_df", "listing_df", |
|
118 |
function(tt, colwidths = NULL, visible_only = TRUE, |
|
119 |
rownum = 0, |
|
120 |
indent = 0L, |
|
121 |
path = character(), |
|
122 |
incontent = FALSE, |
|
123 |
repr_ext = 0L, |
|
124 |
repr_inds = integer(), |
|
125 |
sibpos = NA_integer_, |
|
126 |
nsibs = NA_integer_) { |
|
127 |
## assume sortedness by keycols |
|
128 | 111x |
keycols <- get_keycols(tt) |
129 | 111x |
dispcols <- listing_dispcols(tt) |
130 | 111x |
abs_rownumber <- seq_along(tt[[1]]) |
131 | 111x |
if (length(keycols) >= 1) { |
132 | 110x |
runlens <- basic_run_lens(tt[[tail(keycols, 1)]]) |
133 |
} else { |
|
134 | 1x |
runlens <- rep(1, NROW(tt)) |
135 |
} |
|
136 | 111x |
sibpos <- unlist(lapply(runlens, seq_len)) |
137 | 111x |
nsibs <- rep(runlens, times = runlens) |
138 | 111x |
extents <- rep(1L, nrow(tt)) |
139 | 111x |
if (length(colwidths) > 0 && length(colwidths) != length(dispcols)) { |
140 | ! |
stop( |
141 | ! |
"Non-null colwidths vector must be the same length as the number of display columns.\n", |
142 | ! |
"Got: ", length(colwidths), "(", length(dispcols), " disp cols)." |
143 |
) |
|
144 |
} |
|
145 | 111x |
if (length(colwidths) > 0) { |
146 | ! |
names(colwidths) <- dispcols |
147 |
} |
|
148 |
## extents is a row-wise vector of extents, for each col, we update |
|
149 |
## if that column has any rows wider than the previously recorded extent. |
|
150 | 111x |
for (col in dispcols) { |
151 |
## duplicated from matrix_form method, refactor! |
|
152 | 477x |
col_ext <- vec_nlines(tt[[col]], max_width = colwidths[col]) |
153 | 477x |
extents <- ifelse(col_ext > extents, col_ext, extents) |
154 |
} |
|
155 | 111x |
ret <- data.frame( |
156 | 111x |
label = "", name = "", |
157 | 111x |
abs_rownumber = abs_rownumber, |
158 | 111x |
path = I(as.list(rep(NA_character_, NROW(tt)))), |
159 | 111x |
pos_in_siblings = sibpos, |
160 | 111x |
n_siblings = nsibs, |
161 | 111x |
self_extent = extents, |
162 | 111x |
par_extent = 0L, |
163 | 111x |
reprint_inds = I(replicate(NROW(tt), list(integer()))), |
164 | 111x |
node_class = "listing_df", |
165 | 111x |
indent = 0L, |
166 | 111x |
nrowrefs = 0L, ## XXX this doesn't support footnotes |
167 | 111x |
ncellrefs = 0L, ## XXX this doesn't support footnotes |
168 | 111x |
nreflines = 0L, ## XXX this doesn't support footnotes |
169 | 111x |
force_page = FALSE, |
170 | 111x |
page_title = NA_character_, |
171 | 111x |
trailing_sep = NA_character_ |
172 |
) |
|
173 | 111x |
stopifnot(identical( |
174 | 111x |
names(ret), |
175 | 111x |
names(pagdfrow( |
176 | 111x |
nm = "", lab = "", rnum = 1L, pth = NA_character_, extent = 1L, |
177 | 111x |
rclass = "" |
178 |
)) |
|
179 |
)) |
|
180 | 111x |
ret |
181 |
} |
|
182 |
) |
|
183 | ||
184 |
## tt$sibpos <- unlist(lapply( |
|
185 |
## ## don't support pathing for now |
|
186 |
## tt$path <- I(lapply(1:NROW(tt), |
|
187 |
## function(i) { |
|
188 |
## retpath <- character(2*length(keycols)) |
|
189 |
## for(j in seq_along(keycols)) { |
|
190 |
## retpath[2*j - 1] <- keycols[j] |
|
191 |
## retpath[2*j] <- tt[i, keycols[j], drop = TRUE] |
|
192 |
## } |
|
193 |
## retpath |
|
194 |
## })) |
|
195 |
## spl <- split(tt, tt[keycols]) |
|
196 |
## spl <- spl[vapply(spl, function(y) NROW(y) > 0, NA)] |
|
197 |
## dfs <- lapply(spl, function(df) { |
|
198 |
## df <- df[order(df$abs_rownumber),] |
|
199 |
## ndf <- NROW(df) |
|
200 |
## lapply(1:ndf, function(i) { |
|
201 |
## rw <- df[i,] |
|
202 |
## stopifnot(nrow(rw) == 1) |
|
203 |
## pagdfrow(nm = "", |
|
204 |
## lab = "", |
|
205 |
## rnum = rw$abs_rownumber, |
|
206 |
## pth = NA_character_, |
|
207 |
## sibpos = i, |
|
208 |
## nsibs = ndf, |
|
209 |
## extent = 1L, |
|
210 |
## rclass = "listing_df", |
|
211 |
## repind = integer()) |
|
212 |
## }) |
|
213 |
## }) |
|
214 |
## ret <- do.call(rbind, unlist(dfs, recursive = FALSE)) |
|
215 |
## ret <- ret[order(ret$abs_rownumber),] |
|
216 |
## ret |
|
217 |
## }) |
|
218 | ||
219 |
#' @export |
|
220 |
#' @param x listing_df. The listing. |
|
221 |
#' @inheritParams base::Extract |
|
222 |
#' @param i ANY. Passed to base `[` methods. |
|
223 |
#' @param j ANY. Passed to base `[` methods. |
|
224 |
#' @aliases [,listing_df-method |
|
225 |
#' @rdname listing_methods |
|
226 |
#' @keywords internal |
|
227 |
setMethod( |
|
228 |
"[", "listing_df", |
|
229 |
function(x, i, j, drop = FALSE) { |
|
230 | ! |
xattr <- attributes(x) |
231 | ! |
xattr$names <- xattr$names[j] |
232 | ! |
res <- NextMethod() |
233 | ! |
if (!drop) { |
234 | ! |
attributes(res) <- xattr |
235 |
} |
|
236 | ! |
res |
237 |
} |
|
238 |
) |
|
239 | ||
240 |
#' @rdname listing_methods |
|
241 |
#' @param obj The object. |
|
242 |
#' @export |
|
243 |
#' @return for getter methods, the value of the aspect of |
|
244 |
#' \code{obj}; for setter methods, \code{obj} with |
|
245 |
#' the relevant element of the listing updated. |
|
246 |
#' |
|
247 |
#' @examples |
|
248 |
#' |
|
249 |
#' lsting <- as_listing(mtcars) |
|
250 |
#' main_title(lsting) <- "Hi there" |
|
251 |
#' |
|
252 |
#' main_title(lsting) |
|
253 |
setMethod( |
|
254 |
"main_title", "listing_df", |
|
255 | 113x |
function(obj) attr(obj, "main_title") %||% character() |
256 |
) |
|
257 | ||
258 |
#' @rdname listing_methods |
|
259 |
#' @export |
|
260 |
setMethod( |
|
261 |
"subtitles", "listing_df", |
|
262 | 112x |
function(obj) attr(obj, "subtitles") %||% character() |
263 |
) |
|
264 |
#' @rdname listing_methods |
|
265 |
#' @export |
|
266 |
setMethod( |
|
267 |
"main_footer", "listing_df", |
|
268 | 112x |
function(obj) attr(obj, "main_footer") %||% character() |
269 |
) |
|
270 |
#' @rdname listing_methods |
|
271 |
#' @export |
|
272 |
setMethod( |
|
273 |
"prov_footer", "listing_df", |
|
274 | 112x |
function(obj) attr(obj, "prov_footer") %||% character() |
275 |
) |
|
276 | ||
277 |
.chk_value <- function(val, fname, len_one = FALSE, null_ok = TRUE) { |
|
278 | 168x |
if (null_ok && is.null(val)) { |
279 | 148x |
return(TRUE) |
280 |
} |
|
281 | 20x |
if (!is.character(val)) { |
282 | 4x |
stop("value for ", fname, " must be a character, got ", |
283 | 4x |
"object of class: ", paste(class(val), collapse = ","), |
284 | 4x |
call. = FALSE |
285 |
) |
|
286 |
} |
|
287 | 16x |
if (len_one && length(val) > 1) { |
288 | 1x |
stop( |
289 | 1x |
"value for ", fname, " must be length <= 1, got ", |
290 | 1x |
"vector of length ", length(val) |
291 |
) |
|
292 |
} |
|
293 | 15x |
TRUE |
294 |
} |
|
295 | ||
296 |
#' @rdname listing_methods |
|
297 |
#' @export |
|
298 |
setMethod( |
|
299 |
"main_title<-", "listing_df", |
|
300 |
function(obj, value) { |
|
301 |
## length 1 restriction is to match rtables behavior |
|
302 |
## which currently enforces this (though incompletely) |
|
303 | 44x |
.chk_value(value, "main_title", len_one = TRUE) |
304 | 42x |
attr(obj, "main_title") <- value |
305 | 42x |
obj |
306 |
} |
|
307 |
) |
|
308 | ||
309 |
#' @rdname listing_methods |
|
310 |
#' @export |
|
311 |
setMethod( |
|
312 |
"subtitles<-", "listing_df", |
|
313 |
function(obj, value) { |
|
314 | 41x |
.chk_value(value, "subtitles") |
315 | 40x |
attr(obj, "subtitles") <- value |
316 | 40x |
obj |
317 |
} |
|
318 |
) |
|
319 | ||
320 |
#' @rdname listing_methods |
|
321 |
#' @export |
|
322 |
setMethod( |
|
323 |
"main_footer<-", "listing_df", |
|
324 |
function(obj, value) { |
|
325 | 42x |
.chk_value(value, "main_footer") |
326 | 41x |
attr(obj, "main_footer") <- value |
327 | 41x |
obj |
328 |
} |
|
329 |
) |
|
330 | ||
331 |
#' @rdname listing_methods |
|
332 |
#' @export |
|
333 |
setMethod( |
|
334 |
"prov_footer<-", "listing_df", |
|
335 |
function(obj, value) { |
|
336 | 41x |
.chk_value(value, "prov_footer") |
337 | 40x |
attr(obj, "prov_footer") <- value |
338 | 40x |
obj |
339 |
} |
|
340 |
) |
|
341 | ||
342 |
#' @rdname listing_methods |
|
343 |
#' @export |
|
344 |
setMethod( |
|
345 |
"num_rep_cols", "listing_df", |
|
346 |
function(obj) { |
|
347 | 7x |
length(get_keycols(obj)) |
348 |
} |
|
349 |
) |
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 or horizontal if there are many columns. |
|
7 |
#' |
|
8 |
#' @param lsting listing_df. The listing to paginate. |
|
9 |
#' @param lpp numeric(1) or NULL. Number of row lines (not counting titles and |
|
10 |
#' footers) to have per page. Standard is `70` while `NULL` disables vertical |
|
11 |
#' pagination. |
|
12 |
#' @param cpp numeric(1) or NULL. Width (in characters) of the pages for |
|
13 |
#' horizontal pagination. `NULL` (the default) indicates no horizontal |
|
14 |
#' pagination should be done. |
|
15 |
#' @inheritParams formatters::pag_indices_inner |
|
16 |
#' @inheritParams formatters::vert_pag_indices |
|
17 |
#' @inheritParams formatters::page_lcpp |
|
18 |
#' @inheritParams formatters::toString |
|
19 |
#' |
|
20 |
#' @returns A list of listings' objects that are meant to be on separated pages. |
|
21 |
#' For `pag_tt_indices` a list of paginated-groups of row-indices of `lsting`. |
|
22 |
#' |
|
23 |
#' @rdname paginate |
|
24 |
#' |
|
25 |
#' @examples |
|
26 |
#' dat <- ex_adae |
|
27 |
#' lsting <- as_listing(dat[1:25, ], disp_cols = c("USUBJID", "AESOC", "RACE", "AETOXGR", "BMRKR1")) |
|
28 |
#' |
|
29 |
#' mat <- matrix_form(lsting) |
|
30 |
#' |
|
31 |
#' cat(toString(mat)) |
|
32 |
#' |
|
33 |
#' paginate_listing(lsting, lpp = 10) |
|
34 |
#' |
|
35 |
#' paginate_listing(lsting, cpp = 100, lpp = 40) |
|
36 |
#' |
|
37 |
#' paginate_listing(lsting, cpp = 80, lpp = 40, verbose = TRUE) |
|
38 |
#' @export |
|
39 |
#' |
|
40 |
#' @return for `paginate_listing` a list containing separate |
|
41 |
#' `listing_df` objects for each page, for `pag_listing_indices`, |
|
42 |
#' a list of indices in the direction being paginated corresponding |
|
43 |
#' to the individual pages in that dimension. |
|
44 |
paginate_listing <- function(lsting, |
|
45 |
page_type = "letter", |
|
46 |
font_family = "Courier", |
|
47 |
font_size = 8, |
|
48 |
lineheight = 1, |
|
49 |
landscape = FALSE, |
|
50 |
pg_width = NULL, |
|
51 |
pg_height = NULL, |
|
52 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
53 |
lpp = NA_integer_, |
|
54 |
cpp = NA_integer_, |
|
55 |
colwidths = propose_column_widths(lsting), |
|
56 |
tf_wrap = !is.null(max_width), |
|
57 |
max_width = NULL, |
|
58 |
verbose = FALSE) { |
|
59 | 21x |
checkmate::assert_class(lsting, "listing_df") |
60 | 21x |
checkmate::assert_numeric(colwidths, lower = 0, len = length(listing_dispcols(lsting)), null.ok = TRUE) |
61 | 20x |
checkmate::assert_flag(tf_wrap) |
62 | 20x |
checkmate::assert_count(max_width, null.ok = TRUE) |
63 | 20x |
checkmate::assert_flag(verbose) |
64 | ||
65 | 20x |
indx <- paginate_indices(lsting, |
66 | 20x |
page_type = page_type, |
67 | 20x |
font_family = font_family, |
68 | 20x |
font_size = font_size, |
69 | 20x |
lineheight = lineheight, |
70 | 20x |
landscape = landscape, |
71 | 20x |
pg_width = pg_width, |
72 | 20x |
pg_height = pg_height, |
73 | 20x |
margins = margins, |
74 | 20x |
lpp = lpp, |
75 | 20x |
cpp = cpp, |
76 | 20x |
colwidths = colwidths, |
77 | 20x |
tf_wrap = tf_wrap, |
78 | 20x |
max_width = max_width, |
79 | 20x |
rep_cols = length(get_keycols(lsting)), |
80 | 20x |
verbose = verbose |
81 |
) |
|
82 | ||
83 | 20x |
vert_pags <- lapply( |
84 | 20x |
indx$pag_row_indices, |
85 | 20x |
function(ii) lsting[ii, ] |
86 |
) |
|
87 | 20x |
dispnames <- listing_dispcols(lsting) |
88 | 20x |
full_pag <- lapply( |
89 | 20x |
vert_pags, |
90 | 20x |
function(onepag) { |
91 | 29x |
if (!is.null(indx$pag_col_indices)) { |
92 | 29x |
lapply( |
93 | 29x |
indx$pag_col_indices, |
94 | 29x |
function(jj) { |
95 | 78x |
res <- onepag[, dispnames[jj], drop = FALSE] |
96 | 78x |
listing_dispcols(res) <- intersect(dispnames, names(res)) |
97 | 78x |
res |
98 |
} |
|
99 |
) |
|
100 |
} else { |
|
101 | ! |
list(onepag) |
102 |
} |
|
103 |
} |
|
104 |
) |
|
105 | ||
106 | 20x |
ret <- unlist(full_pag, recursive = FALSE) |
107 | 20x |
ret |
108 |
} |
|
109 | ||
110 |
#' @title Defunct functions |
|
111 |
#' |
|
112 |
#' @description |
|
113 |
#' These functions are defunct and their symbols will be removed entirely |
|
114 |
#' in a future release. |
|
115 |
#' @rdname defunct |
|
116 |
#' @inheritParams paginate_listing |
|
117 |
#' @export |
|
118 |
pag_listing_indices <- function(lsting, |
|
119 |
lpp = 15, |
|
120 |
colwidths = NULL, |
|
121 |
max_width = NULL, |
|
122 |
verbose = FALSE) { |
|
123 | 1x |
.Defunct("paginate_indices", package = "formatters") |
124 |
} |