From edc40595c864aedf3e89dc97ffb89a15a4977e5a Mon Sep 17 00:00:00 2001 From: kbrevoort Date: Thu, 9 Nov 2023 23:08:35 -0500 Subject: [PATCH 1/5] Fix issue #1465 Replaces the use of percentage symbols to express column widths, which cause an error when compiling Latex, with column widths that are expressed as a share of `\linewidth` or the user-supplied `table_width`. --- R/utils_render_latex.R | 39 +++++++++++ tests/testthat/test-cols_width.R | 108 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/R/utils_render_latex.R b/R/utils_render_latex.R index 0ae6ed157d..77c9eaac57 100644 --- a/R/utils_render_latex.R +++ b/R/utils_render_latex.R @@ -183,6 +183,45 @@ create_table_start_l <- function(data) { ">{\\raggedright\\arraybackslash}" ) + # Check if column width was set using gt::pct and + # convert to Latex friendly terminology (i.e., + # '14.7%' becomes '0.147\\linewidth') + if (grepl('^[[:digit:].]+%$', col_widths[i])) { + + table_width <- dt_options_get_value(data = data, option = 'table_width') + + col_pct <- as.numeric(gsub('%$', '', col_widths[i])) / 100 + + if (table_width == 'auto') { + + # Table width not specified, use all available space + col_scalar <- col_pct + tab_unit <- '\\linewidth' + + } else if (endsWith(table_width, suffix = '%')) { + + # If table width is expressed as a percentage, adjust the scaler + col_scalar <- col_pct * as.numeric(gsub('%', '', table_width)) / 100 + tab_unit <- '\\linewidth' + + } else { + + # When table width is expressed in units, convert to points + col_scalar <- col_pct * convert_to_px(table_width) * 0.75 # 0.75 converts pixels to points + tab_unit <- 'pt' + + } + + col_widths[i] <- + paste0( + "\\dimexpr ", + col_scalar, + tab_unit, + "-2\\tabcolsep-1.5\\arrayrulewidth" + ) + + } + col_defs_i <- paste0(align, "p{", col_widths[i], "}") } else { diff --git a/tests/testthat/test-cols_width.R b/tests/testthat/test-cols_width.R index f96bb06276..c9262a4559 100644 --- a/tests/testthat/test-cols_width.R +++ b/tests/testthat/test-cols_width.R @@ -897,3 +897,111 @@ test_that("The function `cols_width()` works correctly with a complex table", { ) %>% expect_true() }) + +test_that("The function `cols_width()` correctly specifies LaTeX table when column widths are specified by user as percentages", { + + # Check that specific suggested packages are available + check_suggests() + + # Create a `tbl_latex` object with `gt()` and size + # all columns in percentages + tbl_latex <- + gt(tbl) %>% + cols_width( + col_1 ~ pct(50), + col_2 ~ pct(30), + col_3 ~ pct(20), + col_4 ~ pct(10) + ) + + pct_string <- function(x, unit = '\\\\linewidth') { + + prefix <- '>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash\\}' + + sprintf( + '%sp\\{\\\\dimexpr %s%s-2\\\\tabcolsep-1.5\\\\arrayrulewidth\\}', + prefix, + format(x, scientific = FALSE, trim = TRUE), + unit + ) + + } + + build_longtable_regex <- function(...) { + + paste0( + c( + "^\\\\begin\\{longtable\\}\\{", + c(...), + "\\}\\n" + ), + collapse = '' + ) + + } + + latex_col_regex <- + paste0( + c( + '^\\\\begin\\{longtable\\}\\{', + # '>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash' + sprintf('>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash\\}p\\{\\\\dimexpr 0\\.%d\\\\linewidth-2\\\\tabcolsep-1.5\\\\arrayrulewidth}', + c(5L, 3L, 2L, 1L)), + '\\}\\n' + ), + collapse = '' + ) + + # Expect that all column widths are expressed as percentage of \linewidth + c(0.5, 0.3, 0.2, 0.1) %>% + pct_string() %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex)) %>% + expect_true() + + + # Check that LaTeX is correctly generated when only some + # column widths are specified as percentages + tbl_latex_partial <- + gt(tbl) %>% + cols_width( + col_1 ~ pct(30), + col_3 ~ pct(20) + ) + + c( + pct_string(0.3), + 'r', + pct_string(0.2), + 'r' + ) %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_partial)) %>% + expect_true() + + # Check that LaTeX longtable command is correctly generated + # when table_width is specified by the user as a percentage + tbl_latex_tw_pct <- + tbl_latex %>% + tab_options(table.width = pct(70)) + + (0.7 * c(0.5, 0.3, 0.2, 0.1)) %>% + pct_string() %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_tw_pct)) %>% + expect_true() + + # Check that LaTeX longtable command is correctly generated + # when table width is specified by user in pixels + tbl_latex_tw_px <- + tbl_latex %>% + tab_options(table.width = '400px') + + (400 * 0.75 * c(0.5, 0.3, 0.2, 0.1)) %>% + pct_string(unit = 'pt') %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_tw_px)) + + +}) + From 4a58df9f6fc5f7d2d79ea91a6a40d2511f981a6e Mon Sep 17 00:00:00 2001 From: kbrevoort Date: Fri, 17 Nov 2023 22:33:50 -0500 Subject: [PATCH 2/5] Fix issue #119 (and #329) Anchors LaTeX longtable to match a width set by the user. Table width is achieved by changing the space between columns to produce a table that matches the specified width. --- R/export.R | 4 +++ R/utils_render_latex.R | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/R/export.R b/R/export.R index 7cc57939cc..92ee01f6ae 100644 --- a/R/export.R +++ b/R/export.R @@ -725,15 +725,19 @@ as_latex <- function(data) { latex_packages <- NULL } + table_width_bookends <- derive_table_width_bookends(data = data) + # Compose the LaTeX table knitr::asis_output( paste0( + table_width_bookends[1L], table_start, heading_component, columns_component, body_component, table_end, footer_component, + table_width_bookends[2L], collapse = "" ), meta = latex_packages diff --git a/R/utils_render_latex.R b/R/utils_render_latex.R index 77c9eaac57..49d2990a5a 100644 --- a/R/utils_render_latex.R +++ b/R/utils_render_latex.R @@ -244,11 +244,18 @@ create_table_start_l <- function(data) { paste0(col_defs[seq_along(stub_layout)], "|") } + # If a table width is specified, add an extra column + # space to fill in enough space to match the width + extra_sep <- '' + if (dt_options_get_value(data = data, option = 'table_width') != 'auto') + extra_sep <- '@{\\extracolsep{\\fill}}' + # Generate setup statements for table including default left # alignments and vertical lines for any stub columns paste0( longtable_post_length, "\\begin{longtable}{", + extra_sep, paste(col_defs, collapse = ""), "}\n", collapse = "" @@ -1142,3 +1149,63 @@ split_row_content <- function(x) { split(row_content, ceiling(seq_along(row_content) / ncol(x))) } + +derive_table_width_bookends <- function(data) { + + table_width <- dt_options_get_value(data = data, 'table_width') + + # Bookends are not required if a table width is not specified + if (table_width == 'auto') { + + bookends <- c('', '') + + } else if (endsWith(table_width, "%")) { + + tw <- as.numeric(gsub('%', '', table_width)) + + side_width <- + ((100 - tw) / 200) %>% + format(scientific = FALSE, trim = TRUE) + + bookends <- + c( + sprintf( + '\\setlength\\LTleft{%s\\linewidth}\n\\setlength\\LTright{%s\\linewidth}\n', + side_width, + side_width + ), + "" + ) + + } else { + + width_in_pt <- 0.75 * convert_to_px(table_width) + + halfwidth_in_pt <- format(width_in_pt / 2, scientific = FALSE, trim = TRUE) + + bookends <- + c( + paste0( + "\\newlength\\holdLTleft", + "\\newlength\\holdLTright", + "\\setlength\\holdLTleft{\\LTleft}\\relax", + "\\setlength\\holdLTright{\\LTright}\\relax", + sprintf( + "\\setlength\\LTleft{\\dimexpr(0.5\\linewidth - %spt)}\n\\setlength\\LTright{\\dimexpr(0.5\\linewidth - %spt)}", + halfwidth_in_pt, + halfwidth_in_pt + ), + collapse = '\n' + ), + paste0( + "\\setlength\\LTleft{\\holdLTleft}", + "\\setlength\\LTright{\\holdLTright}", + collapse = "\n" + ) + ) + + } + + bookends + +} From a6f801c4e4d0e8d6a4893314a0e844f4003afd14 Mon Sep 17 00:00:00 2001 From: kbrevoort Date: Sat, 18 Nov 2023 22:27:26 -0500 Subject: [PATCH 3/5] Update existing unit tests for compatibility with specifying table width in LaTeX --- tests/testthat/test-cols_width.R | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-cols_width.R b/tests/testthat/test-cols_width.R index c9262a4559..43e830f156 100644 --- a/tests/testthat/test-cols_width.R +++ b/tests/testthat/test-cols_width.R @@ -916,7 +916,7 @@ test_that("The function `cols_width()` correctly specifies LaTeX table when colu pct_string <- function(x, unit = '\\\\linewidth') { - prefix <- '>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash\\}' + prefix <- '>\\{\\\\(raggedright|raggedleft|centering)\\\\arraybackslash\\}' sprintf( '%sp\\{\\\\dimexpr %s%s-2\\\\tabcolsep-1.5\\\\arrayrulewidth\\}', @@ -931,7 +931,8 @@ test_that("The function `cols_width()` correctly specifies LaTeX table when colu paste0( c( - "^\\\\begin\\{longtable\\}\\{", + "\\\\begin\\{longtable\\}\\{", + "(@\\{\\\\extracolsep\\{\\\\fill\\}\\})*", c(...), "\\}\\n" ), @@ -943,11 +944,12 @@ test_that("The function `cols_width()` correctly specifies LaTeX table when colu latex_col_regex <- paste0( c( - '^\\\\begin\\{longtable\\}\\{', + "\\\\begin\\{longtable\\}\\{", + "(@\\{\\\\extracolsep\\{\\\\fill\\}\\})*", # '>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash' - sprintf('>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash\\}p\\{\\\\dimexpr 0\\.%d\\\\linewidth-2\\\\tabcolsep-1.5\\\\arrayrulewidth}', + sprintf(">\\{\\\\(raggedright|raggedleft|centering)\\\\arraybackslash\\}p\\{\\\\dimexpr 0\\.%d\\\\linewidth-2\\\\tabcolsep-1.5\\\\arrayrulewidth}", c(5L, 3L, 2L, 1L)), - '\\}\\n' + "\\}\\n" ), collapse = '' ) From bbbed3509f08cfd3480d4b35c25042297a9bd122 Mon Sep 17 00:00:00 2001 From: kbrevoort Date: Sun, 19 Nov 2023 23:21:07 -0500 Subject: [PATCH 4/5] Complete specification of LaTeX bookends --- R/utils_render_latex.R | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/R/utils_render_latex.R b/R/utils_render_latex.R index 49d2990a5a..d4120d4e85 100644 --- a/R/utils_render_latex.R +++ b/R/utils_render_latex.R @@ -1169,12 +1169,19 @@ derive_table_width_bookends <- function(data) { bookends <- c( - sprintf( - '\\setlength\\LTleft{%s\\linewidth}\n\\setlength\\LTright{%s\\linewidth}\n', - side_width, - side_width + paste0( + "\\newlength\\holdLTleft", + "\\newlength\\holdLTright", + "\\setlength\\holdLTleft{\\LTleft}\\relax", + "\\setlength\\holdLTright{\\LTright}\\relax", + sprintf( + '\\setlength\\LTleft{%s\\linewidth}\n\\setlength\\LTright{%s\\linewidth}', + side_width, + side_width + ), + collapse = "\n" ), - "" + "\\setlength\\LTleft{\\holdLTleft}\n\\setlength\\LTright{\\holdLTright}" ) } else { From 374230ea99e86166ca688866f9ce7266b70c9fb7 Mon Sep 17 00:00:00 2001 From: kbrevoort Date: Sun, 19 Nov 2023 23:21:29 -0500 Subject: [PATCH 5/5] Add tests for LaTeX table widths --- tests/testthat/test-as_latex.R | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/testthat/test-as_latex.R diff --git a/tests/testthat/test-as_latex.R b/tests/testthat/test-as_latex.R new file mode 100644 index 0000000000..17e74a2380 --- /dev/null +++ b/tests/testthat/test-as_latex.R @@ -0,0 +1,50 @@ +test_that("Table width correctly output in LaTeX", { + + gt_latex_width_1 <- + gt(exibble) %>% + tab_options(table.width = pct(90)) %>% + as_latex() + + start_pt <- regexpr("begin\\{longtable", gt_latex_width_1) + + expect_gt(start_pt, 0) # Verifies the long table command appears in the text + + end_pt <- regexpr("end\\{longtable", gt_latex_width_1) + + expect_gt(end_pt, 0) + + # Verify that the holdLTleft and holdLTright variables are defined and set + latex_prefix <- substr(gt_latex_width_1, 1L, start_pt) + + expect_match(latex_prefix, "\\\\newlength\\\\holdLTleft") + + expect_match(latex_prefix, "\\\\newlength\\\\holdLTright") + + expect_match(latex_prefix, "\\\\setlength\\\\holdLTleft\\{\\\\LTleft\\}\\\\relax") + + expect_match(latex_prefix, "\\\\setlength\\\\holdLTright\\{\\\\LTright\\}\\\\relax") + + # Verify that LTleft and LTright are correctly specified + expect_match(latex_prefix, "\\\\setlength\\\\LTleft\\{0.05\\\\linewidth\\}") + + expect_match(latex_prefix, "\\\\setlength\\\\LTright\\{0.05\\\\linewidth\\}") + + # Verify that after the longtable environment, the LTleft and LT right are + # changed back to their previous values + latex_suffix <- substr(gt_latex_width_1, end_pt, nchar(gt_latex_width_1)) + + expect_match(latex_suffix, "\\\\setlength\\\\LTleft\\{\\\\holdLTleft\\}") + + expect_match(latex_suffix, "\\\\setlength\\\\LTright\\{\\\\holdLTright\\}") + + # Test specification of a table width in pixels + gt_latex_width_2 <- + gt(exibble) %>% + tab_options(table.width = '600px') %>% + as_latex() + + expect_match(gt_latex_width_2, "\\\\setlength\\\\LTleft\\{\\\\dimexpr\\(0.5\\\\linewidth - 225pt\\)\\}") + + expect_match(gt_latex_width_2, "\\\\setlength\\\\LTright\\{\\\\dimexpr\\(0.5\\\\linewidth - 225pt\\)\\}") + +})