diff --git a/DESCRIPTION b/DESCRIPTION index 43fee1c9d..7719a39a0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,6 +40,7 @@ Imports: Suggests: broom (>= 0.7.10), car (>= 3.0-13), + checkmate (>= 2.1.0), dplyr (>= 1.0.5), flextable (>= 0.8.4), knitr (>= 1.42), @@ -54,7 +55,7 @@ VignetteBuilder: knitr Config/Needs/verdepcheck: insightsengineering/formatters, tidyverse/magrittr, rstudio/htmltools, tidymodels/broom, cran/car, - tidyverse/dplyr, davidgohel/flextable, yihui/knitr, + mllg/checkmate, tidyverse/dplyr, davidgohel/flextable, yihui/knitr, davidgohel/officer, Merck/r2rtf, r-lib/testthat, tidyverse/tibble, tidyverse/tidyr, r-lib/xml2 Encoding: UTF-8 diff --git a/NAMESPACE b/NAMESPACE index 4d497f718..4124e66b4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -82,6 +82,7 @@ export(do_base_split) export(drop_and_remove_levels) export(drop_facet_levels) export(drop_split_levels) +export(export_as_docx) export(export_as_pdf) export(export_as_tsv) export(export_as_txt) @@ -108,6 +109,8 @@ export(make_col_df) export(make_split_fun) export(make_split_result) export(manual_cols) +export(margins_landscape) +export(margins_potrait) export(no_colinfo) export(non_ref_rcell) export(obj_avar) @@ -139,6 +142,8 @@ export(rrowl) export(rtable) export(rtablel) export(sanitize_table_struct) +export(section_properties_landscape) +export(section_properties_portrait) export(select_all_levels) export(simple_analysis) export(sort_at_path) @@ -161,6 +166,7 @@ export(table_shell) export(table_shell_str) export(table_structure) export(tail) +export(theme_docx_default) export(top_left) export(tree_children) export(trim_levels_in_facets) diff --git a/NEWS.md b/NEWS.md index 1bb141e1e..c9d705e18 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,8 @@ ## rtables 0.6.3.9001 +### New Features + * Added support for `.docx` exports with `export_as_docx()`. + * Expanded support for `flextable` customization with theme function specific for word documents (`theme_docx_default()`). + ### Miscellaneous * Specified minimal version of package dependencies. diff --git a/R/tt_export.R b/R/tt_export.R index b18794fe5..d99e5c7df 100644 --- a/R/tt_export.R +++ b/R/tt_export.R @@ -102,14 +102,15 @@ path_enriched_df <- function(tt, path_fun = collapse_path, value_fun = collapse_ do_label_row <- function(rdfrow, maxlen) { pth <- rdfrow$path[[1]] - c(as.list(pth), replicate(maxlen - length(pth), list(NA_character_)), list(row_num = rdfrow$abs_rownumber, content = FALSE, node_class = rdfrow$node_class)) + c(as.list(pth), replicate(maxlen - length(pth), list(NA_character_)), + list(row_num = rdfrow$abs_rownumber, content = FALSE, node_class = rdfrow$node_class)) } make_result_df_md_colnames <- function(maxlen) { spllen <- floor((maxlen - 2) / 2) ret <- character() - if(spllen > 0 ) + if (spllen > 0) ret <- paste(c("spl_var", "spl_value"), rep(seq_len(spllen), rep(2, spllen)), sep = "_") ret <- c(ret, c("avar_name", "row_name", "row_num", "is_group_summary", "node_class")) } @@ -122,7 +123,7 @@ do_content_row <- function(rdfrow, maxlen) { seq_before <- seq_len(contpos - 1) - ret <- c(as.list(pth[seq_before]), replicate(maxlen - contpos, list(NA_character_)), + c(as.list(pth[seq_before]), replicate(maxlen - contpos, list(NA_character_)), list(tail(pth, 1)), list(row_num = rdfrow$abs_rownumber, content = TRUE, node_class = rdfrow$node_class)) } @@ -133,7 +134,7 @@ do_data_row <- function(rdfrow, maxlen) { pthlen <- length(pth) ## odd means we have a multi-analsysis step in the path, we dont' want that in the result data frame if(pthlen %% 2 == 1) { - pth <- pth[-1*(pthlen - 2)] + pth <- pth[-1 * (pthlen - 2)] } c(as.list(pth[seq_len(pthlen - 2)]), replicate(maxlen - pthlen, list(NA_character_)), @@ -193,8 +194,9 @@ result_df_v0_experimental <- function(tt) { c("name", "label", "abs_rownumber", "path", "reprint_inds", "node_class")], cellvals) maxlen <- max(lengths(df$path)) - metadf <- do.call(rbind.data.frame, lapply(seq_len(NROW(df)), function(ii) handle_rdf_row(df[ii,], maxlen = maxlen))) - cbind(metadf[metadf$node_class != "LabelRow",], + metadf <- do.call(rbind.data.frame, lapply(seq_len(NROW(df)), + function(ii) handle_rdf_row(df[ii, ], maxlen = maxlen))) + cbind(metadf[metadf$node_class != "LabelRow", ], cellvals) } @@ -239,7 +241,7 @@ as_result_df <- function(tt, spec = "v0_experimental", ...) { while(donenc < nctot) { curnc <- NCOL(ptabs[[i]]) ret[[i]] <- c(rlw, colwidths[seq_len(curnc)]) - colwidths <- colwidths[-1*seq_len(curnc)] + colwidths <- colwidths[-1 * seq_len(curnc)] donenc <- donenc + curnc i <- i + 1 } @@ -268,212 +270,6 @@ as_result_df <- function(tt, spec = "v0_experimental", ...) { #' @export formatters::export_as_txt -## #' Export as plain text with page break symbol -## #' -## #' @inheritParams gen_args -## #' @inheritParams tostring -## #' @inheritParams paginate_table -## #' @param file character(1). File to write. -## #' @param paginate logical(1). Should \code{tt} be paginated before writing the file. Defaults to `TRUE` if any sort of page dimension is specified. -## #' @param \dots Passed directly to \code{\link{paginate_table}} -## #' @param page_break character(1). Page break symbol (defaults to outputting \code{"\\s"}). -## #' @return \code{file} (this function is called for the side effect of writing the file. -## #' -## #' @note When specified, `font_size` is used *only* to determine pagination based -## #' on page dimensions. The written file is populated in raw ASCII text, which -## #' does not have the concept of font size. -## #' -## #' @export -## #' -## #' @seealso [export_as_pdf()] -## #' -## export_as_txt <- function(tt, file = NULL, -## page_type = NULL, -## landscape = FALSE, -## pg_width = page_dim(page_type)[if(landscape) 2 else 1], -## pg_height = page_dim(page_type)[if(landscape) 1 else 2], -## font_family = "Courier", -## font_size = 8, # grid parameters -## paginate = .need_pag(page_type, pg_width, pg_height, lpp, cpp), -## cpp = NULL, -## lpp = NULL, -## ..., page_break = "\\s\\n", -## hsep = default_hsep(), -## indent_size = 2, -## tf_wrap = paginate, -## max_width = cpp, -## colwidths = propose_column_widths(matrix_form(tt, TRUE))) { -## if(!is.null(colwidths) && length(colwidths) != ncol(tt) + 1) -## stop("non-null colwidths argument must have length ncol(tt) + 1 [", -## ncol(tt) + 1, "], got length ", length(colwidths)) -## if(paginate) { -## gp_plot <- gpar(fontsize = font_size, fontfamily = font_family) - -## pdf(file = tempfile(), width = pg_width, height = pg_height) -## on.exit(dev.off()) -## grid.newpage() -## pushViewport(plotViewport(margins = c(0, 0, 0, 0), gp = gp_plot)) - -## cur_gpar <- get.gpar() -## if(is.null(page_type) && is.null(pg_width) && is.null(pg_height) && -## (is.null(cpp) || is.null(lpp))) { -## page_type <- "letter" -## pg_width <- page_dim(page_type)[if(landscape) 2 else 1] -## pg_height <- page_dim(page_type)[if(landscape) 1 else 2] -## } - -## if (is.null(lpp)) { -## lpp <- floor(convertHeight(unit(1, "npc"), "lines", valueOnly = TRUE) / -## (cur_gpar$cex * cur_gpar$lineheight)) -## } -## if(is.null(cpp)) { -## cpp <- floor(convertWidth(unit(1, "npc"), "inches", valueOnly = TRUE) * -## font_lcpi(font_family, font_size, cur_gpar$lineheight)$cpi) -## } -## if(tf_wrap && is.null(max_width)) -## max_width <- cpp - -## tbls <- paginate_table(tt, cpp = cpp, lpp = lpp, tf_wrap = tf_wrap, max_width = max_width, -## colwidths = colwidths, ...) -## } else { -## tbls <- list(tt) -## } - -## res <- paste(mapply(function(tb, cwidths, ...) { -## ## 1 and +1 are because cwidths includes rowlabel 'column' -## cinds <- c(1, .figure_out_colinds(tb, tt) + 1L) -## toString(tb, widths = cwidths[cinds], ...) -## }, -## MoreArgs = list(hsep = hsep, -## indent_size = indent_size, -## tf_wrap = tf_wrap, -## max_width = max_width, -## cwidths = colwidths), -## SIMPLIFY = FALSE, -## tb = tbls), -## collapse = page_break) - -## if(!is.null(file)) -## cat(res, file = file) -## else -## res -## } - - -#' Create a `FlexTable` object representing an `rtables` `TableTree` -#' -#' @inheritParams gen_args -#' @param paginate logical(1). Should \code{tt} be paginated and exported as -#' multiple `flextables`. Defaults to \code{FALSE} -#' @inheritParams paginate_table -#' @param total_width numeric(1). Total width in inches for the resulting -#' `flextable(s)`. Defaults to 5. -#' @return a `flextable` object -#' @export -#' @examples -#' analysisfun <- function(x, ...) { -#' in_rows(row1 = 5, -#' row2 = c(1, 2), -#' .row_footnotes = list(row1 = "row 1 - row footnote"), -#' .cell_footnotes = list(row2 = "row 2 - cell footnote")) -#' } -#' -#' lyt <- basic_table(title = "Title says Whaaaat", subtitles = "Oh, ok.", -#' main_footer = "ha HA! Footer!") %>% -#' split_cols_by("ARM") %>% -#' analyze("AGE", afun = analysisfun) -#' -#' tbl <- build_table(lyt, ex_adsl) -#' ft <- tt_to_flextable(tbl) -#' ft - -tt_to_flextable <- function(tt, paginate = FALSE, lpp = NULL, - cpp = NULL, - ..., - colwidths = propose_column_widths(matrix_form(tt, indent_rownames = TRUE)), - tf_wrap = !is.null(cpp), - max_width = cpp, - total_width = 10) { - if(!requireNamespace("flextable") || !requireNamespace("officer")) { - stop("this function requires the flextable and officer packages. ", - "Please install them if you wish to use it") - } - - ## if we're paginating, just call - if(paginate) { - if(is.null(lpp)) - stop("lpp must be specified when calling tt_to_flextable with paginate=TRUE") - tabs <- paginate_table(tt, lpp = lpp, cpp = cpp, tf_wrap = tf_wrap, max_width = max_width, ...) - cinds <- lapply(tabs, function(tb) c(1, .figure_out_colinds(tb, tt) + 1L)) - return(mapply(tt_to_flextable, tt = tabs, colwidths = cinds, MoreArgs = list(paginate = FALSE, total_width = total_width), - SIMPLIFY = FALSE)) - } - - final_cwidths <- total_width * colwidths / sum(colwidths) - matform <- matrix_form(tt, indent_rownames = TRUE) - - ## this was nrow_header before but that seems wrong! - hnum <- mf_nlheader(matform) ## attr(matform, "nrow_header") - - content <- as.data.frame(matform$strings[-(1:hnum), , drop = FALSE]) - - rdf <- make_row_df(tt) - - hdr <- matform$strings[1:hnum, , drop = FALSE] - - flx <- flextable::qflextable(content) - - flx <- flextable::set_header_labels(flx, values = setNames(as.vector(hdr[hnum, , drop = TRUE]), names(content))) - flx <- flextable::width(flx, width = final_cwidths) - - if(hnum > 1) { - for(i in (hnum - 1):1) { - sel <- spans_to_viscell(matform$spans[i, ]) - flx <- flextable::add_header_row(flx, top = TRUE, - values = as.vector(hdr[i, sel]), - colwidths = as.integer(matform$spans[i, sel])) - } - - } - flx <- flextable::align(flx, j = 2:(NCOL(tt) + 1), align = "center", part = "header") - flx <- flextable::align(flx, j = 2:(NCOL(tt) + 1), align = "center", part = "body") - for(i in seq_len(NROW(tt))) { - flx <- flextable::padding(flx, i = i, j = 1, padding.left = 10 * rdf$indent[[i]]) - } - if(length(matform$ref_footnotes) > 0) { - flx <- flextable::add_footer_lines(flx, values = matform$ref_footnotes) - } - - if(length(all_titles(tt)) > 0 && any(nzchar(all_titles(tt)))) { - real_titles <- all_titles(tt) - real_titles <- real_titles[nzchar(real_titles)] - flx <- flextable::hline(flx, i = 1L, - border = officer::fp_border(), part = "header") - ## rev is because add_header_lines(top=TRUE) seems to put them in backwards!!! AAHHHH - flx <- flextable::add_header_lines(flx, values = rev(real_titles), - top = TRUE) - } - - if(length(all_footers(tt)) > 0) { - flx <- flextable::hline(flx, i = length(matform$ref_footnotes), - border = officer::fp_border(), part = "footer") - flx <- flextable::add_footer_lines(flx, values = all_footers(tt)) - } - - flx <- flextable::font(flx, fontname = "courier") - - flextable::set_table_properties(flx, layout = "autofit") -} - -.tab_to_colpath_set <- function(tt) { - vapply(collect_leaves(coltree(tt)), - function(y) paste(pos_to_path(tree_pos(y)), collapse = " "), - "") -} -.figure_out_colinds <- function(subtab, fulltab) { - match(.tab_to_colpath_set(subtab), - .tab_to_colpath_set(fulltab)) -} #' Export as PDF #' @@ -586,7 +382,8 @@ export_as_pdf <- function(tt, max_width <- cpp tbls <- if (paginate) { - paginate_table(tt, lpp = lpp, cpp = cpp, tf_wrap = tf_wrap, max_width = max_width, colwidths = colwidths, ...) + paginate_table(tt, lpp = lpp, cpp = cpp, tf_wrap = tf_wrap, max_width = max_width, + colwidths = colwidths, ...) } else { list(tt) } @@ -630,5 +427,644 @@ export_as_pdf <- function(tt, grid.draw(g) } - list(file = file, npages = npages, exceeds_width = exceeds_width, exceeds_height = exceeds_height, lpp = lpp, cpp = cpp) + list(file = file, npages = npages, exceeds_width = exceeds_width, exceeds_height = exceeds_height, + lpp = lpp, cpp = cpp) +} +# Flextable and docx ----------------------------------------------------------- +#' Export as word document +#' +#' @description +#' From a table, produce a self-contained word document or attach it to a template word +#' file (`template_file`). This function is based on [tt_to_flextable()] transformer and +#' `officer` package. +#' +#' @inheritParams gen_args +#' @param file character(1). String that indicates the final file output. It needs to have `.docx` +#' extension. +#' @param doc_metadata list of character(1)s. Any value that can be used as metadata by +#' `?officer::set_doc_properties`. Important text values are `title, subject, creator, description` +#' while `created` is a date object. +#' @inheritParams tt_to_flextable +#' @param template_file character(1). Template file that `officer` will use as a starting +#' point for the final document. It will attach the table and use the defaults defined in +#' the template file. Output will be doc `file` nonetheless. +#' @param section_properties `officer::prop_section` object. Here you can set margins and page +#' size. +#' +#' @note `export_as_docx()` does not have many options available. We suggest, if you need +#' specific formats and details to use [tt_to_flextable()] first and then `export_as_docx`. +#' Only `title_as_header` and `footer_as_text` need to be specified again if changed in +#' `tt_to_flextable()`. +#' +#' @seealso [tt_to_flextable()] +#' +#' @examples +#' lyt <- basic_table() %>% +#' split_cols_by("ARM") %>% +#' analyze(c("AGE", "BMRKR2", "COUNTRY")) +#' +#' tbl <- build_table(lyt, ex_adsl) +#' +#' # See how section_properties_portrait function is built for custom +#' \dontrun{ +#' tf <- tempfile(fileext = ".docx") +#' export_as_docx(tbl, file = tf, section_properties = section_properties_portrait()) +#' } +#' +#' @name export_as_docx +#' @export +export_as_docx <- function(tt, + file, + doc_metadata = NULL, + titles_as_header = FALSE, + footers_as_text = TRUE, + template_file = NULL, + section_properties = NULL) { + # Checks + check_required_packages(c("flextable", "officer")) + if (inherits(tt, "VTableTree")) { + flex_tbl <- tt_to_flextable(tt, + titles_as_header = titles_as_header, + footers_as_text = footers_as_text + ) + if (isFALSE(titles_as_header) || isTRUE(footers_as_text)) { + # Ugly but I could not find a getter for font.size + font_sz <- flex_tbl$header$styles$text$font.size$data[1, 1] + font_sz_footer <- flex_tbl$header$styles$text$font.size$data[1, 1] - 1 + font_fam <- flex_tbl$header$styles$text$font.family$data[1, 1] + + # Set the test as the tt + fpt <- officer::fp_text(font.family = font_fam, font.size = font_sz) + fpt_footer <- officer::fp_text(font.family = font_fam, font.size = font_sz_footer) + } + } else { + flex_tbl <- tt + } + if (!is.null(template_file) && !file.exists(template_file)) { + template_file <- NULL + } + + # Create a new empty Word document + if (!is.null(template_file)) { + doc <- officer::read_docx(template_file) + } else { + doc <- officer::read_docx() + } + + if (!is.null(section_properties)) { + doc <- officer::body_set_default_section(doc, section_properties) + } + + # Extract title + if (isFALSE(titles_as_header) && inherits(tt, "VTableTree")) { + ts_tbl <- all_titles(tt) + if (length(ts_tbl) > 0) { + doc <- add_text_par(doc, ts_tbl, fpt) + } + } + + # Add the table to the document + doc <- flextable::body_add_flextable(doc, flex_tbl, align = "left") + + # add footers as paragraphs + if (isTRUE(footers_as_text) && inherits(tt, "VTableTree")) { + # Adding referantial footer line separator if present + # (this is usually done differently, i.e. inside footnotes) + matform <- matrix_form(tt, indent_rownames = TRUE) + if (length(matform$ref_footnotes) > 0) { + doc <- add_text_par(doc, matform$ref_footnotes, fpt_footer) + } + # Footer lines + if (length(all_footers(tt)) > 0) { + doc <- add_text_par(doc, all_footers(tt), fpt_footer) + } + } + + if (!is.null(doc_metadata)) { + # Checks for values rely on officer function + doc <- do.call(officer::set_doc_properties, c(list("x" = doc), doc_metadata)) + } + + # Save the Word document to a file + print(doc, target = file) +} + +# Shorthand to add text paragraph +add_text_par <- function(doc, chr_v, text_format) { + for (ii in seq_along(chr_v)) { + cur_fp <- officer::fpar(officer::ftext(chr_v[ii], prop = text_format)) + doc <- officer::body_add_fpar(doc, cur_fp) + } + doc +} + +#' @describeIn export_as_docx helper function that defines standard portrait properties for tables. +#' @export +section_properties_portrait <- function() { + officer::prop_section( + page_size = officer::page_size( + orient = "portrait", + width = 8.5, height = 11 + ), + type = "continuous", + page_margins = margins_potrait() + ) +} + +#' @describeIn export_as_docx helper function that defines standard landscape properties for tables. +#' @export +section_properties_landscape <- function() { + officer::prop_section( + page_size = officer::page_size( + orient = "landscape", + width = 8.5, height = 11 + ), + type = "continuous", + page_margins = margins_landscape() + ) +} + +#' @describeIn export_as_docx helper function that defines standard portrait margins for tables. +#' @export +margins_potrait <- function() { + officer::page_mar(bottom = 0.98, top = 0.95, left = 1.5, right = 1, gutter = 0) +} +#' @describeIn export_as_docx helper function that defines standard landscape margins for tables. +#' @export +margins_landscape <- function() { + officer::page_mar(bottom = 1, top = 1.5, left = 0.98, right = 0.95, gutter = 0) +} + +#' Create a `FlexTable` from an `rtables` table +#' +#' @description +#' Principally used for export ([export_as_docx()]), this function produces a `flextable` +#' from an `rtables` table. If `theme = NULL`, `rtables`-like style will be used. Otherwise, +#' [theme_docx_default()] will produce a `.docx`-friendly table. +#' +#' @inheritParams gen_args +#' @param theme function(1). Defaults to `theme_docx_default(tt)`. It expects a +#' a theme function that is designed internally as a function of a `flextable` object +#' and changes its layout and style. If set to `NULL`, it will produce a table similar +#' to `rtables` default. +#' @param border `officer` border object. Defaults to `officer::fp_border(width = 0.5)`. +#' @param indent_size integer(1). If `NULL`, the default indent size of the table (see +#' [matrix_form()] `indent_size`) is used. To work with `docx`, any size is multiplied +#' by 2 mm (5.67 pt) as default. +#' @param titles_as_header logical(1). Defaults to `TRUE` for [tt_to_flextable()], so the +#' table is self-contained as it makes additional header rows for [main_title()] +#' string and [subtitles()] character vector (one per element). `FALSE` is suggested +#' for [export_as_docx()]. This adds titles and subtitles as a text paragraph above +#' the table. Same style is applied. +#' @param footers_as_text logical(1). Defaults to `FALSE` for [tt_to_flextable()], so +#' the table is self-contained with the flextable definition of footnotes. `TRUE` is +#' used for [export_as_docx()] to add the footers as a new paragraph after the table. +#' Same style is applied, but with a smaller font. +#' @param counts_in_newline logical(1). Defaults to `FALSE`. In `rtables` text printing +#' ([formatters::toString()]), the column counts, i.e. `(N=xx)`, is always on a new line. +#' We noticed that for `docx` exports could be necessary to have it on the same line. +#' @param paginate logical(1). If you need `.docx` export and you use +#' `export_as_docx`, we suggest relying on `word` pagination system. Cooperation +#' between the two mechanisms is not guaranteed. This option splits `tt` in different +#' "pages" as multiple `flextables`. Defaults to `FALSE`. +#' @inheritParams paginate_table +#' @param total_width numeric(1). Total width in inches for the resulting +#' `flextable(s)`. Defaults to 10. +#' +#' @return a `flextable` object. +#' +#' @seealso [export_as_docx()] +#' +#' @examples +#' analysisfun <- function(x, ...) { +#' in_rows( +#' row1 = 5, +#' row2 = c(1, 2), +#' .row_footnotes = list(row1 = "row 1 - row footnote"), +#' .cell_footnotes = list(row2 = "row 2 - cell footnote") +#' ) +#' } +#' +#' lyt <- basic_table( +#' title = "Title says Whaaaat", subtitles = "Oh, ok.", +#' main_footer = "ha HA! Footer!" +#' ) %>% +#' split_cols_by("ARM") %>% +#' analyze("AGE", afun = analysisfun) +#' +#' tbl <- build_table(lyt, ex_adsl) +#' # rtables style +#' tt_to_flextable(tbl, theme = NULL) +#' +#' tt_to_flextable(tbl, theme = theme_docx_default(tbl, font_size = 7)) +#' +#' @name tt_to_flextable +#' @export +tt_to_flextable <- function(tt, + theme = theme_docx_default(tt), + border = flextable::fp_border_default(width = 0.5), + indent_size = NULL, + titles_as_header = TRUE, + footers_as_text = FALSE, + counts_in_newline = FALSE, + paginate = FALSE, + lpp = NULL, + cpp = NULL, + ..., + colwidths = propose_column_widths(matrix_form(tt, indent_rownames = TRUE)), + tf_wrap = !is.null(cpp), + max_width = cpp, + total_width = 10) { + check_required_packages(c("flextable", "checkmate")) + if (!inherits(tt, "VTableTree")) { + stop("Input table is not an rtables' object.") + } + checkmate::assert_flag(titles_as_header) + checkmate::assert_flag(footers_as_text) + checkmate::assert_flag(counts_in_newline) + + ## if we're paginating, just call -> pagination happens also afterwards if needed + if (paginate) { + if (is.null(lpp)) { + stop("lpp must be specified when calling tt_to_flextable with paginate=TRUE") + } + tabs <- paginate_table(tt, lpp = lpp, cpp = cpp, tf_wrap = tf_wrap, max_width = max_width, ...) + cinds <- lapply(tabs, function(tb) c(1, .figure_out_colinds(tb, tt) + 1L)) + return(mapply(tt_to_flextable, + tt = tabs, colwidths = cinds, + MoreArgs = list(paginate = FALSE, total_width = total_width), + SIMPLIFY = FALSE + )) + } + + # Calculate the needed colwidths + final_cwidths <- total_width * colwidths / sum(colwidths) # xxx to fix + # xxx FIXME missing transformer from character based widths to mm or pt + + # Extract relevant information + matform <- matrix_form(tt, indent_rownames = TRUE) + body <- mf_strings(matform) # Contains header + spans <- mf_spans(matform) # Contains header + mpf_aligns <- mf_aligns(matform) # Contains header + hnum <- mf_nlheader(matform) # Number of lines for the header + rdf <- make_row_df(tt) # Row-wise info + + # decimal alignment pre-proc + if (any(grepl("dec", mpf_aligns))) { + body <- decimal_align(body, mpf_aligns) + # Coercion for flextable + mpf_aligns[mpf_aligns == "decimal"] <- "center" + mpf_aligns[mpf_aligns == "dec_left"] <- "left" + mpf_aligns[mpf_aligns == "dec_right"] <- "right" + } + + # Fundamental content of the table + content <- as.data.frame(body[-seq_len(hnum), , drop = FALSE]) + flx <- flextable::qflextable(content) %>% + # Default rtables if no footnotes + remove_hborder(part = "body", w = "bottom") + + # Header addition -> NB: here we have a problem with (N=xx) + hdr <- body[seq_len(hnum), , drop = FALSE] + + # IMPORTANT: Fix of (N=xx) which is by default on a new line but we usually do not + # want this, and it depends on the size of the table, it is not another + # row with different columns -> All of this should be fixed at source (in toString) + if (hnum > 1) { # otherwise nothing to do + det_nclab <- apply(hdr, 2, grepl, pattern = "\\(N=[0-9]+\\)$") + has_nclab <- apply(det_nclab, 1, any) + if (isFALSE(counts_in_newline) && any(has_nclab)) { + whsnc <- which(has_nclab) # which rows have it + what_is_nclab <- det_nclab[whsnc, ] + # condition for popping the interested row by merging the upper one + hdr[whsnc - 1, what_is_nclab] <- paste(hdr[whsnc - 1, what_is_nclab], + hdr[whsnc, what_is_nclab], + sep = " " + ) + hdr[whsnc, what_is_nclab] <- "" + + # We can remove the row if they are all "" + if (all(!nzchar(hdr[whsnc, ]))) { + hdr <- hdr[-whsnc, , drop = FALSE] + spans <- spans[-whsnc, , drop = FALSE] + body <- body[-whsnc, , drop = FALSE] + mpf_aligns <- mpf_aligns[-whsnc, , drop = FALSE] + hnum <- hnum - 1 + } + } + } + + flx <- flx %>% + flextable::set_header_labels( # Needed bc headers must be unique + values = setNames( + as.vector(hdr[hnum, , drop = TRUE]), + names(content) + ) + ) + # If there are more rows + if (hnum > 1) { + for (i in seq(hnum - 1, 1)) { + sel <- spans_to_viscell(spans[i, ]) + flx <- flextable::add_header_row( + flx, + top = TRUE, + values = as.vector(hdr[i, sel]), + colwidths = as.integer(spans[i, sel]) # xxx to fix + ) + } + } + + # Polish the inner horizontal borders from the header + flx <- flx %>% + remove_hborder(part = "header", w = "all") %>% + add_hborder("header", ii = c(0, hnum), border = border) + + # ALIGNS + flx <- flx %>% + apply_alignments(mpf_aligns[seq_len(hnum), , drop = FALSE], "header") %>% + apply_alignments(mpf_aligns[-seq_len(hnum), , drop = FALSE], "body") + + # Rownames indentation + checkmate::check_int(indent_size, null.ok = TRUE) + if (is.null(indent_size)) { + indent_size <- matform$indent_size * word_mm_to_pt(2) # default is 2mm (5.7pt) + } + for (i in seq_len(NROW(tt))) { + flx <- flextable::padding(flx, + i = i, j = 1, + padding.left = indent_size * rdf$indent[[i]] + word_mm_to_pt(0.1), # 0.1 mmm in pt + padding.right = word_mm_to_pt(0.1) # 0.1 mmm in pt (so not to touch the border) + ) + } + + # Adding referantial footer line separator if present + if (length(matform$ref_footnotes) > 0 && isFALSE(footers_as_text)) { + flx <- flextable::add_footer_lines(flx, values = matform$ref_footnotes) %>% + add_hborder(part = "body", ii = nrow(tt), border = border) + } + + # Footer lines + if (length(all_footers(tt)) > 0 && isFALSE(footers_as_text)) { + flx <- flextable::add_footer_lines(flx, values = all_footers(tt)) + } + + flx <- flextable::width(flx, width = final_cwidths) # xxx to fix + + if (!is.null(theme)) { + flx <- theme(flx) + } + + # Title lines (after theme for problems with lines) + if (titles_as_header && + length(all_titles(tt)) > 0 && any(nzchar(all_titles(tt)))) { + real_titles <- all_titles(tt) + real_titles <- real_titles[nzchar(real_titles)] + flx <- flextable::add_header_lines(flx, values = real_titles, top = TRUE) %>% + # Remove the added borders + remove_hborder(part = "header", w = c("inner", "top")) %>% + # Re-add the separator between titles and real headers + add_hborder( + part = "header", ii = length(real_titles), + border = border + ) %>% + # Remove vertical borders added by theme eventually + remove_vborder(part = "header", ii = seq_along(real_titles)) + } + + # These final formatting need to work with colwidths + flx <- flextable::set_table_properties(flx, layout = "autofit", align = "left") # xxx to fix + # NB: autofit or fixed may be switched if widths are correctly staying in the page + flx <- flextable::fix_border_issues(flx) # Fixes some rendering gaps in borders + + flx +} + +#' @describeIn tt_to_flextable main theme function for [export_as_docx()] +#' @param font character(1). Defaults to `"Arial"`. If the font is not vailable, `flextable` +#' default is used. +#' @param font_size integer(1). Positive integerish value that defaults to 9. +#' @param bold character vector. It can be any combination of `c("header", "content_rows", +#' "label_rows")`. The first one renders all column names bold (not `topleft` content). +#' Second and third option use [rtables::make_row_df()] to render content or/and label +#' rows as bold. +#' @param bold_manual named list. List of indexes lists. See example for needed structure. +#' Accepted groupings/names are `c("header", "body")`. +#' @inheritParams export_as_docx +#' +#' @seealso [export_as_docx()] +#' +#' @examples +#' # Custom theme +#' special_bold <- list( +#' "header" = list("i" = 1, "j" = c(1, 3)), +#' "body" = list("i" = c(1, 2), "j" = 1) +#' ) +#' custom_theme <- theme_docx_default(tbl, +#' font_size = 10, +#' font = "Brush Script MT", +#' border = flextable::fp_border_default(color = "pink", width = 2), +#' bold = NULL, +#' bold_manual = special_bold +#' ) +#' tt_to_flextable(tbl, +#' border = flextable::fp_border_default(color = "pink", width = 2), +#' theme = custom_theme +#' ) +#' +#' @export +theme_docx_default <- function(tt = NULL, # Option for more complicated stuff + font = "Arial", + font_size = 9, + bold = c("header", "content_rows", "label_rows"), + bold_manual = NULL, + border = flextable::fp_border_default(width = 0.5)) { + function(flx) { + check_required_packages(c("flextable", "checkmate")) + if (!inherits(flx, "flextable")) { + stop(sprintf( + "Function `%s` supports only flextable objects.", + "theme_box()" + )) + } + if (!is.null(tt) && !inherits(tt, "VTableTree")) { + stop("Input table is not an rtables' object.") + } + checkmate::assert_int(font_size, lower = 1) + checkmate::assert_string(font) + checkmate::assert_subset(bold, + eval(formals(theme_docx_default)$bold), + empty.ok = TRUE + ) + + # Font setting + flx <- flextable::fontsize(flx, size = font_size, part = "all") %>% + flextable::fontsize(size = font_size - 1, part = "footer") %>% + flextable::font(fontname = font, part = "all") + + # Vertical borders + flx <- flx %>% + flextable::border_outer(part = "body", border = border) %>% + flextable::border_outer(part = "header", border = border) + + # Vertical alignment -> all top for now, we will set it for the future + flx <- flx %>% + flextable::valign(j = 2:(NCOL(tt) + 1), valign = "top", part = "body") %>% + flextable::valign(j = 1, valign = "top", part = "body") %>% + flextable::valign(j = 2:(NCOL(tt) + 1), valign = "top", part = "header") + + # Bold settings + if (any(bold == "header")) { + flx <- flextable::bold(flx, j = 2:(NCOL(tt) + 1), part = "header") # Done with theme + } + # Content rows are effectively our labels in row names + if (any(bold == "content_rows")) { + if (is.null(tt)) stop('bold = "content_rows" needs the original rtables object (tt).') + rdf <- make_row_df(tt) + which_body <- which(rdf$node_class == "ContentRow") + flx <- flextable::bold(flx, j = 1, i = which_body, part = "body") + } + if (any(bold == "label_rows")) { + if (is.null(tt)) stop('bold = "content_rows" needs the original rtables object (tt).') + rdf <- make_row_df(tt) + which_body <- which(rdf$node_class == "LabelRow") + flx <- flextable::bold(flx, j = 1, i = which_body, part = "body") + } + # If you want specific cells to be bold + if (!is.null(bold_manual)) { + checkmate::assert_list(bold_manual) + valid_sections <- c("header", "body") # Only valid values + checkmate::assert_subset(names(bold_manual), valid_sections) + for (bi in seq_along(bold_manual)) { + bld_tmp <- bold_manual[[bi]] + checkmate::assert_list(bld_tmp) + if (!all(c("i", "j") %in% names(bld_tmp)) || + !all(vapply(bld_tmp, checkmate::test_integerish, logical(1)))) { + stop( + "Found an allowed section for manual bold (", names(bold_manual)[bi], + ") that was not a named list with i (row) and j (col) integer vectors." + ) + } + flx <- flextable::bold(flx, + i = bld_tmp$i, j = bld_tmp$j, + part = names(bold_manual)[bi] + ) + } + } + + # vertical padding is manual atm and respect doc std + flx <- flx %>% + # flextable::padding(j = 2:(NCOL(tt) + 1), padding.top = , part = "body") %>% # not specified + flextable::padding(j = 1, padding.top = 1, padding.bottom = 1, part = "body") %>% + flextable::padding(j = 2:(NCOL(tt) + 1), padding.top = 0, padding.bottom = 3, part = "header") + + # single line spacing (for safety) -> space = 1 + flx <- flextable::line_spacing(flx, space = 1, part = "all") + + flx + } +} +# Padding helper functions to transform mm to pt and viceversa +# # General note for word: 1pt -> 0.3527777778mm -> 0.013888888888889" +word_inch_to_pt <- function(inch) { # nocov + inch / 0.013888888888889 # nocov +} +word_mm_to_pt <- function(mm) { + mm / 0.3527777778 +} + +# Polish horizontal borders +remove_hborder <- function(flx, part, w = c("top", "bottom", "inner")) { + # If you need to remove all of them + if (length(w) == 1 && w == "all") { + w <- eval(formals(remove_hborder)$w) + } + + if (any(w == "top")) { + flx <- flextable::hline_top(flx, + border = flextable::fp_border_default(width = 0), + part = part + ) + } + if (any(w == "bottom")) { + flx <- flextable::hline_bottom(flx, + border = flextable::fp_border_default(width = 0), + part = part + ) + } + # Inner horizontal lines removal + if (any(w == "inner")) { + flx <- flextable::border_inner_h( + flx, + border = flextable::fp_border_default(width = 0), + part = part + ) + } + flx +} + +# Remove vertical borders from both sides (for titles) +remove_vborder <- function(flx, part, ii) { + flx <- flextable::border(flx, + i = ii, part = part, + border.left = flextable::fp_border_default(width = 0), + border.right = flextable::fp_border_default(width = 0) + ) +} + +# Add horizontal border +add_hborder <- function(flx, part, ii, border) { + if (any(ii == 0)) { + flx <- flextable::border(flx, i = 1, border.top = border, part = part) + ii <- ii[!(ii == 0)] + } + if (length(ii) > 0) { + flx <- flextable::border(flx, i = ii, border.bottom = border, part = part) + } + flx +} + +apply_alignments <- function(flx, aligns_df, part) { + # List of characters you want to search for + search_chars <- unique(c(aligns_df)) + + # Loop through each character and find its indexes + for (char in search_chars) { + indexes <- which(aligns_df == char, arr.ind = TRUE) + tmp_inds <- as.data.frame(indexes) + flx <- flx %>% + flextable::align( + i = tmp_inds[["row"]], + j = tmp_inds[["col"]], + align = char, + part = part + ) + } + + flx +} + +# only used in pagination +.tab_to_colpath_set <- function(tt) { + vapply( + collect_leaves(coltree(tt)), + function(y) paste(pos_to_path(tree_pos(y)), collapse = " "), + "" + ) +} +.figure_out_colinds <- function(subtab, fulltab) { + match( + .tab_to_colpath_set(subtab), + .tab_to_colpath_set(fulltab) + ) +} + +check_required_packages <- function(pkgs) { + for (pkgi in pkgs) { + if (!requireNamespace(pkgi)) { + stop( + "This function requires the ", pkgi, " package. ", + "Please install it if you wish to use it" + ) + } + } } diff --git a/man/export_as_docx.Rd b/man/export_as_docx.Rd new file mode 100644 index 000000000..f014e6d6a --- /dev/null +++ b/man/export_as_docx.Rd @@ -0,0 +1,96 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tt_export.R +\name{export_as_docx} +\alias{export_as_docx} +\alias{section_properties_portrait} +\alias{section_properties_landscape} +\alias{margins_potrait} +\alias{margins_landscape} +\title{Export as word document} +\usage{ +export_as_docx( + tt, + file, + doc_metadata = NULL, + titles_as_header = FALSE, + footers_as_text = TRUE, + template_file = NULL, + section_properties = NULL +) + +section_properties_portrait() + +section_properties_landscape() + +margins_potrait() + +margins_landscape() +} +\arguments{ +\item{tt}{\code{TableTree} (or related class). A \code{TableTree} object representing a +populated table.} + +\item{file}{character(1). String that indicates the final file output. It needs to have \code{.docx} +extension.} + +\item{doc_metadata}{list of character(1)s. Any value that can be used as metadata by +\code{?officer::set_doc_properties}. Important text values are \verb{title, subject, creator, description} +while \code{created} is a date object.} + +\item{titles_as_header}{logical(1). Defaults to \code{TRUE} for \code{\link[=tt_to_flextable]{tt_to_flextable()}}, so the +table is self-contained as it makes additional header rows for \code{\link[=main_title]{main_title()}} +string and \code{\link[=subtitles]{subtitles()}} character vector (one per element). \code{FALSE} is suggested +for \code{\link[=export_as_docx]{export_as_docx()}}. This adds titles and subtitles as a text paragraph above +the table. Same style is applied.} + +\item{footers_as_text}{logical(1). Defaults to \code{FALSE} for \code{\link[=tt_to_flextable]{tt_to_flextable()}}, so +the table is self-contained with the flextable definition of footnotes. \code{TRUE} is +used for \code{\link[=export_as_docx]{export_as_docx()}} to add the footers as a new paragraph after the table. +Same style is applied, but with a smaller font.} + +\item{template_file}{character(1). Template file that \code{officer} will use as a starting +point for the final document. It will attach the table and use the defaults defined in +the template file. Output will be doc \code{file} nonetheless.} + +\item{section_properties}{\code{officer::prop_section} object. Here you can set margins and page +size.} +} +\description{ +From a table, produce a self-contained word document or attach it to a template word +file (\code{template_file}). This function is based on \code{\link[=tt_to_flextable]{tt_to_flextable()}} transformer and +\code{officer} package. +} +\section{Functions}{ +\itemize{ +\item \code{section_properties_portrait()}: helper function that defines standard portrait properties for tables. + +\item \code{section_properties_landscape()}: helper function that defines standard landscape properties for tables. + +\item \code{margins_potrait()}: helper function that defines standard portrait margins for tables. + +\item \code{margins_landscape()}: helper function that defines standard landscape margins for tables. + +}} +\note{ +\code{export_as_docx()} does not have many options available. We suggest, if you need +specific formats and details to use \code{\link[=tt_to_flextable]{tt_to_flextable()}} first and then \code{export_as_docx}. +Only \code{title_as_header} and \code{footer_as_text} need to be specified again if changed in +\code{tt_to_flextable()}. +} +\examples{ +lyt <- basic_table() \%>\% + split_cols_by("ARM") \%>\% + analyze(c("AGE", "BMRKR2", "COUNTRY")) + +tbl <- build_table(lyt, ex_adsl) + +# See how section_properties_portrait function is built for custom +\dontrun{ +tf <- tempfile(fileext = ".docx") +export_as_docx(tbl, file = tf, section_properties = section_properties_portrait()) +} + +} +\seealso{ +\code{\link[=tt_to_flextable]{tt_to_flextable()}} +} diff --git a/man/tt_to_flextable.Rd b/man/tt_to_flextable.Rd index f3b0bcef7..64ecd92a6 100644 --- a/man/tt_to_flextable.Rd +++ b/man/tt_to_flextable.Rd @@ -2,10 +2,17 @@ % Please edit documentation in R/tt_export.R \name{tt_to_flextable} \alias{tt_to_flextable} -\title{Create a \code{FlexTable} object representing an \code{rtables} \code{TableTree}} +\alias{theme_docx_default} +\title{Create a \code{FlexTable} from an \code{rtables} table} \usage{ tt_to_flextable( tt, + theme = theme_docx_default(tt), + border = flextable::fp_border_default(width = 0.5), + indent_size = NULL, + titles_as_header = TRUE, + footers_as_text = FALSE, + counts_in_newline = FALSE, paginate = FALSE, lpp = NULL, cpp = NULL, @@ -15,13 +22,50 @@ tt_to_flextable( max_width = cpp, total_width = 10 ) + +theme_docx_default( + tt = NULL, + font = "Arial", + font_size = 9, + bold = c("header", "content_rows", "label_rows"), + bold_manual = NULL, + border = flextable::fp_border_default(width = 0.5) +) } \arguments{ \item{tt}{\code{TableTree} (or related class). A \code{TableTree} object representing a populated table.} -\item{paginate}{logical(1). Should \code{tt} be paginated and exported as -multiple \code{flextables}. Defaults to \code{FALSE}} +\item{theme}{function(1). Defaults to \code{theme_docx_default(tt)}. It expects a +a theme function that is designed internally as a function of a \code{flextable} object +and changes its layout and style. If set to \code{NULL}, it will produce a table similar +to \code{rtables} default.} + +\item{border}{\code{officer} border object. Defaults to \code{officer::fp_border(width = 0.5)}.} + +\item{indent_size}{integer(1). If \code{NULL}, the default indent size of the table (see +\code{\link[=matrix_form]{matrix_form()}} \code{indent_size}) is used. To work with \code{docx}, any size is multiplied +by 2 mm (5.67 pt) as default.} + +\item{titles_as_header}{logical(1). Defaults to \code{TRUE} for \code{\link[=tt_to_flextable]{tt_to_flextable()}}, so the +table is self-contained as it makes additional header rows for \code{\link[=main_title]{main_title()}} +string and \code{\link[=subtitles]{subtitles()}} character vector (one per element). \code{FALSE} is suggested +for \code{\link[=export_as_docx]{export_as_docx()}}. This adds titles and subtitles as a text paragraph above +the table. Same style is applied.} + +\item{footers_as_text}{logical(1). Defaults to \code{FALSE} for \code{\link[=tt_to_flextable]{tt_to_flextable()}}, so +the table is self-contained with the flextable definition of footnotes. \code{TRUE} is +used for \code{\link[=export_as_docx]{export_as_docx()}} to add the footers as a new paragraph after the table. +Same style is applied, but with a smaller font.} + +\item{counts_in_newline}{logical(1). Defaults to \code{FALSE}. In \code{rtables} text printing +(\code{\link[formatters:tostring]{formatters::toString()}}), the column counts, i.e. \code{(N=xx)}, is always on a new line. +We noticed that for \code{docx} exports could be necessary to have it on the same line.} + +\item{paginate}{logical(1). If you need \code{.docx} export and you use +\code{export_as_docx}, we suggest relying on \code{word} pagination system. Cooperation +between the two mechanisms is not guaranteed. This option splits \code{tt} in different +"pages" as multiple \code{flextables}. Defaults to \code{FALSE}.} \item{lpp}{numeric. Maximum lines per page including (re)printed header and context rows} @@ -45,28 +89,76 @@ the width of the table (plus any table inset) is used. Ignored completely if \code{tf_wrap} is \code{FALSE}.} \item{total_width}{numeric(1). Total width in inches for the resulting -\code{flextable(s)}. Defaults to 5.} +\code{flextable(s)}. Defaults to 10.} + +\item{font}{character(1). Defaults to \code{"Arial"}. If the font is not vailable, \code{flextable} +default is used.} + +\item{font_size}{integer(1). Positive integerish value that defaults to 9.} + +\item{bold}{character vector. It can be any combination of \code{c("header", "content_rows", "label_rows")}. The first one renders all column names bold (not \code{topleft} content). +Second and third option use \code{\link[=make_row_df]{make_row_df()}} to render content or/and label +rows as bold.} + +\item{bold_manual}{named list. List of indexes lists. See example for needed structure. +Accepted groupings/names are \code{c("header", "body")}.} } \value{ -a \code{flextable} object +a \code{flextable} object. } \description{ -Create a \code{FlexTable} object representing an \code{rtables} \code{TableTree} +Principally used for export (\code{\link[=export_as_docx]{export_as_docx()}}), this function produces a \code{flextable} +from an \code{rtables} table. If \code{theme = NULL}, \code{rtables}-like style will be used. Otherwise, +\code{\link[=theme_docx_default]{theme_docx_default()}} will produce a \code{.docx}-friendly table. } +\section{Functions}{ +\itemize{ +\item \code{theme_docx_default()}: main theme function for \code{\link[=export_as_docx]{export_as_docx()}} + +}} \examples{ analysisfun <- function(x, ...) { - in_rows(row1 = 5, - row2 = c(1, 2), - .row_footnotes = list(row1 = "row 1 - row footnote"), - .cell_footnotes = list(row2 = "row 2 - cell footnote")) + in_rows( + row1 = 5, + row2 = c(1, 2), + .row_footnotes = list(row1 = "row 1 - row footnote"), + .cell_footnotes = list(row2 = "row 2 - cell footnote") + ) } -lyt <- basic_table(title = "Title says Whaaaat", subtitles = "Oh, ok.", - main_footer = "ha HA! Footer!") \%>\% -split_cols_by("ARM") \%>\% -analyze("AGE", afun = analysisfun) +lyt <- basic_table( + title = "Title says Whaaaat", subtitles = "Oh, ok.", + main_footer = "ha HA! Footer!" +) \%>\% + split_cols_by("ARM") \%>\% + analyze("AGE", afun = analysisfun) + +tbl <- build_table(lyt, ex_adsl) +# rtables style +tt_to_flextable(tbl, theme = NULL) + +tt_to_flextable(tbl, theme = theme_docx_default(tbl, font_size = 7)) + +# Custom theme +special_bold <- list( + "header" = list("i" = 1, "j" = c(1, 3)), + "body" = list("i" = c(1, 2), "j" = 1) +) +custom_theme <- theme_docx_default(tbl, + font_size = 10, + font = "Brush Script MT", + border = flextable::fp_border_default(color = "pink", width = 2), + bold = NULL, + bold_manual = special_bold +) +tt_to_flextable(tbl, + border = flextable::fp_border_default(color = "pink", width = 2), + theme = custom_theme +) + +} +\seealso{ +\code{\link[=export_as_docx]{export_as_docx()}} -tbl <- build_table(lyt, ex_adsl) -ft <- tt_to_flextable(tbl) -ft +\code{\link[=export_as_docx]{export_as_docx()}} } diff --git a/tests/testthat/test-exporters.R b/tests/testthat/test-exporters.R index d977fa29d..2625ab640 100644 --- a/tests/testthat/test-exporters.R +++ b/tests/testthat/test-exporters.R @@ -188,34 +188,6 @@ test_that("exporting pdf does the inset", { }) -test_that("flextable export works", { - - analysisfun <- function(x, ...) { - in_rows(row1 = 5, - row2 = c(1, 2), - .row_footnotes = list(row1 = "row 1 - row footnote"), - .cell_footnotes = list(row2 = "row 2 - cell footnote")) - } - - lyt <- basic_table() %>% - split_cols_by("ARM") %>% - split_cols_by("SEX", split_fun = keep_split_levels(c("M", "F"))) %>% - split_rows_by("STRATA1") %>% - summarize_row_groups() %>% - split_rows_by("RACE", split_fun = keep_split_levels(c("WHITE", "ASIAN"))) %>% - analyze("AGE", afun = analysisfun) - - - tbl <- build_table(lyt, ex_adsl) - ft <- tt_to_flextable(tbl, total_width = 20) - expect_equal(sum(unlist(nrow(ft))), 20) - ft - - ft2 <- tt_to_flextable(tbl, paginate = TRUE, lpp = 20, verbose = TRUE) - expect_equal(length(ft2), 6) -}) - - test_that("as_html smoke test", { tmpf <- tempfile(fileext = ".html") @@ -251,3 +223,125 @@ test_that("export_as_rtf works", { res <- export_as_rtf(tbl, file = tmpf) expect_true(file.exists(tmpf)) }) +# Flextable and docx support --------------------------------------------------- +test_that("Can create flextable object that works with different styles", { + analysisfun <- function(x, ...) { + in_rows( + row1 = 5, + row2 = c(1, 2), + .row_footnotes = list(row1 = "row 1 - row footnote"), + .cell_footnotes = list(row2 = "row 2 - cell footnote") + ) + } + + lyt <- basic_table() %>% + split_cols_by("ARM") %>% + split_cols_by("SEX", split_fun = keep_split_levels(c("M", "F"))) %>% + split_rows_by("STRATA1") %>% + summarize_row_groups() %>% + split_rows_by("RACE", split_fun = keep_split_levels(c("WHITE", "ASIAN"))) %>% + analyze("AGE", afun = analysisfun) + + + tbl <- build_table(lyt, ex_adsl) + ft <- tt_to_flextable(tbl, total_width = 20) + expect_equal(sum(unlist(nrow(ft))), 20) + + ft2 <- tt_to_flextable(tbl, paginate = TRUE, lpp = 20, verbose = TRUE) + expect_equal(length(ft2), 6) + + expect_silent(ft3 <- tt_to_flextable(tbl, theme = NULL)) + + # Custom theme + special_bold <- list( + "header" = list("i" = c(1, 2), "j" = c(1, 3)), + "body" = list("i" = c(1, 2), "j" = 1) + ) + custom_theme <- theme_docx_default(tbl, + font_size = 10, + font = "Brush Script MT", + border = officer::fp_border(color = "pink", width = 2), + bold = NULL, + bold_manual = special_bold + ) + expect_silent(tt_to_flextable(tbl, theme = custom_theme)) + + # Custom theme error + special_bold <- list( + "header" = list("asdai" = c(1, 2), "j" = c(1, 3)), + "body" = list("i" = c(1, 2), "j" = 1) + ) + custom_theme <- theme_docx_default(tbl, + font_size = 10, + font = "Brush Script MT", + bold = NULL, + bold_manual = special_bold + ) + expect_error(tt_to_flextable(tbl, theme = custom_theme), regexp = "header") + + + # header colcounts not in a newline works + topleft_t1 <- topleft_t2 <- basic_table(show_colcounts = TRUE) %>% + split_rows_by("ARM", label_pos = "topleft") %>% + split_cols_by("STRATA1") + + topleft_t1 <- topleft_t1 %>% + analyze("BMRKR1") %>% + build_table(DM) + topleft_t1a <- tt_to_flextable(topleft_t1, counts_in_newline = FALSE) + topleft_t1b <- tt_to_flextable(topleft_t1, counts_in_newline = TRUE) + + topleft_t2 <- topleft_t2 %>% + split_rows_by("SEX", label_pos = "topleft") %>% + analyze("BMRKR1") %>% + build_table(DM) %>% + tt_to_flextable(counts_in_newline = FALSE) + + expect_equal(flextable::nrow_part(topleft_t2, part = "header"), 2L) + expect_equal(flextable::nrow_part(topleft_t1a, part = "header"), 1L) + expect_equal(flextable::nrow_part(topleft_t1b, part = "header"), 2L) + + + # internal package check + not_a_pkg <- "bwrereloakdosirabttjtaeerr" + expect_error(check_required_packages(c("flextable", not_a_pkg)), not_a_pkg) +}) + +test_that("export_as_doc works thanks to tt_to_flextable", { + lyt <- make_big_lyt() + tbl <- build_table(lyt, rawdat) + top_left(tbl) <- "Ethnicity" + main_title(tbl) <- "Main title" + subtitles(tbl) <- c("Some Many", "Subtitles") + main_footer(tbl) <- c("Some Footer", "Mehr") + prov_footer(tbl) <- "Some prov Footer" + fnotes_at_path(tbl, rowpath = c("RACE", "BLACK")) <- "factor 2" + fnotes_at_path(tbl, + rowpath = c("RACE", "BLACK"), + colpath = c("ARM", "ARM1", "SEX", "F") + ) <- "factor 3" + + # Get the flextable + flex_tbl <- tt_to_flextable(tbl, titles_as_header = TRUE, footers_as_text = FALSE) + + doc_file <- tempfile(fileext = ".docx") + + expect_silent(export_as_docx(tbl, + file = doc_file, doc_metadata = list("title" = "meh"), + template_file = doc_file, + section_properties = section_properties_portrait() + )) + # flx table in input + expect_silent(export_as_docx(flex_tbl, + file = doc_file, doc_metadata = list("title" = "meh"), + template_file = doc_file, + section_properties = section_properties_portrait() + )) + expect_silent(export_as_docx(tbl, + file = doc_file, doc_metadata = list("title" = "meh"), + template_file = doc_file, + section_properties = section_properties_landscape() + )) + + expect_true(file.exists(doc_file)) +})