diff --git a/DESCRIPTION b/DESCRIPTION index 04b5dcf41..9a87c75ee 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -29,10 +29,10 @@ URL: https://github.com/insightsengineering/rtables, https://insightsengineering.github.io/rtables/ BugReports: https://github.com/insightsengineering/rtables/issues Depends: - formatters (>= 0.5.6), + R (>= 2.10), + formatters (>= 0.5.7.9000), magrittr (>= 1.5), - methods, - R (>= 2.10) + methods Imports: checkmate (>= 2.1.0), htmltools (>= 0.5.4), diff --git a/NEWS.md b/NEWS.md index d4347a482..ec3947d1e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,13 @@ * Added `top_level_section_div` for `basic_table` to set section dividers for top level rows. * Added `keep_label_rows` to `as_result_df` to have these lines visible. * `sort_at_path` now gives informative error messages when the given path does not exist. + * Add support for truetype fonts based on formatters `>= 0.5.6.9007`. Nearly all functions related to pagination or export now accept `fontspec` argument and pass it around accordingly, by @gmbecker. + * Core splitting machinery can now be overridden in column space via `make_split_fun` provided that `core_split` associates the generated facets with subsetting expressions. Subsetting expressions remain unnecessary for splits in row space. By @gmbecker. + * ValueWrapper objects now carry around subsetting expressions for use during tabulation, by @gmbecker. + * `make_split_res`, `add_to_split_result` now accept a list of subsetting expressions which will be attached to the values, by @gmbecker. + * New `value_expr` internal getter and setter methods, by @gmbecker. + + ### Bug Fixes * Fixed `rlistings` decoration (e.g. titles and footers) expansion when there are new lines. Moved relevant handling from `rtables`' `matrix_form` function to `formatters`' dedicated `mform_handle_newlines` function. @@ -12,10 +19,13 @@ * Fixed `section_div` for analysis of multiple variables (`AnalyzeMultiVars`). * Fixed mismatch between indentation declared in row info (`mf_rinfo(mf)`) and actual selected indentation from `matrix_form(mf, indent_rownames = FALSE)`. * Fixed bug in `as_html` preventing indentation from being applied in `Viewer` output. + * `col_counts<-` and `col_total<-` methods now explicitly convert `value` to integer, by @gmbecker. + * `col_gap` is now respected in `nlines` row methods, and thus by `make_row_df`, by @gmbecker. + ### Miscellaneous * Removed deprecated functions `add_analyzed_var` and `trim_zero_rows`. - * Added `lifecycle` badge files for documentation. + * Added `lifecycle` badge files for deprecated documentation. * Deprecated the `gap` and `check_headers` arguments to `rbindl_rtables` using `lifecycle`. ## rtables 0.6.6 diff --git a/R/00tabletrees.R b/R/00tabletrees.R index d80ad5421..db8d04f6f 100644 --- a/R/00tabletrees.R +++ b/R/00tabletrees.R @@ -64,10 +64,12 @@ setClassUnion("functionOrNULL", c("NULL", "function")) setClassUnion("listOrNULL", c("NULL", "list")) ## TODO (?) make "list" more specific, e.g FormatList, or FunctionList? setClassUnion("FormatSpec", c("NULL", "character", "function", "list")) +setClassUnion("ExprOrNULL", c("NULL", "expression")) setClass("ValueWrapper", representation( value = "ANY", - label = "characterOrNULL" + label = "characterOrNULL", + subset_expression = "ExprOrNULL" ), contains = "VIRTUAL" ) @@ -80,7 +82,7 @@ setClass("SplitValue", representation(extra = "list") ) -SplitValue <- function(val, extr = list(), label = val) { +SplitValue <- function(val, extr = list(), label = val, sub_expr = NULL) { if (is(val, "SplitValue")) { if (length(splv_extra(val)) > 0) { extr <- c(splv_extra(val), extr) @@ -94,10 +96,16 @@ SplitValue <- function(val, extr = list(), label = val) { if (!is(label, "character")) { label <- as.character(label) } + + if (!is.null(sub_expr) && !is.expression(sub_expr)) { + sub_expr <- as.expression(sub_expr) + } ## sometimes they will be "call" objects, etc check_ok_label(label) new("SplitValue", value = val, - extra = extr, label = label + extra = extr, + label = label, + subset_expression = sub_expr ) } @@ -107,13 +115,14 @@ setClass("LevelComboSplitValue", ) ## wrapped in user-facing `add_combo_facet` -LevelComboSplitValue <- function(val, extr, combolevels, label = val) { +LevelComboSplitValue <- function(val, extr, combolevels, label = val, sub_expr = NULL) { check_ok_label(label) new("LevelComboSplitValue", value = val, extra = extr, combolevels = combolevels, - label = label + label = label, + subset_expression = sub_expr ) } @@ -955,7 +964,7 @@ TreePos <- function(spls = list(), svlabels = character(), sub = NULL) { check_ok_label(svlabels, multi_ok = TRUE) - svals <- make_splvalue_vec(vals = svals) + svals <- make_splvalue_vec(vals = svals, subset_exprs = lapply(svals, value_expr)) if (is.null(sub)) { if (length(spls) > 0) { sub <- make_pos_subset( @@ -992,6 +1001,7 @@ make_child_pos <- function(parpos, svlabels = c(pos_splval_labels(parpos), newlab), sub = .combine_subset_exprs( pos_subset(parpos), + ## this will grab the value's custom subset expression if present make_subset_expr(newspl, nsplitval) ) ) diff --git a/R/make_split_fun.R b/R/make_split_fun.R index e1d97384d..aff197de1 100644 --- a/R/make_split_fun.R +++ b/R/make_split_fun.R @@ -81,6 +81,8 @@ validate_split_result <- function(pinfo, component = NULL) { #' @param labels (`character`)\cr the labels associated with each facet. #' @param extras (`list` or `NULL`)\cr extra values associated with each of the facets which will be passed to #' analysis functions applied within the facet. +#' @param subset_exprs (`list`)\cr A list of subsetting expressions (e.g., +#' created with `quote()`) to be used during column subsetting. #' #' @return A named list representing the facets generated by the split with elements `values`, `datasplit`, and #' `labels`, which are the same length and correspond to each other element-wise. @@ -89,27 +91,37 @@ validate_split_result <- function(pinfo, component = NULL) { #' These functions performs various housekeeping tasks to ensure that the split result list is as the rtables #' internals expect it, most of which are not relevant to end users. #' +#' @note Column splitting will not work correctly if a split function +#' calls `make_split_result` without specifying subset expressions; +#' row splitting will work as normal. This is due to the fact that +#' subsetting expressions are used during column splitting to +#' represent the data associated with facets, while actual data +#' subsets are used during row splitting. +#' #' @examples #' splres <- make_split_result( #' values = c("hi", "lo"), #' datasplit = list(hi = mtcars, lo = mtcars[1:10, ]), -#' labels = c("more data", "less data") +#' labels = c("more data", "less data"), +#' subset_exprs = list(expression(TRUE), expression(seq_along(wt) <= 10)) #' ) #' #' splres2 <- add_to_split_result(splres, #' values = "med", #' datasplit = list(med = mtcars[1:20, ]), -#' labels = "kinda some data" +#' labels = "kinda some data", +#' subset_exprs = quote(seq_along(wt) <= 20) #' ) #' #' @family make_custom_split #' @rdname make_split_result #' @export -make_split_result <- function(values, datasplit, labels, extras = NULL) { +#' @family make_custom_split +make_split_result <- function(values, datasplit, labels, extras = NULL, subset_exprs = vector("list", length(values))) { if (length(values) == 1 && is(datasplit, "data.frame")) { datasplit <- list(datasplit) } - ret <- list(values = values, datasplit = datasplit, labels = labels) + ret <- list(values = values, datasplit = datasplit, labels = labels, subset_exprs = subset_exprs) if (!is.null(extras)) { ret$extras <- extras } @@ -120,9 +132,9 @@ make_split_result <- function(values, datasplit, labels, extras = NULL) { #' #' @rdname make_split_result #' @export -add_to_split_result <- function(splres, values, datasplit, labels, extras = NULL) { +add_to_split_result <- function(splres, values, datasplit, labels, extras = NULL, subset_exprs = NULL) { validate_split_result(splres) - newstuff <- make_split_result(values, datasplit, labels, extras) + newstuff <- make_split_result(values, datasplit, labels, extras, subset_exprs = list(subset_exprs)) ret <- lapply( names(splres), function(nm) c(splres[[nm]], newstuff[[nm]]) @@ -131,6 +143,7 @@ add_to_split_result <- function(splres, values, datasplit, labels, extras = NULL .fixupvals(ret) } + .can_take_spl_context <- function(f) any(c(".spl_context", "...") %in% names(formals(f))) #' Create a custom splitting function @@ -161,15 +174,17 @@ add_to_split_result <- function(splres, values, datasplit, labels, extras = NULL #' The preprocessing component is useful for things such as manipulating factor levels, e.g., to trim unobserved ones #' or to reorder levels based on observed counts, etc. #' -#' Customization of core splitting (2) is currently only supported in row splits. Core splitting functions override the -#' fundamental splitting procedure, and are only necessary in rare cases. These must accept `spl`, `df`, `vals`, and -#' `labels`, and can optionally accept `.spl_context`. They must return a named list with elements, all of the same -#' length, as follows: +#' Core splitting functions override the fundamental +#' splitting procedure, and are only necessary in rare cases. These +#' must accept `spl`, `df`, `vals`, `labels`, and can optionally +#' accept `.spl_context`. They should return a split result object +#' constructed via `make_split_result()`. +#' +#' In particular, if the custom split function will be used in +#' column space, subsetting expressions (e.g., as returned by +#' `quote()` or `bquote` must be provided, while they are +#' optional (and largely ignored, currently) in row space. #' -#' - `datasplit`, containing a list of `data.frame` objects. -#' - `values`, containing values associated with the facets, which must be `character` or `SplitValue` objects. -#' These values will appear in the paths of the resulting table. -#' - `labels`, containing the character labels associated with `values` #' #' Post-processing functions (3) must accept the result of the core split as their first argument (which can be #' anything), in addition to `spl`, and `fulldf`, and can optionally accept `.spl_context`. They must each return a @@ -275,14 +290,9 @@ make_split_fun <- function(pre = list(), core_split = NULL, post = list()) { if (is.null(core_split)) { ret <- do_base_split(spl = spl, df = df, vals = vals, labels = labels) - } else if (!in_col_split(.spl_context)) { + } else { ret <- core_split(spl = spl, df = df, vals = vals, labels = labels, .spl_context) validate_split_result(ret, component = "core_split") - } else { - stop( - "Use of custom split functions which override core splitting ", - "behavior is not currently supported in column space." - ) } for (post_fn in post) { @@ -334,14 +344,49 @@ make_split_fun <- function(pre = list(), core_split = NULL, post = list()) { #' @export add_combo_facet <- function(name, label = name, levels, extra = list()) { function(ret, spl, .spl_context, fulldf) { - val <- LevelComboSplitValue(val = name, extr = extra, combolevels = levels, label = label) + if (is(levels, "AllLevelsSentinel")) { + subexpr <- expression(TRUE) + datpart <- list(fulldf) + } else { + subexpr <- .combine_value_exprs(ret$value[levels]) + datpart <- list(do.call(rbind, ret$datasplit[levels])) + } + + val <- LevelComboSplitValue( + val = name, extr = extra, combolevels = levels, label = label, + sub_expr = subexpr + ) add_to_split_result(ret, values = list(val), labels = label, - datasplit = list(do.call(rbind, ret$datasplit[levels])) + datasplit = datpart ) } } +.combine_value_exprs <- function(val_lst, spl) { + exprs <- lapply(val_lst, value_expr) + nulls <- vapply(exprs, is.null, TRUE) + if (all(nulls)) { + return(NULL) # default behavior all the way down the line, no need to do anything. + } else if (any(nulls)) { + exprs[nulls] <- lapply(val_lst[nulls], function(vali) make_subset_expr(spl, vali)) + } + Reduce(.or_combine_exprs, exprs) +} + +## no NULLS coming in here, everything has been populated +## by either custom subsetting expressions or the result of make_subset_expr(spl, val) +.or_combine_exprs <- function(ex1, ex2) { + if (identical(ex1, expression(FALSE))) { + return(ex2) + } else if (identical(ex2, expression(FALSE))) { + return(ex1) + } else if (identical(ex1, expression(TRUE)) || identical(ex2, expression(TRUE))) { + return(TRUE) + } + as.expression(bquote((.(a)) | .(b), list(a = ex1[[1]], b = ex2[[1]]))) +} + #' @rdname add_combo_facet #' @export add_overall_facet <- function(name, label, extra = list()) { diff --git a/R/make_subset_expr.R b/R/make_subset_expr.R index 71bd2cc46..0392df010 100644 --- a/R/make_subset_expr.R +++ b/R/make_subset_expr.R @@ -7,6 +7,12 @@ setGeneric("make_subset_expr", function(spl, val) standardGeneric("make_subset_e setMethod( "make_subset_expr", "VarLevelSplit", function(spl, val) { + ## this is how custom split functions will communicate the correct expression + ## to the column modeling code + if (length(value_expr(val)) > 0) { + return(value_expr(val)) + } + v <- unlist(rawvalues(val)) ## XXX if we're including all levels should even missing be included? if (is(v, "AllLevelsSentinel")) { @@ -23,6 +29,12 @@ setMethod( setMethod( "make_subset_expr", "MultiVarSplit", function(spl, val) { + ## this is how custom split functions will communicate the correct expression + ## to the column modeling code + if (length(value_expr(val)) > 0) { + return(value_expr(val)) + } + ## v = rawvalues(val) ## as.expression(bquote(!is.na(.(a)), list(a = v))) expression(TRUE) @@ -139,6 +151,10 @@ setMethod( return(expression(TRUE)) } } + + if (is.null(ex2)) { + ex2 <- expression(TRUE) + } stopifnot(is.expression(ex1), is.expression(ex2)) as.expression(bquote((.(a)) & .(b), list(a = ex1[[1]], b = ex2[[1]]))) } @@ -240,7 +256,7 @@ create_colinfo <- function(lyt, df, rtpos = TreePos(), if (identical(ex, expression(TRUE))) { nrow(alt_counts_df) } else if (identical(ex, expression(FALSE))) { - 0 + 0L } else { vec <- try(eval(ex, envir = alt_counts_df), silent = TRUE) if (is(vec, "try-error")) { @@ -263,6 +279,7 @@ create_colinfo <- function(lyt, df, rtpos = TreePos(), } }) counts[calcpos] <- calccounts[calcpos] + counts <- as.integer(counts) if (is.null(total)) { total <- sum(counts) } diff --git a/R/split_funs.R b/R/split_funs.R index cc60197cf..e7ea1c778 100644 --- a/R/split_funs.R +++ b/R/split_funs.R @@ -144,6 +144,14 @@ NULL } } + subsets <- partinfo$subset_exprs + if (is.null(subsets)) { + subsets <- vector(mode = "list", length = length(vals)) + ## use labels here cause we already did all that work + ## to get the names on the labels vector right + names(subsets) <- names(labels) + } + if (is.null(vals) && !is.null(extr)) { vals <- seq_along(extr) } @@ -173,7 +181,7 @@ NULL if (is.null(extr)) { extr <- rep(list(list()), length(vals)) } - vals <- make_splvalue_vec(vals, extr, labels = labels) + vals <- make_splvalue_vec(vals, extr, labels = labels, subset_exprs = subsets) } ## we're done with this so take it off partinfo$extras <- NULL @@ -675,7 +683,8 @@ setMethod( function(spl, df, vals, labels) value_labels(spl) ) -make_splvalue_vec <- function(vals, extrs = list(list()), labels = vals) { +make_splvalue_vec <- function(vals, extrs = list(list()), labels = vals, + subset_exprs) { if (length(vals) == 0) { return(vals) } @@ -691,6 +700,7 @@ make_splvalue_vec <- function(vals, extrs = list(list()), labels = vals) { mapply(SplitValue, val = vals, extr = extrs, label = labels, + sub_expr = subset_exprs, SIMPLIFY = FALSE ) } diff --git a/R/tree_accessors.R b/R/tree_accessors.R index 312f4fb1f..b31b13e4b 100644 --- a/R/tree_accessors.R +++ b/R/tree_accessors.R @@ -1761,6 +1761,15 @@ setMethod( #' @rdname int_methods setMethod("value_labels", "MultiVarSplit", function(obj) obj@var_labels) +#' @rdname int_methods +setGeneric("value_expr", function(obj) standardGeneric("value_expr")) +#' @rdname int_methods +setMethod("value_expr", "ValueWrapper", function(obj) obj@subset_expression) +#' @rdname int_methods +setMethod("value_expr", "ANY", function(obj) NULL) +## no setters for now, we'll see about that. + + #' @rdname int_methods setGeneric("spl_varlabels", function(obj) standardGeneric("spl_varlabels")) @@ -2167,7 +2176,8 @@ setGeneric("col_counts<-", function(obj, path = NULL, value) standardGeneric("co setMethod( "col_counts<-", "InstantiatedColumnInfo", function(obj, path = NULL, value) { - obj@counts[.path_to_pos(path, obj, cols = TRUE)] <- value + ## all methods funnel to this one so ensure integer-ness here. + obj@counts[.path_to_pos(path, obj, cols = TRUE)] <- as.integer(value) obj } ) @@ -2211,7 +2221,8 @@ setGeneric("col_total<-", function(obj, value) standardGeneric("col_total<-")) setMethod( "col_total<-", "InstantiatedColumnInfo", function(obj, value) { - obj@total_count <- value + ## all methods funnel to this one so ensure integer-ness here. + obj@total_count <- as.integer(value) obj } ) diff --git a/R/tt_dotabulation.R b/R/tt_dotabulation.R index 903f12eb4..64bd620b7 100644 --- a/R/tt_dotabulation.R +++ b/R/tt_dotabulation.R @@ -1132,7 +1132,7 @@ recursive_applysplit <- function(df, #' #' @inheritParams gen_args #' @inheritParams lyt_args -#' @param col_counts (`numeric` or `NULL`)\cr if non-`NULL`, column counts +#' @param col_counts (`numeric` or `NULL`)\cr `r lifecycle::badge("deprecated")` if non-`NULL`, column counts #' which override those calculated automatically during tabulation. Must specify "counts" for *all* #' resulting columns if non-`NULL`. `NA` elements will be replaced with the automatically calculated counts. #' @param col_total (`integer(1)`)\cr the total observations across all columns. Defaults to `nrow(df)`. @@ -1588,12 +1588,16 @@ splitvec_to_coltree <- function(df, splvec, pos = NULL, kids <- mapply( function(dfpart, value, partlab) { + ## we could pass subset expression in here but the spec + ## currently doesn't call for it in column space newprev <- context_df_row( split = obj_name(spl), value = value_names(value), full_parent_df = list(dfpart), cinfo = NULL ) + ## subset expressions handled inside make_child_pos, + ## value is (optionally, for the moment) carrying it around newpos <- make_child_pos(pos, spl, value, partlab) splitvec_to_coltree(dfpart, splvec, newpos, lvl + 1L, partlab, diff --git a/R/tt_paginate.R b/R/tt_paginate.R index 4a6eaa37a..d3f3ff81e 100644 --- a/R/tt_paginate.R +++ b/R/tt_paginate.R @@ -17,12 +17,9 @@ #' @exportMethod nlines setMethod( "nlines", "TableRow", - function(x, colwidths, max_width) { - ## XXX this is wrong and needs to be fixed - ## should not be hardcoded here - col_gap <- 3L - fns <- sum(unlist(lapply(row_footnotes(x), nlines, max_width = max_width))) + - sum(unlist(lapply(cell_footnotes(x), nlines, max_width = max_width))) + function(x, colwidths, max_width, fontspec, col_gap = 3) { + fns <- sum(unlist(lapply(row_footnotes(x), nlines, max_width = max_width, fontspec = fontspec))) + + sum(unlist(lapply(cell_footnotes(x), nlines, max_width = max_width, fontspec = fontspec))) fcells <- as.vector(get_formatted_cells(x)) spans <- row_cspans(x) have_cw <- !is.null(colwidths) @@ -48,9 +45,18 @@ setMethod( ## rowext <- max(vapply(strsplit(c(obj_label(x), fcells), "\n", fixed = TRUE), ## length, ## 1L)) - rowext <- max(unlist(mapply(function(s, w) { - nlines(strsplit(s, "\n", fixed = TRUE), max_width = w) - }, s = c(obj_label(x), fcells), w = (colwidths %||% max_width) %||% 1000L, SIMPLIFY = FALSE))) + rowext <- max( + unlist( + mapply( + function(s, w) { + nlines(strsplit(s, "\n", fixed = TRUE), max_width = w, fontspec = fontspec) + }, + s = c(obj_label(x), fcells), + w = (colwidths %||% max_width) %||% vector("list", length(c(obj_label(x), fcells))), + SIMPLIFY = FALSE + ) + ) + ) rowext + fns } @@ -60,10 +66,10 @@ setMethod( #' @rdname formatters_methods setMethod( "nlines", "LabelRow", - function(x, colwidths, max_width) { + function(x, colwidths, max_width, fontspec = fontspec, col_gap = NULL) { if (labelrow_visible(x)) { - nlines(strsplit(obj_label(x), "\n", fixed = TRUE)[[1]], max_width = colwidths[1]) + - sum(unlist(lapply(row_footnotes(x), nlines, max_width = max_width))) + nlines(strsplit(obj_label(x), "\n", fixed = TRUE)[[1]], max_width = colwidths[1], fontspec = fontspec) + + sum(unlist(lapply(row_footnotes(x), nlines, max_width = max_width, fontspec = fontspec))) } else { 0L } @@ -74,8 +80,8 @@ setMethod( #' @rdname formatters_methods setMethod( "nlines", "RefFootnote", - function(x, colwidths, max_width) { - nlines(format_fnote_note(x), colwidths = colwidths, max_width = max_width) + function(x, colwidths, max_width, fontspec, col_gap = NULL) { + nlines(format_fnote_note(x), colwidths = colwidths, max_width = max_width, fontspec = fontspec) } ) @@ -83,12 +89,24 @@ setMethod( #' @rdname formatters_methods setMethod( "nlines", "InstantiatedColumnInfo", - function(x, colwidths, max_width) { + function(x, colwidths, max_width, fontspec, col_gap = 3) { h_rows <- .do_tbl_h_piece2(x) tl <- top_left(x) %||% rep("", length(h_rows)) main_nls <- vapply( seq_along(h_rows), - function(i) max(nlines(h_rows[[i]], colwidths = colwidths), nlines(tl[i], colwidths = colwidths[1])), + function(i) { + max( + nlines(h_rows[[i]], + colwidths = colwidths, + fontspec = fontspec, + col_gap = col_gap + ), + nlines(tl[i], + colwidths = colwidths[1], + fontspec = fontspec + ) + ) + }, 1L ) @@ -106,7 +124,8 @@ setMethod( vapply(unlist(coldf$col_fnotes), nlines, 1, - max_width = max_width + max_width = max_width, + fontspec = fontspec ), 2 * divider_height(x) ) @@ -187,7 +206,9 @@ setMethod( repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL) { + max_width = NULL, + fontspec = NULL, + col_gap = 3) { indent <- indent + indent_mod(tt) ## retained for debugging info orig_rownum <- rownum # nolint @@ -217,7 +238,8 @@ setMethod( nsibs = nsibs, nrowrefs = 0L, ncellrefs = 0L, - nreflines = 0L + nreflines = 0L, + fontspec = fontspec )) ) } @@ -232,7 +254,8 @@ setMethod( incontent = TRUE, repr_ext = repr_ext, repr_inds = repr_inds, - max_width = max_width + max_width = max_width, + fontspec = fontspec ) rownum <- max(newdf$abs_rownumber, na.rm = TRUE) @@ -258,7 +281,8 @@ setMethod( incontent = TRUE, repr_ext = repr_ext, repr_inds = repr_inds, - max_width = max_width + max_width = max_width, + fontspec = fontspec ) crnums <- contdf$abs_rownumber crnums <- crnums[!is.na(crnums)] @@ -288,7 +312,8 @@ setMethod( repr_inds = repr_inds, nsibs = newnsibs, sibpos = i, - max_width = max_width + max_width = max_width, + fontspec = fontspec ) # print(kiddfs$abs_rownumber) @@ -322,12 +347,23 @@ setMethod( repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL) { + max_width = NULL, + fontspec, + col_gap = 3) { indent <- indent + indent_mod(tt) rownum <- rownum + 1 rrefs <- row_footnotes(tt) crefs <- cell_footnotes(tt) - reflines <- sum(sapply(c(rrefs, crefs), nlines, colwidths = colwidths, max_width = max_width)) + reflines <- sum( + sapply( + c(rrefs, crefs), + nlines, + colwidths = colwidths, + max_width = max_width, + fontspec = fontspec, + col_gap = col_gap + ) + ) ## col_gap not strictly necessary as these aren't rows, but why not ret <- pagdfrow( row = tt, rnum = rownum, @@ -338,12 +374,13 @@ setMethod( repext = repr_ext, repind = repr_inds, indent = indent, - extent = nlines(tt, colwidths = colwidths, max_width = max_width), + extent = nlines(tt, colwidths = colwidths, max_width = max_width, fontspec = fontspec, col_gap = col_gap), ## these two are unlist calls cause they come in lists even with no footnotes nrowrefs = length(rrefs), ncellrefs = length(unlist(crefs)), nreflines = reflines, - trailing_sep = trailing_section_div(tt) + trailing_sep = trailing_section_div(tt), + fontspec = fontspec ) ret } @@ -363,11 +400,13 @@ setMethod( repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL) { + max_width = NULL, + fontspec, + col_gap = 3) { rownum <- rownum + 1 indent <- indent + indent_mod(tt) ret <- pagdfrow(tt, - extent = nlines(tt, colwidths = colwidths, max_width = max_width), + extent = nlines(tt, colwidths = colwidths, max_width = max_width, fontspec = fontspec, col_gap = col_gap), rnum = rownum, colwidths = colwidths, sibpos = sibpos, @@ -380,9 +419,12 @@ setMethod( ncellrefs = 0L, nreflines = sum(vapply(row_footnotes(tt), nlines, NA_integer_, colwidths = colwidths, - max_width = max_width + max_width = max_width, + fontspec = fontspec, + col_gap = col_gap )), - trailing_sep = trailing_section_div(tt) + trailing_sep = trailing_section_div(tt), + fontspec = fontspec ) if (!labelrow_visible(tt)) { ret <- ret[0, , drop = FALSE] @@ -488,13 +530,14 @@ setMethod( ## THIS INCLUDES BOTH "table stub" (i.e. column label and top_left) AND ## title/subtitle!!!!! -.header_rep_nlines <- function(tt, colwidths, max_width, verbose = FALSE) { - cinfo_lines <- nlines(col_info(tt), colwidths = colwidths, max_width = max_width) +.header_rep_nlines <- function(tt, colwidths, max_width, fontspec, verbose = FALSE) { + cinfo_lines <- nlines(col_info(tt), colwidths = colwidths, max_width = max_width, fontspec = fontspec) if (any(nzchar(all_titles(tt)))) { ## +1 is for blank line between subtitles and divider tlines <- sum(nlines(all_titles(tt), colwidths = colwidths, - max_width = max_width + max_width = max_width, + fontspec = fontspec )) + divider_height(tt) + 1L } else { tlines <- 0 @@ -512,12 +555,13 @@ setMethod( ## this is ***only*** lines that are expected to be repeated on multiple pages: ## main footer, prov footer, and referential footnotes on **columns** -.footer_rep_nlines <- function(tt, colwidths, max_width, have_cfnotes, verbose = FALSE) { +.footer_rep_nlines <- function(tt, colwidths, max_width, have_cfnotes, fontspec, verbose = FALSE) { flines <- nlines(main_footer(tt), colwidths = colwidths, - max_width = max_width - table_inset(tt) + max_width = max_width - table_inset(tt), + fontspec = fontspec ) + - nlines(prov_footer(tt), colwidths = colwidths, max_width = max_width) + nlines(prov_footer(tt), colwidths = colwidths, max_width = max_width, fontspec = fontspec) if (flines > 0) { dl_contrib <- if (have_cfnotes) 0 else divider_height(tt) flines <- flines + dl_contrib + 1L @@ -635,6 +679,8 @@ pag_tt_indices <- function(tt, nosplitin = character(), colwidths = NULL, max_width = NULL, + fontspec = NULL, + col_gap = 3, verbose = FALSE) { dheight <- divider_height(tt) @@ -644,7 +690,8 @@ pag_tt_indices <- function(tt, hlines <- .header_rep_nlines(tt, colwidths = colwidths, max_width = max_width, - verbose = verbose + verbose = verbose, + fontspec = fontspec ) ## if(any(nzchar(all_titles(tt)))) { ## tlines <- sum(nlines(all_titles(tt), colwidths = colwidths, max_width = max_width)) + @@ -661,8 +708,11 @@ pag_tt_indices <- function(tt, ## flines <- flines + dl_contrib + 1L ## } flines <- .footer_rep_nlines(tt, - colwidths = colwidths, max_width = max_width, - have_cfnotes = have_cfnotes, verbose = verbose + colwidths = colwidths, + max_width = max_width, + have_cfnotes = have_cfnotes, + fontspec = fontspec, + verbose = verbose ) ## row lines per page rlpp <- lpp - hlines - flines @@ -679,7 +729,9 @@ pag_tt_indices <- function(tt, nosplitin = nosplitin, verbose = verbose, have_col_fnotes = have_cfnotes, - div_height = dheight + div_height = dheight, + col_gap = col_gap, + has_rowlabels = TRUE ) } @@ -773,7 +825,14 @@ paginate_table <- function(tt, colwidths = NULL, tf_wrap = FALSE, max_width = NULL, + fontspec = font_spec(font_family, font_size, lineheight), + col_gap = 3, verbose = FALSE) { + new_dev <- open_font_dev(fontspec) + if (new_dev) { + on.exit(close_font_dev()) + } + if ((non_null_na(lpp) || non_null_na(cpp)) && (!is.null(page_type) || (!is.null(pg_width) && !is.null(pg_height)))) { # nolint pg_lcpp <- page_lcpp( @@ -784,7 +843,8 @@ paginate_table <- function(tt, pg_width = pg_width, pg_height = pg_height, margins = margins, - landscape = landscape + landscape = landscape, + fontspec = fontspec ) if (non_null_na(lpp)) { @@ -803,7 +863,15 @@ paginate_table <- function(tt, } if (is.null(colwidths)) { - colwidths <- propose_column_widths(matrix_form(tt, indent_rownames = TRUE)) + colwidths <- propose_column_widths( + matrix_form( + tt, + indent_rownames = TRUE, + fontspec = fontspec, + col_gap = col_gap + ), + fontspec = fontspec + ) } if (!tf_wrap) { @@ -815,7 +883,7 @@ paginate_table <- function(tt, max_width <- cpp } else if (identical(max_width, "auto")) { ## XXX this 3 is column sep width!!!!!!! - max_width <- sum(colwidths) + 3 * (length(colwidths) - 1) + max_width <- sum(colwidths) + col_gap * (length(colwidths) - 1) } if (!is.null(cpp) && !is.null(max_width) && max_width > cpp) { warning("max_width specified is wider than characters per page width (cpp).") @@ -837,16 +905,19 @@ paginate_table <- function(tt, colwidths = colwidths, tf_wrap = tf_wrap, max_width = max_width, - verbose = verbose + fontspec = fontspec, + verbose = verbose, + col_gap = col_gap ) return(unlist(ret, recursive = TRUE)) } inds <- paginate_indices(tt, page_type = page_type, - font_family = font_family, - font_size = font_size, - lineheight = lineheight, + fontspec = fontspec, + ## font_family = font_family, + ## font_size = font_size, + ## lineheight = lineheight, landscape = landscape, pg_width = pg_width, pg_height = pg_height, @@ -858,6 +929,7 @@ paginate_table <- function(tt, colwidths = colwidths, tf_wrap = tf_wrap, max_width = max_width, + col_gap = col_gap, verbose = verbose ) ## paginate_table apparently doesn't accept indent_size diff --git a/R/tt_toString.R b/R/tt_toString.R index 269e2db46..9a3a03173 100644 --- a/R/tt_toString.R +++ b/R/tt_toString.R @@ -47,16 +47,22 @@ setMethod("toString", "VTableTree", function(x, hsep = horizontal_sep(x), indent_size = 2, tf_wrap = FALSE, - max_width = NULL) { + max_width = NULL, + fontspec = font_spec(), + ttype_ok = FALSE) { toString( matrix_form(x, indent_rownames = TRUE, - indent_size = indent_size + indent_size = indent_size, + fontspec = fontspec, + col_gap = col_gap ), widths = widths, col_gap = col_gap, hsep = hsep, tf_wrap = tf_wrap, - max_width = max_width + max_width = max_width, + fontspec = fontspec, + ttype_ok = ttype_ok ) }) @@ -144,6 +150,10 @@ table_shell_str <- function(tt, widths = NULL, col_gap = 3, hsep = default_hsep( #' has indented row names (strings pre-fixed). #' @param expand_newlines (`flag`)\cr whether the matrix form generated should expand rows whose values contain #' newlines into multiple 'physical' rows (as they will appear when rendered into ASCII). Defaults to `TRUE`. +#' @param fontspec (`font_spec` or `NULL`)\cr Font specification that should be +#' assumed during wrapping, as returned by [formatters::font_spec()]. +#' @param col_gap (`numeric(1)`)\cr The column gap to assume between columns, in +#' number of spaces assuming `fontspec` (this reduces to number of characters for monospace fonts). #' #' @details #' The strings in the return object are defined as follows: row labels are those determined by `make_row_df` and cell @@ -189,11 +199,13 @@ setMethod( function(obj, indent_rownames = FALSE, expand_newlines = TRUE, - indent_size = 2) { + indent_size = 2, + fontspec = NULL, + col_gap = 3L) { stopifnot(is(obj, "VTableTree")) header_content <- .tbl_header_mat(obj) # first col are for row.names - sr <- make_row_df(obj) + sr <- make_row_df(obj, fontspec = fontspec) body_content_strings <- if (NROW(sr) == 0) { character() @@ -293,6 +305,7 @@ setMethod( formats = formats, ## display = display, purely a function of spans, handled in constructor now row_info = sr, + colpaths = make_col_df(obj)[["path"]], ## line_grouping handled internally now line_grouping = 1:nrow(body), ref_fnotes = ref_fnotes, nlines_header = nr_header, ## this is fixed internally @@ -308,7 +321,9 @@ setMethod( table_inset = table_inset(obj), header_section_div = header_section_div(obj), horizontal_sep = horizontal_sep(obj), - indent_size = indent_size + indent_size = indent_size, + fontspec = fontspec, + col_gap = col_gap ) } ) diff --git a/inst/WORDLIST b/inst/WORDLIST index 85eef8d0e..7a2fe8159 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,3 +1,4 @@ +Bové CRAN's Carreras Cheatsheet @@ -21,12 +22,14 @@ RStudio Resync Rua STUDYID +Sabanés Saibah Stoilova Subtable Subtables Tadeusz Unstratified +ValueWrapper Yung amongst charset @@ -41,6 +44,7 @@ facetted facetting flextable formatter +formatters funder getter getters @@ -50,6 +54,7 @@ iteratively labelled layouting mandatorily +monospace multivariable orderable pathing @@ -58,6 +63,7 @@ postprocessing pre priori programmatically +quartiles reindexed repo repped @@ -73,6 +79,7 @@ subtables summarization tableone todo +truetype unaggregated unicode univariable diff --git a/man/build_table.Rd b/man/build_table.Rd index 0ebaef7b9..37869f8b0 100644 --- a/man/build_table.Rd +++ b/man/build_table.Rd @@ -23,7 +23,7 @@ build_table( \item{alt_counts_df}{(\code{data.frame} or \code{tibble})\cr alternative full dataset the rtables framework will use \emph{only} when calculating column counts.} -\item{col_counts}{(\code{numeric} or \code{NULL})\cr if non-\code{NULL}, column counts +\item{col_counts}{(\code{numeric} or \code{NULL})\cr \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} if non-\code{NULL}, column counts which override those calculated automatically during tabulation. Must specify "counts" for \emph{all} resulting columns if non-\code{NULL}. \code{NA} elements will be replaced with the automatically calculated counts.} diff --git a/man/formatters_methods.Rd b/man/formatters_methods.Rd index c50ac3c5c..6a7767a00 100644 --- a/man/formatters_methods.Rd +++ b/man/formatters_methods.Rd @@ -119,13 +119,19 @@ \S4method{table_inset}{InstantiatedColumnInfo}(obj) <- value -\S4method{nlines}{TableRow}(x, colwidths = NULL, max_width = NULL) +\S4method{nlines}{TableRow}(x, colwidths = NULL, max_width = NULL, fontspec, col_gap = 3) -\S4method{nlines}{LabelRow}(x, colwidths = NULL, max_width = NULL) +\S4method{nlines}{LabelRow}( + x, + colwidths = NULL, + max_width = NULL, + fontspec = fontspec, + col_gap = NULL +) -\S4method{nlines}{RefFootnote}(x, colwidths = NULL, max_width = NULL) +\S4method{nlines}{RefFootnote}(x, colwidths = NULL, max_width = NULL, fontspec, col_gap = NULL) -\S4method{nlines}{InstantiatedColumnInfo}(x, colwidths = NULL, max_width = NULL) +\S4method{nlines}{InstantiatedColumnInfo}(x, colwidths = NULL, max_width = NULL, fontspec, col_gap = 3) \S4method{make_row_df}{VTableTree}( tt, @@ -139,7 +145,9 @@ repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL + max_width = NULL, + fontspec = NULL, + col_gap = 3 ) \S4method{make_row_df}{TableRow}( @@ -154,7 +162,9 @@ repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL + max_width = NULL, + fontspec = font_spec(), + col_gap = 3 ) \S4method{make_row_df}{LabelRow}( @@ -169,7 +179,9 @@ repr_inds = integer(), sibpos = NA_integer_, nsibs = NA_integer_, - max_width = NULL + max_width = NULL, + fontspec = font_spec(), + col_gap = 3 ) } \arguments{ @@ -184,6 +196,12 @@ \item{max_width}{(\code{numeric(1)})\cr width that strings should be wrapped to when determining how many lines they require.} +\item{fontspec}{(\code{font_spec})\cr a font_spec object specifying the font information to use for +calculating string widths and heights, as returned by \code{\link[formatters:font_spec]{font_spec()}}.} + +\item{col_gap}{(\code{numeric(1)})\cr width of gap between columns in number of spaces. +Only used by methods which must calculate span widths after wrapping.} + \item{tt}{(\code{TableTree} or related class)\cr a \code{TableTree} object representing a populated table.} \item{visible_only}{(\code{flag})\cr should only visible aspects of the table structure be reflected @@ -230,7 +248,7 @@ Most arguments beyond \code{tt} and \code{visible_only} are present so that \cod } \note{ The technically present root tree node is excluded from the summary returned by -both \code{make_row_df} and \code{make_col_df} (see \code{\link[rtables:make_col_df]{rtables::make_col_df()}}), as it is simply the +both \code{make_row_df} and \code{make_col_df} (see relevant functions in\code{rtables}), as it is the row/column structure of \code{tt} and thus not useful for pathing or pagination. } \examples{ diff --git a/man/int_methods.Rd b/man/int_methods.Rd index a34c1a74b..0407afafe 100644 --- a/man/int_methods.Rd +++ b/man/int_methods.Rd @@ -247,6 +247,9 @@ \alias{value_labels,ValueWrapper-method} \alias{value_labels,LevelComboSplitValue-method} \alias{value_labels,MultiVarSplit-method} +\alias{value_expr} +\alias{value_expr,ValueWrapper-method} +\alias{value_expr,ANY-method} \alias{spl_varlabels} \alias{spl_varlabels,MultiVarSplit-method} \alias{spl_varlabels<-} @@ -885,6 +888,12 @@ value_labels(obj) \S4method{value_labels}{MultiVarSplit}(obj) +value_expr(obj) + +\S4method{value_expr}{ValueWrapper}(obj) + +\S4method{value_expr}{ANY}(obj) + spl_varlabels(obj) \S4method{spl_varlabels}{MultiVarSplit}(obj) diff --git a/man/make_split_fun.Rd b/man/make_split_fun.Rd index a730c270c..4afef8df7 100644 --- a/man/make_split_fun.Rd +++ b/man/make_split_fun.Rd @@ -42,16 +42,16 @@ because they will not be present when validity checking is done). The preprocessing component is useful for things such as manipulating factor levels, e.g., to trim unobserved ones or to reorder levels based on observed counts, etc. -Customization of core splitting (2) is currently only supported in row splits. Core splitting functions override the -fundamental splitting procedure, and are only necessary in rare cases. These must accept \code{spl}, \code{df}, \code{vals}, and -\code{labels}, and can optionally accept \code{.spl_context}. They must return a named list with elements, all of the same -length, as follows: -\itemize{ -\item \code{datasplit}, containing a list of \code{data.frame} objects. -\item \code{values}, containing values associated with the facets, which must be \code{character} or \code{SplitValue} objects. -These values will appear in the paths of the resulting table. -\item \code{labels}, containing the character labels associated with \code{values} -} +Core splitting functions override the fundamental +splitting procedure, and are only necessary in rare cases. These +must accept \code{spl}, \code{df}, \code{vals}, \code{labels}, and can optionally +accept \code{.spl_context}. They should return a split result object +constructed via \code{make_split_result()}. + +In particular, if the custom split function will be used in +column space, subsetting expressions (e.g., as returned by +\code{quote()} or \code{bquote} must be provided, while they are +optional (and largely ignored, currently) in row space. Post-processing functions (3) must accept the result of the core split as their first argument (which can be anything), in addition to \code{spl}, and \code{fulldf}, and can optionally accept \code{.spl_context}. They must each return a diff --git a/man/make_split_result.Rd b/man/make_split_result.Rd index 77363e4ee..408e8f134 100644 --- a/man/make_split_result.Rd +++ b/man/make_split_result.Rd @@ -5,9 +5,22 @@ \alias{add_to_split_result} \title{Construct split result object} \usage{ -make_split_result(values, datasplit, labels, extras = NULL) +make_split_result( + values, + datasplit, + labels, + extras = NULL, + subset_exprs = vector("list", length(values)) +) -add_to_split_result(splres, values, datasplit, labels, extras = NULL) +add_to_split_result( + splres, + values, + datasplit, + labels, + extras = NULL, + subset_exprs = NULL +) } \arguments{ \item{values}{(\code{character} or \code{list(SplitValue)})\cr the values associated with each facet.} @@ -19,6 +32,9 @@ add_to_split_result(splres, values, datasplit, labels, extras = NULL) \item{extras}{(\code{list} or \code{NULL})\cr extra values associated with each of the facets which will be passed to analysis functions applied within the facet.} +\item{subset_exprs}{(\code{list})\cr A list of subsetting expressions (e.g., +created with \code{quote()}) to be used during column subsetting.} + \item{splres}{(\code{list})\cr a list representing the result of splitting.} } \value{ @@ -33,21 +49,37 @@ post-processing within a custom split function. These functions performs various housekeeping tasks to ensure that the split result list is as the rtables internals expect it, most of which are not relevant to end users. } +\note{ +Column splitting will not work correctly if a split function +calls \code{make_split_result} without specifying subset expressions; +row splitting will work as normal. This is due to the fact that +subsetting expressions are used during column splitting to +represent the data associated with facets, while actual data +subsets are used during row splitting. +} \examples{ splres <- make_split_result( values = c("hi", "lo"), datasplit = list(hi = mtcars, lo = mtcars[1:10, ]), - labels = c("more data", "less data") + labels = c("more data", "less data"), + subset_exprs = list(expression(TRUE), expression(seq_along(wt) <= 10)) ) splres2 <- add_to_split_result(splres, values = "med", datasplit = list(med = mtcars[1:20, ]), - labels = "kinda some data" + labels = "kinda some data", + subset_exprs = quote(seq_along(wt) <= 20) ) } \seealso{ +Other make_custom_split: +\code{\link{add_combo_facet}()}, +\code{\link{drop_facet_levels}()}, +\code{\link{make_split_fun}()}, +\code{\link{trim_levels_in_facets}()} + Other make_custom_split: \code{\link{add_combo_facet}()}, \code{\link{drop_facet_levels}()}, diff --git a/man/matrix_form-VTableTree-method.Rd b/man/matrix_form-VTableTree-method.Rd index 3ebb6a4c1..76831bb77 100644 --- a/man/matrix_form-VTableTree-method.Rd +++ b/man/matrix_form-VTableTree-method.Rd @@ -8,7 +8,9 @@ obj, indent_rownames = FALSE, expand_newlines = TRUE, - indent_size = 2 + indent_size = 2, + fontspec = NULL, + col_gap = 3L ) } \arguments{ @@ -21,6 +23,12 @@ has indented row names (strings pre-fixed).} newlines into multiple 'physical' rows (as they will appear when rendered into ASCII). Defaults to \code{TRUE}.} \item{indent_size}{(\code{numeric(1)})\cr number of spaces to use per indent level. Defaults to 2.} + +\item{fontspec}{(\code{font_spec} or \code{NULL})\cr Font specification that should be +assumed during wrapping, as returned by \code{\link[formatters:font_spec]{formatters::font_spec()}}.} + +\item{col_gap}{(\code{numeric(1)})\cr The column gap to assume between columns, in +number of spaces assuming \code{fontspec} (this reduces to number of characters for monospace fonts).} } \value{ A list with the following elements: diff --git a/man/paginate.Rd b/man/paginate.Rd index 1403d97a9..d0ac79230 100644 --- a/man/paginate.Rd +++ b/man/paginate.Rd @@ -12,6 +12,8 @@ pag_tt_indices( nosplitin = character(), colwidths = NULL, max_width = NULL, + fontspec = NULL, + col_gap = 3, verbose = FALSE ) @@ -32,6 +34,8 @@ paginate_table( colwidths = NULL, tf_wrap = FALSE, max_width = NULL, + fontspec = font_spec(font_family, font_size, lineheight), + col_gap = 3, verbose = FALSE ) } @@ -53,6 +57,11 @@ footnotes) materials should be word-wrapped to. If \code{NULL}, it is set to the session (\code{getOption("width")}). If set to \code{"auto"}, the width of the table (plus any table inset) is used. Parameter is ignored if \code{tf_wrap = FALSE}.} +\item{fontspec}{(\code{font_spec})\cr a font_spec object specifying the font information to use for +calculating string widths and heights, as returned by \code{\link[formatters:font_spec]{font_spec()}}.} + +\item{col_gap}{(\code{numeric(1)})\cr space (in characters) between columns.} + \item{verbose}{(\code{flag})\cr whether additional information should be displayed to the user. Defaults to \code{FALSE}.} \item{page_type}{(\code{string})\cr name of a page type. See \code{\link[formatters]{page_types}}. Ignored diff --git a/man/tostring.Rd b/man/tostring.Rd index f5e53f4b1..a6e1933c1 100644 --- a/man/tostring.Rd +++ b/man/tostring.Rd @@ -12,7 +12,9 @@ hsep = horizontal_sep(x), indent_size = 2, tf_wrap = FALSE, - max_width = NULL + max_width = NULL, + fontspec = font_spec(), + ttype_ok = FALSE ) } \arguments{ @@ -36,6 +38,13 @@ must also be considered.} footnotes) materials should be word-wrapped to. If \code{NULL}, it is set to the current print width of the session (\code{getOption("width")}). If set to \code{"auto"}, the width of the table (plus any table inset) is used. Parameter is ignored if \code{tf_wrap = FALSE}.} + +\item{fontspec}{(\code{font_spec})\cr a font_spec object specifying the font information to use for +calculating string widths and heights, as returned by \code{\link[formatters:font_spec]{font_spec()}}.} + +\item{ttype_ok}{(\code{logical(1)})\cr should truetype (non-monospace) fonts be +allowed via \code{fontspec}. Defaults to \code{FALSE}. This parameter is primarily +for internal testing and generally should not be set by end users.} } \value{ A string representation of \code{x} as it appears when printed. diff --git a/tests/testthat/test-exporters.R b/tests/testthat/test-exporters.R index ec10f6793..c11103921 100644 --- a/tests/testthat/test-exporters.R +++ b/tests/testthat/test-exporters.R @@ -146,21 +146,29 @@ test_that("export_as_pdf works", { tmpf <- tempfile(fileext = ".pdf") expect_warning( - export_as_pdf(tbl, file = tmpf, landscape = TRUE, width = 3, paginate = FALSE), + export_as_pdf(tbl, file = tmpf, landscape = TRUE, height = 1000, width = 3, paginate = FALSE), "width of page 1 exceeds the available space" ) expect_true(file.exists(tmpf)) file.remove(tmpf) expect_warning( - export_as_pdf(tbl, file = tmpf, height = 3, paginate = FALSE), + export_as_pdf(tbl, file = tmpf, height = 3, width = 1000, paginate = FALSE), "height of page 1 exceeds the available space" ) res <- export_as_pdf(tbl, file = tmpf) - expect_equal(res$npages, 3) + + ## non-monospace fonts work + ## this tests the actual pagination behavior... + fspec <- font_spec("Times", 20, 1.2) + file.remove(tmpf) + expect_error(export_as_pdf(tbl, file = tmpf, fontspec = fspec), "non-monospace") + file.remove(tmpf) ## blank file created (currently, this could be better) + res <- export_as_pdf(tbl, file = tmpf, fontspec = fspec, ttype_ok = TRUE) }) + # test_that("exporting pdfs gives the correct values", { # if (check_pdf) { # lyt <- basic_table(title = " ") %>% @@ -198,7 +206,7 @@ test_that("exporting pdf does the inset", { table_inset(tbl) <- 100 tmpf <- tempfile(fileext = ".pdf") - expect_error(export_as_pdf(tbl, file = tmpf)) + expect_error(export_as_pdf(tbl, file = tmpf), "Width of row labels equal to or larger than") }) diff --git a/tests/testthat/test-pagination.R b/tests/testthat/test-pagination.R index 3e4ebf814..a939a1fe8 100644 --- a/tests/testthat/test-pagination.R +++ b/tests/testthat/test-pagination.R @@ -334,8 +334,8 @@ test_that("cell and column wrapping works in pagination", { list(1:5, 6:10) ) expect_identical( - nlines(col_info(tt_for_wrap), colwidths = clw), - nlines(col_info(tt_for_wrap)) + 2L + nlines(col_info(tt_for_wrap), colwidths = clw, fontspec = NULL), + nlines(col_info(tt_for_wrap), fontspec = NULL) + 2L ) ## 2 new lines from wrapping pdf <- make_row_df(tt_for_wrap, colwidths = clw) @@ -450,7 +450,7 @@ test_that("Pagination works with non-default min_siblings", { suppressMessages( expect_error( paginate_table(tt, lpp = 3, min_siblings = 1), - "*Unable to find any valid pagination split for page 1 between rows 1 and 1*" + ".*Unable to find any valid pagination .*between rows 1 and 1.*" ) ) }) @@ -572,3 +572,18 @@ test_that("Pagination works with referential footnotes", { expect_equal(ref_fn_res4$ref_index, 1) expect_equal(ref_fn_res4$symbol, "3") }) + + +test_that("setting colgap during pagination works", { + tt <- tt_to_export() + ## row labels take up 12, all other columns 10 + 3 (default colgap) + ## so 2 cols per page, 3 pages total + pags1 <- paginate_table(tt, lpp = NULL, cpp = 38) + expect_equal(length(pags1), 3) + ## increase col_gap by one prevents second column on each page + ## so 6 pages + pags2 <- paginate_table(tt, lpp = NULL, cpp = 38, col_gap = 4) + expect_equal(length(pags2), ncol(tt)) + ## too wide a column gap, no columns fit after labels + expect_error(suppressMessages(paginate_table(tt, lpp = NULL, cpp = 38, col_gap = 26))) +}) diff --git a/tests/testthat/test-split_funs.R b/tests/testthat/test-split_funs.R index 659a85dbb..4c3be13c4 100644 --- a/tests/testthat/test-split_funs.R +++ b/tests/testthat/test-split_funs.R @@ -339,7 +339,12 @@ test_that("make_split_fun works", { very_stupid_core <- function(spl, df, vals, labels, .spl_context) { make_split_result( c("stupid", "silly"), - datasplit = list(df[1:10, ], df[11:30, ]), labels = c("first 10", "second 20") + datasplit = list(df[1:10, ], df[11:30, ]), + labels = c("first 10", "second 20"), + subset_exprs = list( + quote(seq_along(AGE) <= 10), + quote(seq_along(AGE) %in% 11:30) + ) ) } @@ -350,12 +355,16 @@ test_that("make_split_fun works", { levels = c("stupid", "silly") )) ) - lyt4a <- basic_table() %>% + lyt4a <- basic_table(show_colcounts = TRUE) %>% split_cols_by("ARM", split_fun = nonsense_splfun) %>% analyze("AGE") - ## not supported in column space, currently - expect_error(build_table(lyt4a, DM), "override core splitting") + tbl4a <- build_table(lyt4a, DM) + expect_equal( + col_counts(tbl4a), + c(10L, 20L, 30L) + ) + lyt4b <- basic_table() %>% split_rows_by("ARM", split_fun = nonsense_splfun) %>% @@ -379,6 +388,20 @@ test_that("make_split_fun works", { 30, cell_values(tbl4b, pths[[5]])[[1]][[1]] ) + + ## add_comb_facet within make_split_fun in column space, regression test + + combofun <- add_combo_facet("combo", "Drug X or Combo", c("A: Drug X", "C: Combination")) + mysplfun <- make_split_fun(post = list(combofun)) + + lyt5 <- basic_table() %>% + split_cols_by("ARM", split_fun = mysplfun) %>% + analyze("STRATA1") + + tbl5 <- build_table(lyt5, ex_adsl) + ## each combo value is A count + C count + vals <- cell_values(tbl5) + expect_true(all(sapply(vals, function(vi) vi$combo == vi[[1]] + vi[[3]]))) }) test_that("spl_variable works", { @@ -412,3 +435,5 @@ test_that("spl_variable works", { "Split class MultiVarSplit not associated with a single variable" ) }) + +## combo levels diff --git a/vignettes/introduction.Rmd b/vignettes/introduction.Rmd index 681ce562d..0c0651922 100644 --- a/vignettes/introduction.Rmd +++ b/vignettes/introduction.Rmd @@ -31,7 +31,7 @@ The content in this vignette is based on the following two resources: * The [`rtables` useR 2020 presentation](https://www.youtube.com/watch?v=CBQzZ8ZhXLA) by Gabriel Becker * [`rtables` - A Framework For Creating Complex Structured Reporting Tables Via -Multi-Level Faceted Computations](https://arxiv.org/pdf/2306.16610.pdf). +Multi-Level Faceted Computations](https://arxiv.org/pdf/2306.16610). The packages used in this vignette are `rtables` and `dplyr`: