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 | 86x |
checkmate::assert_multi_class(add_trailing_sep, c("character", "numeric"), null.ok = TRUE) |
185 | 86x |
checkmate::assert_string(trailing_sep, n.chars = 1) |
186 | ||
187 | 85x |
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 | 84x |
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 | 83x |
} 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 | 51x |
cols <- disp_cols |
201 |
} |
|
202 | 83x |
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 | 82x |
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 | 80x |
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 | 79x |
df <- as_tibble(df) |
220 | 79x |
varlabs <- var_labels(df, fill = TRUE) |
221 | 79x |
if (!is.null(sort_cols)) { |
222 | 77x |
sort_miss <- setdiff(sort_cols, names(df)) |
223 | 77x |
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 | 76x |
o <- do.call(order, df[sort_cols]) |
230 | 76x |
if (is.unsorted(o)) { |
231 | 39x |
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 | 39x |
df <- df[o, ] |
242 |
} |
|
243 |
} |
|
244 | ||
245 |
## reorder the full set of cols to ensure key columns are first |
|
246 | 78x |
ordercols <- c(key_cols, setdiff(names(df), key_cols)) |
247 | 78x |
df <- df[, ordercols] |
248 | 78x |
var_labels(df) <- varlabs[ordercols] |
249 | ||
250 | 78x |
for (cnm in key_cols) { |
251 | 125x |
df[[cnm]] <- as_keycol(df[[cnm]]) |
252 |
} |
|
253 | ||
254 |
## key cols must be leftmost cols |
|
255 | 78x |
cols <- c(key_cols, setdiff(cols, key_cols)) |
256 | ||
257 | 78x |
row_all_na <- apply(df[cols], 1, function(x) all(is.na(x))) |
258 | 78x |
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 | 78x |
df[cols] <- lapply(cols, function(col) { |
265 | 460x |
col_class <- tail(class(df[[col]]), 1) |
266 | 460x |
col_fmt_class <- if (!col_class %in% names(default_formatting) && is.numeric(df[[col]])) "numeric" else col_class |
267 | 460x |
col_fmt <- if (col %in% names(col_formatting)) { |
268 | 8x |
col_formatting[[col]] |
269 | 460x |
} else if (col_fmt_class %in% names(default_formatting)) { |
270 | 6x |
default_formatting[[col_fmt_class]] |
271 |
} else { |
|
272 | 446x |
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 | 445x |
default_formatting[["all"]] |
280 |
} |
|
281 |
# ANY attr <- fmt_config slot |
|
282 | 459x |
obj_format(df[[col]]) <- obj_format(col_fmt) |
283 | 459x |
obj_na_str(df[[col]]) <- if (is.null(obj_na_str(col_fmt))) "NA" else obj_na_str(col_fmt) |
284 | 459x |
obj_align(df[[col]]) <- if (is.null(obj_align(col_fmt))) "left" else obj_align(col_fmt) |
285 | 459x |
df[[col]] |
286 |
}) |
|
287 | ||
288 |
# Check and set align_colnames |
|
289 | 77x |
checkmate::assert_flag(align_colnames) |
290 | 77x |
align_colnames(df) <- align_colnames |
291 | ||
292 | 3x |
if (unique_rows) df <- df[!duplicated(df[, cols]), ] |
293 | ||
294 | 77x |
class(df) <- c("listing_df", class(df)) |
295 | ||
296 |
## these all work even when the value is NULL |
|
297 | 77x |
main_title(df) <- main_title |
298 | 77x |
main_footer(df) <- main_footer |
299 | 77x |
subtitles(df) <- subtitles |
300 | 77x |
prov_footer(df) <- prov_footer |
301 | 77x |
listing_dispcols(df) <- cols |
302 | 77x |
spanning_col_label_df(df) <- spanning_col_labels |
303 | ||
304 | 76x |
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 | 76x |
if (!is.null(add_trailing_sep)) { |
310 | 9x |
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 | 8x |
df <- .do_add_trailing_sep(df, add_trailing_sep, trailing_sep) |
318 |
} |
|
319 |
} |
|
320 | ||
321 | 74x |
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 | 14x |
if (is.character(add_trailing_sep)) { |
327 | 11x |
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 | 10x |
row_ind_for_trail_sep <- apply( |
333 | 10x |
apply(as.data.frame(df_tmp)[, add_trailing_sep, drop = FALSE], 2, function(col_i) { |
334 | 11x |
diff(as.numeric(as.factor(col_i))) |
335 |
}), |
|
336 | 10x |
1, function(row_i) any(row_i != 0) |
337 |
) %>% |
|
338 | 10x |
which() |
339 | 10x |
listing_trailing_sep(df_tmp) <- list( |
340 | 10x |
"var_trailing_sep" = add_trailing_sep, |
341 | 10x |
"where_trailing_sep" = row_ind_for_trail_sep, |
342 | 10x |
"what_to_separate" = 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_separate" = trailing_sep |
354 |
) |
|
355 |
} |
|
356 | ||
357 | 12x |
df_tmp |
358 |
} |
|
359 | ||
360 |
#' @export |
|
361 |
#' @rdname listings |
|
362 |
spanning_col_label_df <- function(df) { |
|
363 | 222x |
ret <- attr(df, "colspan_label_df") |
364 | 222x |
if (is.null(ret)) { |
365 | ! |
ret <- no_spans_df |
366 |
} |
|
367 | 222x |
ret |
368 |
} |
|
369 | ||
370 |
#' @export |
|
371 |
#' @rdname listings |
|
372 |
`spanning_col_label_df<-` <- function(df, value) { |
|
373 | 95x |
if (is.null(value)) { |
374 | ! |
value <- no_spans_df |
375 |
} |
|
376 | ||
377 | 95x |
checkmate::assert_data_frame(value, min.cols = 4, max.cols = 4, col.names = "named") |
378 | 95x |
checkmate::assert_set_equal(names(value), c("span_level", "label", "start", "span")) |
379 | 95x |
if (NROW(value)) { |
380 |
## can't have spanning labels that span across both key and non-key cols |
|
381 |
## because then what would we do after horizontal pagination??? |
|
382 |
## not clear people should *really* be horizontally paginatting listings |
|
383 |
## but we support it so here we are... |
|
384 | ||
385 | 4x |
nkeycols <- length(get_keycols(df)) |
386 | 4x |
badrow_lgl <- nzchar(value$label) & |
387 | 4x |
value$start <= nkeycols & |
388 | 4x |
value$start + value$span - 1 > nkeycols |
389 | 4x |
if (any(badrow_lgl)) { |
390 | 1x |
badrow <- value[which(badrow_lgl)[1], ] |
391 | 1x |
stop( |
392 | 1x |
"A spanning column label cannot span across both key and non-key displayed columns of a listing.\n", |
393 | 1x |
"First issue - span_level: ", badrow$span_level, |
394 | 1x |
" label: ", badrow$label, |
395 | 1x |
" start: ", badrow$start, |
396 | 1x |
"cols spanned: ", badrow$span, |
397 | 1x |
" key columns: ", nkeycols |
398 |
) |
|
399 |
} |
|
400 |
} |
|
401 | 94x |
attr(df, "colspan_label_df") <- value |
402 | 94x |
df |
403 |
} |
|
404 | ||
405 | ||
406 |
#' @export |
|
407 |
#' @rdname listings |
|
408 |
as_keycol <- function(vec) { |
|
409 | 125x |
if (is.factor(vec)) { |
410 | 31x |
lab <- obj_label(vec) |
411 | 31x |
vec <- as.character(vec) |
412 | 31x |
obj_label(vec) <- lab |
413 |
} |
|
414 | 125x |
class(vec) <- c("listing_keycol", class(vec)) |
415 | 125x |
vec |
416 |
} |
|
417 | ||
418 |
#' @export |
|
419 |
#' @rdname listings |
|
420 |
is_keycol <- function(vec) { |
|
421 | 9652x |
inherits(vec, "listing_keycol") |
422 |
} |
|
423 | ||
424 |
#' @export |
|
425 |
#' @rdname listings |
|
426 |
get_keycols <- function(df) { |
|
427 | 522x |
names(which(sapply(df, is_keycol))) |
428 |
} |
|
429 | ||
430 |
#' @inherit formatters::matrix_form |
|
431 |
#' @param indent_rownames (`flag`)\cr silently ignored, as listings do not have row names |
|
432 |
#' nor indenting structure. |
|
433 |
#' @param expand_newlines (`flag`)\cr this should always be `TRUE` for listings. We keep it |
|
434 |
#' for debugging reasons. |
|
435 |
#' |
|
436 |
#' @return a [formatters::MatrixPrintForm] object. |
|
437 |
#' |
|
438 |
#' @seealso [formatters::matrix_form()] |
|
439 |
#' |
|
440 |
#' @examples |
|
441 |
#' lsting <- as_listing(mtcars) |
|
442 |
#' mf <- matrix_form(lsting) |
|
443 |
#' |
|
444 |
#' @export |
|
445 |
setMethod( |
|
446 |
"matrix_form", "listing_df", |
|
447 |
rix_form <- function(obj, |
|
448 |
indent_rownames = FALSE, |
|
449 |
expand_newlines = TRUE, |
|
450 |
fontspec = font_spec, |
|
451 |
col_gap = 3L, |
|
452 |
round_type = c("iec", "sas")) { |
|
453 |
## we intentionally silently ignore indent_rownames because listings have |
|
454 |
## no rownames, but formatters::vert_pag_indices calls matrix_form(obj, TRUE) |
|
455 |
## unconditionally. |
|
456 | 205x |
cols <- attr(obj, "listing_dispcols") |
457 | 205x |
listing <- obj[, cols] |
458 | 205x |
atts <- attributes(obj) |
459 | 205x |
atts$names <- cols |
460 | 205x |
attributes(listing) <- atts |
461 | 205x |
keycols <- get_keycols(listing) |
462 | ||
463 | 205x |
bodymat <- matrix("", |
464 | 205x |
nrow = nrow(listing), |
465 | 205x |
ncol = ncol(listing) |
466 |
) |
|
467 | ||
468 | 205x |
colnames(bodymat) <- names(listing) |
469 | ||
470 | 205x |
curkey <- "" |
471 | 205x |
for (i in seq_along(keycols)) { |
472 | 309x |
kcol <- keycols[i] |
473 | 309x |
kcolvec <- listing[[kcol]] |
474 | 309x |
kcolvec <- vapply(kcolvec, format_value, "", |
475 | 309x |
format = obj_format(kcolvec), |
476 | 309x |
na_str = obj_na_str(kcolvec), |
477 | 309x |
round_type = round_type |
478 |
) |
|
479 | 309x |
curkey <- paste0(curkey, kcolvec) |
480 | 309x |
disp <- c(TRUE, tail(curkey, -1) != head(curkey, -1)) |
481 | 309x |
bodymat[disp, kcol] <- kcolvec[disp] |
482 |
} |
|
483 | ||
484 | 205x |
nonkeycols <- setdiff(names(listing), keycols) |
485 | 205x |
if (length(nonkeycols) > 0) { |
486 | 197x |
for (nonk in nonkeycols) { |
487 | 516x |
vec <- listing[[nonk]] |
488 | 516x |
vec <- vapply(vec, format_value, "", |
489 | 516x |
format = obj_format(vec), |
490 | 516x |
na_str = obj_na_str(vec), |
491 | 516x |
round_type = round_type |
492 |
) |
|
493 | 516x |
bodymat[, nonk] <- vec |
494 |
} |
|
495 |
} |
|
496 | ||
497 | 205x |
fullmat <- rbind( |
498 | 205x |
var_labels(listing, fill = TRUE), |
499 | 205x |
bodymat |
500 |
) |
|
501 | ||
502 | 205x |
col_alignment_values <- sapply(listing, obj_align) |
503 | 205x |
colnames_align <- if (isFALSE(align_colnames(obj))) { |
504 | 201x |
rep("center", length(cols)) |
505 |
} else { |
|
506 | 4x |
col_alignment_values |
507 |
} |
|
508 | 205x |
colaligns <- rbind( |
509 | 205x |
unname(colnames_align), |
510 | 205x |
matrix(col_alignment_values, |
511 | 205x |
ncol = length(cols), |
512 | 205x |
nrow = nrow(fullmat) - 1, |
513 | 205x |
byrow = TRUE |
514 |
) |
|
515 |
) |
|
516 | ||
517 | 205x |
if (any(grepl("([{}])", fullmat))) { |
518 | 1x |
stop( |
519 | 1x |
"Labels cannot contain { or } due to their use for indicating referential footnotes.\n", |
520 | 1x |
"These are not supported at the moment in {rlistings}." |
521 |
) |
|
522 |
} |
|
523 | ||
524 |
# trailing sep setting |
|
525 | 204x |
row_info <- make_row_df(obj, fontspec = fontspec) |
526 | 204x |
if (!is.null(listing_trailing_sep(obj))) { |
527 | 6x |
lts <- listing_trailing_sep(obj) |
528 | ||
529 |
# We need to make sure that the trailing separator is not beyond the number of rows (cases like head()) |
|
530 | 6x |
lts$where_trailing_sep <- lts$where_trailing_sep[lts$where_trailing_sep <= nrow(row_info)] |
531 | 6x |
row_info$trailing_sep[lts$where_trailing_sep] <- lts$what_to_separate |
532 |
} |
|
533 | ||
534 | 204x |
span_hdr <- make_span_hdr_mats(spanning_col_label_df(obj), length(listing_dispcols(obj))) |
535 | 204x |
span_hdr_mat <- span_hdr$strings |
536 | 204x |
span_hdr_spans <- span_hdr$spans |
537 | ||
538 | 204x |
MatrixPrintForm( |
539 | 204x |
strings = rbind( |
540 | 204x |
span_hdr_mat, |
541 | 204x |
fullmat |
542 |
), |
|
543 | 204x |
spans = rbind( |
544 | 204x |
span_hdr_spans, |
545 | 204x |
matrix(1, |
546 | 204x |
nrow = nrow(fullmat), |
547 | 204x |
ncol = ncol(fullmat) |
548 |
) |
|
549 |
), |
|
550 | 204x |
ref_fnotes = list(), |
551 | 204x |
aligns = rbind( |
552 | 204x |
matrix("center", nrow = NROW(span_hdr_mat), ncol = ncol(fullmat)), |
553 | 204x |
colaligns |
554 |
), |
|
555 | 204x |
formats = matrix(1, |
556 | 204x |
nrow = nrow(fullmat) + NROW(span_hdr_mat), |
557 | 204x |
ncol = ncol(fullmat) |
558 |
), |
|
559 | 204x |
listing_keycols = keycols, # It is always something |
560 | 204x |
row_info = row_info, |
561 | 204x |
nlines_header = 1 + nrow(span_hdr_mat), |
562 | 204x |
nrow_header = 1 + nrow(span_hdr_mat), |
563 | 204x |
has_topleft = FALSE, |
564 | 204x |
has_rowlabs = FALSE, |
565 | 204x |
expand_newlines = expand_newlines, |
566 | 204x |
main_title = main_title(obj), |
567 | 204x |
subtitles = subtitles(obj), |
568 | 204x |
page_titles = page_titles(obj), |
569 | 204x |
main_footer = main_footer(obj), |
570 | 204x |
prov_footer = prov_footer(obj), |
571 | 204x |
col_gap = col_gap, |
572 | 204x |
fontspec = fontspec, |
573 | 204x |
rep_cols = length(keycols) |
574 |
) |
|
575 |
} |
|
576 |
) |
|
577 | ||
578 |
make_span_hdr_mats <- function(spandf, ncol) { |
|
579 | 204x |
if (NROW(spandf) == 0) { |
580 | 198x |
return(list( |
581 | 198x |
strings = matrix("", ncol = ncol, nrow = 0), |
582 | 198x |
spans = matrix(1, ncol = ncol, nrow = 0) |
583 |
)) |
|
584 |
} |
|
585 | ||
586 | 6x |
spldf <- split(spandf, spandf$span_level) |
587 |
## "span_level" 1 should be directly above col lables, ie last |
|
588 | 6x |
res_mats <- rev(lapply(spldf, handle_one_lblspan_row, ncol = ncol)) |
589 | 6x |
list( |
590 | 6x |
strings = do.call( |
591 | 6x |
rbind, |
592 | 6x |
lapply(seq_along(res_mats), function(i) res_mats[[i]]$strings) |
593 |
), |
|
594 | 6x |
spans = do.call( |
595 | 6x |
rbind, |
596 | 6x |
lapply(seq_along(res_mats), function(i) res_mats[[i]]$spans) |
597 |
) |
|
598 |
) |
|
599 |
} |
|
600 | ||
601 |
handle_one_lblspan_row <- function(df, ncol) { |
|
602 | 9x |
strings <- matrix("", nrow = 1, ncol = ncol) |
603 | 9x |
spans <- matrix(1, nrow = 1, ncol = ncol) |
604 | 9x |
for (i in seq_len(nrow(df))) { |
605 | 15x |
spanlen <- df[i, "span", drop = TRUE] |
606 | 15x |
spaninds <- seq(df[i, "start", drop = TRUE], length.out = spanlen) |
607 | 15x |
strings[1, spaninds] <- df[i, "label", drop = TRUE] |
608 | 15x |
spans[1, spaninds] <- spanlen |
609 |
} |
|
610 | 9x |
list(strings = strings, spans = spans) |
611 |
} |
|
612 | ||
613 |
#' @export |
|
614 |
#' @rdname listings |
|
615 | 462x |
listing_dispcols <- function(df) attr(df, "listing_dispcols") %||% character() |
616 | ||
617 |
#' @param new (`character`)\cr vector of names of columns to be added to |
|
618 |
#' the set of display columns. |
|
619 |
#' |
|
620 |
#' @export |
|
621 |
#' @rdname listings |
|
622 |
add_listing_dispcol <- function(df, new) { |
|
623 | 25x |
listing_dispcols(df) <- c(listing_dispcols(df), new) |
624 | 25x |
df |
625 |
} |
|
626 | ||
627 |
#' @param value (`string`)\cr new value. |
|
628 |
#' |
|
629 |
#' @export |
|
630 |
#' @rdname listings |
|
631 |
`listing_dispcols<-` <- function(df, value) { |
|
632 | 102x |
if (!is.character(value)) { |
633 | ! |
stop( |
634 | ! |
"dispcols must be a character vector of column names, got ", |
635 | ! |
"object of class: ", paste(class(value), collapse = ",") |
636 |
) |
|
637 |
} |
|
638 | 102x |
chk <- setdiff(value, names(df)) ## remember setdiff is not symmetrical |
639 | 102x |
if (length(chk) > 0) { |
640 | ! |
stop( |
641 | ! |
"listing display columns must be columns in the underlying data. ", |
642 | ! |
"Column(s) ", paste(chk, collapse = ", "), " not present in the data." |
643 |
) |
|
644 |
} |
|
645 | 102x |
attr(df, "listing_dispcols") <- unique(value) |
646 | 102x |
df |
647 |
} |
|
648 | ||
649 |
#' @export |
|
650 |
#' @rdname listings |
|
651 | 205x |
align_colnames <- function(df) attr(df, "align_colnames") %||% FALSE |
652 | ||
653 |
#' @param value (`string`)\cr new value. |
|
654 |
#' |
|
655 |
#' @export |
|
656 |
#' @rdname listings |
|
657 |
`align_colnames<-` <- function(df, value) { |
|
658 | 80x |
checkmate::assert_flag(value) |
659 | 80x |
attr(df, "align_colnames") <- value |
660 | 80x |
df |
661 |
} |
|
662 | ||
663 |
#' @keywords internal |
|
664 | 221x |
listing_trailing_sep <- function(df) attr(df, "listing_trailing_sep") %||% NULL |
665 | ||
666 |
# xxx @param value (`list`)\cr List of names or rows to be separated and their separator. |
|
667 |
#' |
|
668 |
#' @keywords internal |
|
669 |
`listing_trailing_sep<-` <- function(df, value) { |
|
670 | 14x |
checkmate::assert_list(value, len = 3, null.ok = TRUE) |
671 | 14x |
if (is.null(value)) { |
672 | ! |
attr(df, "listing_trailing_sep") <- NULL |
673 | ! |
return(df) |
674 |
} |
|
675 | 14x |
checkmate::assert_set_equal( |
676 | 14x |
names(value), |
677 | 14x |
c("var_trailing_sep", "where_trailing_sep", "what_to_separate") |
678 |
) |
|
679 | 14x |
attr(df, "listing_trailing_sep") <- value |
680 | 14x |
df |
681 |
} |
|
682 | ||
683 |
#' @inheritParams formatters::fmt_config |
|
684 |
#' @param name (`string`)\cr name of the existing or new column to be |
|
685 |
#' displayed when the listing is rendered. |
|
686 |
#' @param fun (`function` or `NULL`)\cr a function which accepts `df` and |
|
687 |
#' returns the vector for a new column, which is added to `df` as |
|
688 |
#' `name`, or `NULL` if marking an existing column as a listing column. |
|
689 |
#' |
|
690 |
#' @return `df` with `name` created (if necessary) and marked for |
|
691 |
#' display during rendering. |
|
692 |
#' |
|
693 |
#' @export |
|
694 |
#' @rdname listings |
|
695 |
add_listing_col <- function(df, |
|
696 |
name, |
|
697 |
fun = NULL, |
|
698 |
format = NULL, |
|
699 |
na_str = "NA", |
|
700 |
align = "left") { |
|
701 | 26x |
if (class(df)[1] == "list") { |
702 | 1x |
out <- lapply( |
703 | 1x |
df, add_listing_col, |
704 | 1x |
name = name, fun = fun, format = format, na_str = na_str, align = align |
705 |
) |
|
706 | 1x |
return(out) |
707 |
} |
|
708 | ||
709 | 25x |
if (!is.null(fun)) { |
710 | 1x |
vec <- with_label(fun(df), name) |
711 | 24x |
} else if (name %in% names(df)) { |
712 | 24x |
vec <- df[[name]] |
713 |
} else { |
|
714 | ! |
stop( |
715 | ! |
"Column '", name, "' not found. name argument must specify an existing column when ", |
716 | ! |
"no generating function (fun argument) is specified." |
717 |
) |
|
718 |
} |
|
719 | ||
720 | 25x |
if (!is.null(format)) { |
721 | 14x |
obj_format(vec) <- format |
722 |
} |
|
723 | ||
724 | 25x |
obj_na_str(vec) <- na_str |
725 | 25x |
obj_align(vec) <- align |
726 | ||
727 |
## this works for both new and existing columns |
|
728 | 25x |
df[[name]] <- vec |
729 | 25x |
df <- add_listing_dispcol(df, name) |
730 | 25x |
df |
731 |
} |
|
732 | ||
733 |
#' Split Listing by Values of a Variable |
|
734 |
#' |
|
735 |
#' @description `r lifecycle::badge("experimental")` |
|
736 |
#' |
|
737 |
#' Split is performed based on unique values of the given parameter present in the listing. |
|
738 |
#' Each listing can only be split by variable once. If this function is applied prior to |
|
739 |
#' pagination, parameter values will be separated by page. |
|
740 |
#' |
|
741 |
#' @param lsting (`listing_df`)\cr the listing to split. |
|
742 |
#' @param var (`string`)\cr name of the variable to split on. If the column is a factor, |
|
743 |
#' the resulting list follows the order of the levels. |
|
744 |
#' @param page_prefix (`string`)\cr prefix to be appended with the split value (`var` level), |
|
745 |
#' at the end of the subtitles, corresponding to each resulting list element (listing). |
|
746 |
#' |
|
747 |
#' @return A list of `lsting_df` objects each corresponding to a unique value of `var`. |
|
748 |
#' |
|
749 |
#' @note This function should only be used after the complete listing has been created. The |
|
750 |
#' listing cannot be modified further after applying this function. |
|
751 |
#' |
|
752 |
#' @examples |
|
753 |
#' dat <- ex_adae[1:20, ] |
|
754 |
#' |
|
755 |
#' lsting <- as_listing( |
|
756 |
#' dat, |
|
757 |
#' key_cols = c("USUBJID", "AGE"), |
|
758 |
#' disp_cols = "SEX", |
|
759 |
#' main_title = "title", |
|
760 |
#' main_footer = "footer" |
|
761 |
#' ) %>% |
|
762 |
#' add_listing_col("BMRKR1", format = "xx.x") %>% |
|
763 |
#' split_into_pages_by_var("SEX") |
|
764 |
#' |
|
765 |
#' lsting |
|
766 |
#' |
|
767 |
#' @export |
|
768 |
split_into_pages_by_var <- function(lsting, var, page_prefix = var) { |
|
769 | 10x |
checkmate::assert_class(lsting, "listing_df") |
770 | 9x |
checkmate::assert_choice(var, names(lsting)) |
771 | ||
772 |
# Pre-processing in case of factor variable |
|
773 | 9x |
levels_or_vals <- if (is.factor(lsting[[var]])) { |
774 | 8x |
lvls <- levels(lsting[[var]]) |
775 | 8x |
lvls[lvls %in% unique(lsting[[var]])] # Filter out missing values |
776 |
} else { |
|
777 | 1x |
unique(lsting[[var]]) |
778 |
} |
|
779 | ||
780 |
# Main list creator (filters rows by var) |
|
781 | 9x |
lsting_by_var <- list() |
782 | 9x |
for (lvl in levels_or_vals) { |
783 | 18x |
var_desc <- paste0(page_prefix, ": ", lvl) |
784 | 18x |
lsting_by_var[[lvl]] <- lsting[lsting[[var]] == lvl, ] |
785 | 18x |
subtitles(lsting_by_var[[lvl]]) <- c(subtitles(lsting), var_desc) |
786 | 18x |
spanning_col_label_df(lsting_by_var[[lvl]]) <- spanning_col_label_df(lsting) |
787 |
} |
|
788 | ||
789 |
# Correction for cases with trailing separators |
|
790 | 9x |
trailing_sep_directives <- listing_trailing_sep(lsting) |
791 | 9x |
if (!is.null(trailing_sep_directives)) { |
792 | 3x |
if (is.null(trailing_sep_directives$var_trailing_sep)) { |
793 | 1x |
stop( |
794 | 1x |
"Current lsting did have add_trailing_sep directives with numeric indexes. ", |
795 | 1x |
"This is not supported for split_into_pages_by_var. Please use the <var> method." |
796 |
) |
|
797 |
} |
|
798 | 2x |
add_trailing_sep <- trailing_sep_directives$var_trailing_sep |
799 | 2x |
trailing_sep <- trailing_sep_directives$what_to_separate |
800 | 2x |
lsting_by_var <- lapply(lsting_by_var, .do_add_trailing_sep, add_trailing_sep, trailing_sep) |
801 |
} |
|
802 | ||
803 | 8x |
lsting_by_var |
804 |
} |
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 | 204x |
n <- length(x) |
72 | 204x |
if (n == 0) { |
73 | ! |
return(integer()) |
74 |
} |
|
75 | ||
76 | 204x |
y <- x[-1L] != x[-n] |
77 | 204x |
i <- c(which(y), n) |
78 | 204x |
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 | 824x |
if (missing(colvec) && !(colnm %in% names(df))) { |
89 | ! |
stop("column ", colnm, " not found") |
90 |
} |
|
91 | 824x |
na_str <- obj_na_str(colvec) |
92 | 824x |
if (is.null(na_str) || all(is.na(na_str))) { |
93 | ! |
na_str <- rep("-", max(1L, length(na_str))) |
94 |
} |
|
95 | ||
96 | 824x |
strvec <- vapply(colvec, format_value, "", format = obj_format(colvec), na_str = na_str, round_type = round_type) |
97 | 824x |
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 | 824x |
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 | 824x |
round_type <- match.arg(round_type) |
121 | 824x |
if (is.null(max_width)) { |
122 | 824x |
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 | 824x |
unlist(lapply(format_colvector(colvec = vec, round_type = round_type), nlines, |
127 | 824x |
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 | 205x |
keycols <- get_keycols(tt) |
176 | 205x |
dispcols <- listing_dispcols(tt) |
177 | 205x |
abs_rownumber <- seq_along(tt[[1]]) |
178 | 205x |
if (length(keycols) >= 1) { |
179 | 204x |
runlens <- basic_run_lens(tt[[tail(keycols, 1)]]) |
180 |
} else { |
|
181 | 1x |
runlens <- rep(1, NROW(tt)) |
182 |
} |
|
183 | 205x |
sibpos <- unlist(lapply(runlens, seq_len)) |
184 | 205x |
nsibs <- rep(runlens, times = runlens) |
185 | 205x |
extents <- rep(1L, nrow(tt)) |
186 | 205x |
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 | 205x |
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 | 205x |
for (col in dispcols) { |
198 |
## duplicated from matrix_form method, refactor! |
|
199 | 824x |
col_ext <- vec_nlines(tt[[col]], max_width = colwidths[col], fontspec = fontspec, round_type = round_type) |
200 | 824x |
extents <- ifelse(col_ext > extents, col_ext, extents) |
201 |
} |
|
202 | 205x |
ret <- data.frame( |
203 | 205x |
label = "", name = "", |
204 | 205x |
abs_rownumber = abs_rownumber, |
205 | 205x |
path = I(as.list(rep(NA_character_, NROW(tt)))), |
206 | 205x |
pos_in_siblings = sibpos, |
207 | 205x |
n_siblings = nsibs, |
208 | 205x |
self_extent = extents, |
209 | 205x |
par_extent = 0L, |
210 | 205x |
reprint_inds = I(replicate(NROW(tt), list(integer()))), |
211 | 205x |
node_class = "listing_df", |
212 | 205x |
indent = 0L, |
213 | 205x |
nrowrefs = 0L, ## XXX this doesn't support footnotes |
214 | 205x |
ncellrefs = 0L, ## XXX this doesn't support footnotes |
215 | 205x |
nreflines = 0L, ## XXX this doesn't support footnotes |
216 | 205x |
force_page = FALSE, |
217 | 205x |
page_title = NA_character_, |
218 | 205x |
trailing_sep = NA_character_ |
219 |
) |
|
220 | 205x |
stopifnot(identical( |
221 | 205x |
names(ret), |
222 | 205x |
names(pagdfrow( |
223 | 205x |
nm = "", lab = "", rnum = 1L, pth = NA_character_, extent = 1L, |
224 | 205x |
rclass = "" |
225 |
)) |
|
226 |
)) |
|
227 | 205x |
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 | 207x |
function(obj) attr(obj, "main_title") %||% character() |
304 |
) |
|
305 | ||
306 |
#' @rdname listing_methods |
|
307 |
#' @export |
|
308 |
setMethod( |
|
309 |
"subtitles", "listing_df", |
|
310 | 225x |
function(obj) attr(obj, "subtitles") %||% character() |
311 |
) |
|
312 | ||
313 |
#' @rdname listing_methods |
|
314 |
#' @export |
|
315 |
setMethod( |
|
316 |
"main_footer", "listing_df", |
|
317 | 206x |
function(obj) attr(obj, "main_footer") %||% character() |
318 |
) |
|
319 | ||
320 |
#' @rdname listing_methods |
|
321 |
#' @export |
|
322 |
setMethod( |
|
323 |
"prov_footer", "listing_df", |
|
324 | 206x |
function(obj) attr(obj, "prov_footer") %||% character() |
325 |
) |
|
326 | ||
327 |
.chk_value <- function(val, fname, len_one = FALSE, null_ok = TRUE) { |
|
328 | 342x |
if (null_ok && is.null(val)) { |
329 | 285x |
return(TRUE) |
330 |
} |
|
331 | 57x |
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 | 53x |
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 | 52x |
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 | 83x |
.chk_value(value, "main_title", len_one = TRUE) |
354 | 81x |
attr(obj, "main_title") <- value |
355 | 81x |
obj |
356 |
} |
|
357 |
) |
|
358 | ||
359 |
#' @rdname listing_methods |
|
360 |
#' @export |
|
361 |
setMethod( |
|
362 |
"subtitles<-", "listing_df", |
|
363 |
function(obj, value) { |
|
364 | 98x |
.chk_value(value, "subtitles") |
365 | 97x |
attr(obj, "subtitles") <- value |
366 | 97x |
obj |
367 |
} |
|
368 |
) |
|
369 | ||
370 |
#' @rdname listing_methods |
|
371 |
#' @export |
|
372 |
setMethod( |
|
373 |
"main_footer<-", "listing_df", |
|
374 |
function(obj, value) { |
|
375 | 81x |
.chk_value(value, "main_footer") |
376 | 80x |
attr(obj, "main_footer") <- value |
377 | 80x |
obj |
378 |
} |
|
379 |
) |
|
380 | ||
381 |
#' @rdname listing_methods |
|
382 |
#' @export |
|
383 |
setMethod( |
|
384 |
"prov_footer<-", "listing_df", |
|
385 |
function(obj, value) { |
|
386 | 80x |
.chk_value(value, "prov_footer") |
387 | 79x |
attr(obj, "prov_footer") <- value |
388 | 79x |
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 |
} |