diff --git a/NAMESPACE b/NAMESPACE index 259bc361fc..096f6d8a7e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ export(as_raw_html) export(as_rtf) export(cells_column_labels) export(cells_data) +export(cells_grand_summary) export(cells_group) export(cells_stub) export(cells_styles) diff --git a/R/as_rtf.R b/R/as_rtf.R index 5d818dfc20..74377f58c9 100644 --- a/R/as_rtf.R +++ b/R/as_rtf.R @@ -179,7 +179,9 @@ as_rtf <- function(data) { # Perform any necessary column merge operations col_merge_output <- - perform_col_merge(col_merge, data_df, output_df, boxh_df, columns_df) + perform_col_merge( + col_merge, data_df, output_df, boxh_df, columns_df, context + ) # Rewrite `output_df`, `boxh_df`, and `columns_df` as a result of merging output_df <- col_merge_output$output_df @@ -188,7 +190,7 @@ as_rtf <- function(data) { # Create the `list_of_summaries` list of lists list_of_summaries <- - create_summary_dfs(summary_list, data_df, stub_df, output_df) + create_summary_dfs(summary_list, data_df, stub_df, output_df, context) # Determine if there is a populated stub stub_available <- is_stub_available(stub_df) diff --git a/R/build_data.R b/R/build_data.R index f9eac167d5..d10263b023 100644 --- a/R/build_data.R +++ b/R/build_data.R @@ -156,7 +156,9 @@ build_data <- function(data, context) { # Perform any necessary column merge operations col_merge_output <- - perform_col_merge(col_merge, data_df, output_df, boxh_df, columns_df) + perform_col_merge( + col_merge, data_df, output_df, boxh_df, columns_df, context + ) # Rewrite `output_df`, `boxh_df`, and `columns_df` as a result of merging output_df <- col_merge_output$output_df @@ -165,7 +167,7 @@ build_data <- function(data, context) { # Create the `list_of_summaries` list of lists list_of_summaries <- - create_summary_dfs(summary_list, data_df, stub_df, output_df) + create_summary_dfs(summary_list, data_df, stub_df, output_df, context) # Determine if there is a populated stub stub_available <- is_stub_available(stub_df) diff --git a/R/extract_summary.R b/R/extract_summary.R index 9265bc4172..53af783603 100644 --- a/R/extract_summary.R +++ b/R/extract_summary.R @@ -59,86 +59,23 @@ #' @export extract_summary <- function(data) { - # Extract all attributes from the data object into `data_attr` + # Extract all attributes from the `data` + # object into `data_attr` data_attr <- attributes(data) + # Stop function if there are no + # directives to create summary rows if (is.null(data_attr$summary)) { - stop("There is no summary data frame to extract.", call. = FALSE) + stop("There is no summary list to extract.\n", + "Use the `summary_rows()` function to generate summaries.", + call. = FALSE) } - # Move original data frame to `data_df` - data_df <- as.data.frame(data) + # Build the `data` using the standard + # pipeline with the `html` context + built_data <- build_data(data = data, context = "html") - # Get the `boxh_df` data frame - boxh_df <- data_attr$boxh_df - - # Get the `stub_df` data frame - stub_df <- data_attr$stub_df - - # Get the `rows_df` data frame - rows_df <- data_attr$rows_df - - # Get the `cols_df` data frame - cols_df <- data_attr$cols_df - - # Get the `formats` list - formats <- data_attr$formats - - # Get the `arrange_groups` vector - arrange_groups <- data_attr$arrange_groups - - # Get the `others_group` vector - others_group <- data_attr$others_group[[1]] %||% NA_character_ - - # Get the `col_merge` object - col_merge <- data_attr$col_merge - - # Get the `summary_list` object - summary_list <- data_attr$summary - - # Initialize `output_df` - output_df <- initialize_output_df(data_df) - - # Create `output_df` with rendered values - output_df <- render_formats(output_df, data_df, formats, context = "html") - - # Move input data cells to `output_df` that didn't have - # any rendering applied during `render_formats()` - output_df <- migrate_unformatted_to_output(data_df, output_df, context = "html") - - # Get the reordering df (`rows_df`) for the data rows - rows_df <- get_row_reorder_df(arrange_groups, stub_df) - - # Get the `columns_df` data frame for the data columns - columns_df <- get_column_reorder_df(cols_df, boxh_df) - - # Reassemble the rows and columns of `data_df` in the correct order - output_df <- reassemble_output_df(output_df, rows_df, columns_df) - - # Get the `groups_df` data frame, which is a rearranged representation - # of the stub `groupname` and `rowname` columns - groups_df <- get_groupnames_rownames_df(stub_df, rows_df) - - # Replace NA values in the `groupname` column if there is a reserved - # label for the unlabeled group - groups_df[is.na(groups_df[, "groupname"]), "groupname"] <- others_group - - # Create the `groups_rows_df` data frame, which provides information - # on which rows the group rows should appear above - groups_rows_df <- get_groups_rows_df(arrange_groups, groups_df) - - # Perform any necessary column merge operations - col_merge_output <- - perform_col_merge(col_merge, data_df, output_df, boxh_df, columns_df) - - # Rewrite `output_df`, `boxh_df`, and `columns_df` as a result of merging - output_df <- col_merge_output$output_df - boxh_df <- col_merge_output$boxh_df - columns_df <- col_merge_output$columns_df - - # Create the `list_of_summaries` list of lists - list_of_summaries <- - create_summary_dfs(summary_list, data_df, stub_df, output_df) - - list_of_summaries$summary_df_data_list + # Extract the list of summary data frames + # that contains tidy, unformatted data + built_data$list_of_summaries$summary_df_data_list } diff --git a/R/format_data.R b/R/format_data.R index 9ff58d01ed..828388f595 100644 --- a/R/format_data.R +++ b/R/format_data.R @@ -1294,11 +1294,11 @@ fmt_missing <- function(data, fns = list( html = function(x) { - if (missing_text == "---") { - missing_text <- "\u2014" - } else if (missing_text == "--") { - missing_text <- "\u2013" - } + missing_text <- + context_missing_text( + missing_text = missing_text, + context = "html" + ) # Any values of `x` that are `NA` get # `missing_text` as output; any values that diff --git a/R/gt_options_default.R b/R/gt_options_default.R index 555211ea70..c465f656fb 100644 --- a/R/gt_options_default.R +++ b/R/gt_options_default.R @@ -33,12 +33,15 @@ gt_options_default <- function() { "table_body_border_bottom_style", TRUE, "table_body", "solid", "table_body_border_bottom_width", TRUE, "table_body", "2px", "table_body_border_bottom_color", TRUE, "table_body", "#A8A8A8", - "row_padding", TRUE, "row", "10px", + "row_padding", TRUE, "row", "8px", "row_striping_include_stub", TRUE, "row", "TRUE", "row_striping_include_table_body", TRUE, "row", "TRUE", "summary_row_background_color", TRUE, "summary_row", NA_character_, - "summary_row_padding", TRUE, "summary_row", "6px", + "summary_row_padding", TRUE, "summary_row", "8px", "summary_row_text_transform", TRUE, "summary_row", "inherit", + "grand_summary_row_background_color", TRUE, "grand_summary_row", NA_character_, + "grand_summary_row_padding", TRUE, "grand_summary_row", "8px", + "grand_summary_row_text_transform", TRUE, "grand_summary_row", "inherit", "footnote_sep", FALSE, "footnote", "
", "footnote_glyph", FALSE, "footnote", "numbers", "footnote_font_size", TRUE, "footnote", "90%", diff --git a/R/helpers.R b/R/helpers.R index 4bd858b25a..b6fbfbf271 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -364,6 +364,30 @@ cells_summary <- function(groups = NULL, cells } +#' @rdname location_cells +#' @import rlang +#' @export +cells_grand_summary <- function(columns = NULL, + rows = NULL) { + + # Capture expressions for the `columns` + # and `rows` arguments + col_expr <- rlang::enquo(columns) + row_expr <- rlang::enquo(rows) + + # Create the `cells_grand_summary` object + cells <- + list( + columns = col_expr, + rows = row_expr) + + # Apply the `cells_grand_summary` and + # `location_cells` classes + class(cells) <- c("cells_grand_summary", "location_cells") + + cells +} + #' Interpret input text as Markdown-formatted text #' @param text the text that is understood to contain Markdown formatting. #' @return a character object that is tagged for a Markdown-to-HTML diff --git a/R/modify_columns.R b/R/modify_columns.R index 1bf79c7711..d89a97c4a5 100644 --- a/R/modify_columns.R +++ b/R/modify_columns.R @@ -767,6 +767,7 @@ cols_merge <- function(data, attr(data, "col_merge") <- list( pattern = pattern, + sep = "", col_1 = col_1) } @@ -885,6 +886,7 @@ cols_merge_uncert <- function(data, attr(data, "col_merge") <- list( pattern = pattern, + sep = "", col_1 = col_val) } @@ -962,10 +964,11 @@ cols_merge_uncert <- function(data, #' @export cols_merge_range <- function(data, col_begin, - col_end) { + col_end, + sep = "---") { # Set the formatting pattern - pattern <- "{1} \u2014 {2}" + pattern <- "{1} {sep} {2}" col_begin <- enquo(col_begin) col_end <- enquo(col_end) @@ -991,6 +994,9 @@ cols_merge_range <- function(data, attr(data, "col_merge")[["pattern"]] <- c(attr(data, "col_merge")[["pattern"]], pattern) + attr(data, "col_merge")[["sep"]] <- + c(attr(data, "col_merge")[["sep"]], sep) + attr(data, "col_merge")[["col_1"]] <- c(attr(data, "col_merge")[["col_1"]], col_begin) @@ -999,6 +1005,7 @@ cols_merge_range <- function(data, attr(data, "col_merge") <- list( pattern = pattern, + sep = sep, col_1 = col_begin) } diff --git a/R/summary_rows.R b/R/summary_rows.R index 71933ceb02..db345bc761 100644 --- a/R/summary_rows.R +++ b/R/summary_rows.R @@ -1,39 +1,49 @@ #' Add summary rows using aggregation functions #' -#' Add summary rows to one or more row groups by using the input data already -#' provided in the \code{\link{gt}()} function alongside any suitable -#' aggregation functions. Should we need to obtain the summary data for external -#' purposes, the \code{\link{extract_summary}()} can be used with a -#' \code{gt_tbl} object where summary rows were added via \code{summary_rows()}. -#' @param data a table object that is created using the \code{gt()} function. -#' @param groups the row groups labels that identify which summary rows will be -#' added. -#' @param columns the columns for which the summaries should be calculated. If -#' nothing is provided, then the supplied aggregation functions will be -#' applied to all columns. -#' @param fns functions used for aggregations. This can include base functions +#' Add groupwise summary rows to one or more row groups by using the input data +#' already provided in the \code{\link{gt}()} function alongside any suitable +#' aggregation functions. Or, add a grand summary that incorporates all +#' available data, regardless of grouping. You choose how to format the values +#' in the resulting summary cells by use of a \code{formatter} function (e.g, +#' \code{\link{fmt_number}()}) and any relevant options. +#' +#' Should we need to obtain the summary data for external purposes, the +#' \code{\link{extract_summary}()} function can be used with a \code{gt_tbl} +#' object where summary rows were added via \code{summary_rows()}. +#' +#' @param data A table object that is created using the \code{gt()} function. +#' @param groups The groups to consider for generation of groupwise summary +#' rows. By default this is set to \code{NULL}, which results in the formation +#' of grand summary rows (a grand summary operates on all table data). +#' Providing the names of row groups in \code{c()} will create a groupwise +#' summary and generate summary rows for the specified groups. Setting this to +#' \code{TRUE} indicates that all available groups will receive groupwise +#' summary rows. +#' @param columns The columns for which the summaries should be calculated. +#' @param fns Functions used for aggregations. This can include base functions #' like \code{mean}, \code{min}, \code{max}, \code{median}, \code{sd}, or #' \code{sum} or any other user-defined aggregation function. The function(s) #' should be supplied within a \code{list()}. Within that list, we can specify -#' the functions by use of function names (e.g., \code{"sum"}), the functions -#' themselves (e.g., \code{sum}), or one-sided R formulas by prefacing with a -#' \code{~} where \code{.} serves as the data to be summarized (e.g., -#' \code{sum(., na.rm = TRUE)}). By using named arguments, the names will -#' serve as row labels for the corresponding summary rows (otherwise the -#' labels will be derived from the function names). -#' @param missing_text the text to be used in place of \code{NA} values in +#' the functions by use of function names in quotes (e.g., \code{"sum"}), as +#' bare functions (e.g., \code{sum}), or as one-sided R formulas using a +#' leading \code{~}. In the formula representation, a \code{.} serves as the +#' data to be summarized (e.g., \code{sum(., na.rm = TRUE)}). The use of named +#' arguments is recommended as the names will serve as summary row labels for +#' the corresponding summary rows data (the labels can derived from the +#' function names but only when not providing bare function names). +#' @param missing_text The text to be used in place of \code{NA} values in #' summary cells with no data outputs. -#' @param formatter a formatter function name. These can be any of the +#' @param formatter A formatter function name. These can be any of the #' \code{fmt_*()}functions available in the package (e.g., #' \code{\link{fmt_number}()}, \code{link{fmt_percent}()}, etc.), or a custom #' function using \code{\link{fmt}()}. The default function is #' \code{\link{fmt_number}()} and its options can be accessed through #' \code{...}. -#' @param ... values passed to the \code{formatter} function, where the provided +#' @param ... Values passed to the \code{formatter} function, where the provided #' values are to be in the form of named vectors. For example, when using the #' default \code{formatter} function, \code{\link{fmt_number}()}, options such #' as \code{decimals}, \code{use_seps}, and \code{locale} can be used. -#' @return an object of class \code{gt_tbl}. +#' @return An object of class \code{gt_tbl}. #' @examples #' # Use `sp500` to create a gt table with #' # row groups; create summary rows (`min`, @@ -73,7 +83,7 @@ #' @export summary_rows <- function(data, groups = NULL, - columns = NULL, + columns = TRUE, fns, missing_text = "---", formatter = fmt_number, @@ -82,40 +92,206 @@ summary_rows <- function(data, # Collect all provided formatter options in a list formatter_options <- list(...) - if (is.null(groups)) { - groups <- TRUE + # If `groups` is FALSE, then do nothing; just + # return the `data` unchanged; having `groups` + # as `NULL` signifies a grand summary, `TRUE` + # is used for groupwise summaries across all + # groups + if (is_false(groups)) { + return(data) } - columns <- enquo(columns) + # Get the `stub_df` object from `data` + stub_df <- attr(data, "stub_df", exact = TRUE) + # Resolve the column names + columns <- enquo(columns) columns <- resolve_vars(var_expr = !!columns, data = data) - if ("summary" %in% names(attributes(data))) { + # If there isn't a stub available, create an + # 'empty' stub (populated with empty strings); + # the stub is necessary for summary row labels + if (!is_stub_available(stub_df) && is.null(groups)) { - attr(data, "summary") <- - c( - attr(data, "summary"), - list( - list( - groups = groups, - columns = columns, - fns = fns, - missing_text = missing_text, - formatter = formatter, - formatter_options = formatter_options))) + # Place the `rowname` values into `stub_df$rowname` + stub_df[["rowname"]] <- "" - } else { + attr(data, "stub_df") <- stub_df + } + + # Derive the summary labels + summary_labels <- + vapply(fns, derive_summary_label, FUN.VALUE = character(1)) - attr(data, "summary") <- + # If there are names, use those names + # as the summary labels + if (!is.null(names(summary_labels))) { + summary_labels <- names(summary_labels) + } + + # Append list of summary inputs to the + # `summary` attribute + attr(data, "summary") <- + c( + attr(data, "summary"), list( list( groups = groups, columns = columns, fns = fns, + summary_labels = summary_labels, missing_text = missing_text, formatter = formatter, - formatter_options = formatter_options)) + formatter_options = formatter_options + ) + ) + ) + + data +} + +add_summary_location_row <- function(loc, + data, + text, + df_type = "styles_df") { + + stub_df <- attr(data, "stub_df", exact = TRUE) + + row_groups <- + stub_df[, "groupname"] %>% + unique() + + summary_data <- attr(data, "summary", exact = TRUE) + + summary_data_summaries <- + vapply( + seq(summary_data), + function(x) !is.null(summary_data[[x]]$groups), + logical(1) + ) + + summary_data <- summary_data[summary_data_summaries] + + groups <- + row_groups[resolve_data_vals_idx( + var_expr = !!loc$groups, + data = NULL, + vals = row_groups + )] + + # Adding styles to intersections of group, row, and column; any + # that are missing at render time will be ignored + for (group in groups) { + + summary_labels <- + lapply( + summary_data, + function(summary_data_item) { + if (isTRUE(summary_data_item$groups)) { + summary_data_item$summary_labels + } else if (group %in% summary_data_item$groups){ + summary_data_item$summary_labels + } + } + ) %>% + unlist() %>% + unique() + + columns <- + resolve_vars( + var_expr = !!loc$columns, + data = data + ) + + if (length(columns) == 0) { + stop("The location requested could not be resolved:\n", + " * Review the expression provided as `columns`", + call. = FALSE) + } + + rows <- + resolve_data_vals_idx( + var_expr = !!loc$rows, + data = NULL, + vals = summary_labels + ) + + if (length(rows) == 0) { + stop("The location requested could not be resolved:\n", + " * Review the expression provided as `rows`", + call. = FALSE) + } + + attr(data, df_type) <- + add_location_row( + data, + df_type = df_type, + locname = "summary_cells", + locnum = 5, + grpname = group, + colname = columns, + rownum = rows, + text = text + ) + } + + data +} + +add_grand_summary_location_row <- function(loc, + data, + text, + df_type = "styles_df") { + + summary_data <- attr(data, "summary", exact = TRUE) + + grand_summary_labels <- + lapply(summary_data, function(summary_data_item) { + if (is.null(summary_data_item$groups)) { + return(summary_data_item$summary_labels) + } + + NULL + }) %>% + unlist() %>% + unique() + + columns <- + resolve_vars( + var_expr = !!loc$columns, + data = data + ) + + if (length(columns) == 0) { + stop("The location requested could not be resolved:\n", + " * Review the expression provided as `columns`", + call. = FALSE) } + rows <- + resolve_data_vals_idx( + var_expr = !!loc$rows, + data = NULL, + vals = grand_summary_labels + ) + + if (length(rows) == 0) { + stop("The location requested could not be resolved:\n", + " * Review the expression provided as `rows`", + call. = FALSE) + } + + attr(data, df_type) <- + add_location_row( + data, + df_type = df_type, + locname = "grand_summary_cells", + locnum = 6, + grpname = NA_character_, + colname = columns, + rownum = rows, + text = text + ) + data } diff --git a/R/tab_footnote.R b/R/tab_footnote.R index 937d93beb4..eaee3b47dd 100644 --- a/R/tab_footnote.R +++ b/R/tab_footnote.R @@ -200,23 +200,22 @@ set_footnote.cells_title <- function(loc, data, footnote) { set_footnote.cells_summary <- function(loc, data, footnote) { - groups <- (loc$groups %>% as.character())[-1] - rows <- (loc$rows %>% as.character())[-1] %>% as.integer() - - resolved <- resolve_cells_column_labels(data = data, object = loc) - - cols <- resolved$columns - - colnames <- colnames(as.data.frame(data))[cols] + add_summary_location_row( + loc = loc, + data = data, + text = footnote, + df_type = "footnotes_df" + ) +} - attr(data, "footnotes_df") <- - add_location_row( - data, df_type = "footnotes_df", - locname = "summary_cells", locnum = 5, - grpname = groups, colname = colnames, - rownum = rows, text = footnote) +set_footnote.cells_grand_summary <- function(loc, data, footnote) { - data + add_grand_summary_location_row( + loc = loc, + data = data, + text = footnote, + df_type = "footnotes_df" + ) } #' @importFrom dplyr bind_rows tibble distinct diff --git a/R/tab_options.R b/R/tab_options.R index 5dd2cc0c73..39e15ac117 100644 --- a/R/tab_options.R +++ b/R/tab_options.R @@ -19,11 +19,11 @@ #' units of pixels. The \code{\link{px}()} and \code{\link{pct}()} helper #' functions can also be used to pass in numeric values and obtain values as #' pixel or percent units. -#' @param column_labels.font.weight,row_group.font.weight the font weight of the -#' \code{columns} and \code{row_group} text element. -#' @param summary_row.text_transform an option to apply text transformations to -#' the label text in each summary row. -#' @param table.background.color,heading.background.color,column_labels.background.color,row_group.background.color,summary_row.background.color +#' @param column_labels.font.weight,row_group.font.weight the font weight of +#' the \code{columns} and \code{row_group} text element. +#' @param summary_row.text_transform,grand_summary_row.text_transform an option +#' to apply text transformations to the label text in each summary row. +#' @param table.background.color,heading.background.color,column_labels.background.color,row_group.background.color,summary_row.background.color,grand_summary_row.background.color #' background colors for the parent element \code{table} and the following #' child elements: \code{heading}, \code{columns}, \code{row_group}, #' \code{summary_row}, and \code{table_body}. A color name or a hexadecimal @@ -40,8 +40,8 @@ #' the style, width, and color of the table body's top border. #' @param table_body.border.bottom.style,table_body.border.bottom.width,table_body.border.bottom.color #' the style, width, and color of the table body's bottom border. -#' @param row.padding,summary_row.padding the amount of padding in each row and -#' in each summary row. +#' @param row.padding,summary_row.padding,grand_summary_row.padding the amount +#' of padding in each row and in each type of summary row. #' @param footnote.sep the separating characters between adjacent footnotes in #' the footnotes section. The default value produces a linebreak. #' @param footnote.glyph the set of sequential figures or characters used to @@ -190,6 +190,9 @@ tab_options <- function(data, summary_row.background.color = NULL, summary_row.padding = NULL, summary_row.text_transform = NULL, + grand_summary_row.background.color = NULL, + grand_summary_row.padding = NULL, + grand_summary_row.text_transform = NULL, footnote.sep = NULL, footnote.glyph = NULL, footnote.font.size = NULL, @@ -497,6 +500,30 @@ tab_options <- function(data, opts_df, "summary_row_text_transform", summary_row.text_transform) } + # grand_summary_row.background.color + if (!is.null(grand_summary_row.background.color)) { + + opts_df <- opts_df_set( + opts_df, "grand_summary_row_background_color", grand_summary_row.background.color) + } + + # grand_summary_row.padding + if (!is.null(grand_summary_row.padding)) { + + if (is.numeric(grand_summary_row.padding)) { + grand_summary_row.padding <- paste0(grand_summary_row.padding, "px") + } + + opts_df <- opts_df_set(opts_df, "grand_summary_row_padding", grand_summary_row.padding) + } + + # grand_summary_row.text_transform + if (!is.null(grand_summary_row.text_transform)) { + + opts_df <- opts_df_set( + opts_df, "grand_summary_row_text_transform", grand_summary_row.text_transform) + } + # footnote.sep if (!is.null(footnote.sep)) { diff --git a/R/tab_style.R b/R/tab_style.R index fba6e83efd..f73ecea785 100644 --- a/R/tab_style.R +++ b/R/tab_style.R @@ -233,21 +233,20 @@ set_style.cells_title <- function(loc, data, style) { set_style.cells_summary <- function(loc, data, style) { - groups <- (loc$groups %>% as.character())[-1] - rows <- (loc$rows %>% as.character())[-1] %>% as.integer() - - resolved <- resolve_cells_column_labels(data = data, object = loc) - - cols <- resolved$columns - - colnames <- colnames(as.data.frame(data))[cols] + add_summary_location_row( + loc = loc, + data = data, + text = style, + df_type = "styles_df" + ) +} - attr(data, "styles_df") <- - add_location_row( - data, df_type = "styles_df", - locname = "summary_cells", locnum = 5, - grpname = groups, colname = colnames, - rownum = rows, text = style) +set_style.cells_grand_summary <- function(loc, data, style) { - data + add_grand_summary_location_row( + loc = loc, + data = data, + text = style, + df_type = "styles_df" + ) } diff --git a/R/utils.R b/R/utils.R index 07c31f5c27..74c7178599 100644 --- a/R/utils.R +++ b/R/utils.R @@ -615,7 +615,15 @@ warn_on_scale_by_input <- function(scale_by) { #' @noRd derive_summary_label <- function(fn) { - if (inherits(fn, "formula")) { + if (is.function(fn)) { + + # Stop the function if any functions provided + # as bare names (e.g., `mean`) don't have + # names provided + stop("All functions provided as bare names in `fns` need a label.", + call. = FALSE) + + } else if (inherits(fn, "formula")) { (fn %>% rlang::f_rhs())[[1]] %>% as.character() @@ -821,6 +829,10 @@ tidy_gsub <- function(x, pattern, replacement, fixed = FALSE) { gsub(pattern, replacement, x, fixed = fixed) } +tidy_sub <- function(x, pattern, replacement, fixed = FALSE) { + + sub(pattern, replacement, x, fixed = fixed) +} #' An options setter for the `opts_df` data frame #' diff --git a/R/utils_formatters.R b/R/utils_formatters.R index 352ec85b03..8c5034e680 100644 --- a/R/utils_formatters.R +++ b/R/utils_formatters.R @@ -262,6 +262,40 @@ to_latex_math_mode <- function(x, } } +#' Obtain the contextually correct minus mark +#' +#' @param context The output context. +#' @noRd +context_missing_text <- function(missing_text, + context) { + + missing_text <- process_text(missing_text, context) + + switch(context, + html = + { + if (missing_text == "---") { + "—" + } else if (missing_text == "--") { + "–" + } else { + missing_text + } + }, + latex = missing_text, + { + if (missing_text == "---") { + "\u2014" + } else if (missing_text == "--") { + "\u2013" + } else { + missing_text + } + }) +} +context_dash_mark <- context_missing_text + + #' Obtain the contextually correct minus mark #' #' @param context The output context. diff --git a/R/utils_render_common.R b/R/utils_render_common.R index 8639041390..69a02f6d40 100644 --- a/R/utils_render_common.R +++ b/R/utils_render_common.R @@ -1,3 +1,6 @@ + +grand_summary_col <- "::GRAND_SUMMARY" + # Utility function to generate column numbers from column names; # used in: `resolve_footnotes_styles()` colname_to_colnum <- function(boxh_df, @@ -241,7 +244,8 @@ perform_col_merge <- function(col_merge, data_df, output_df, boxh_df, - columns_df) { + columns_df, + context) { if (length(col_merge) == 0) { return( @@ -254,7 +258,13 @@ perform_col_merge <- function(col_merge, for (i in seq(col_merge[[1]])) { - pattern <- col_merge[["pattern"]][i] + sep <- col_merge[["sep"]][i] %>% context_dash_mark(context = context) + + pattern <- + col_merge[["pattern"]][i] %>% + tidy_sub("\\{sep\\}", sep) + + value_1_col <- col_merge[["col_1"]][i] %>% unname() value_2_col <- col_merge[["col_1"]][i] %>% names() @@ -315,12 +325,18 @@ perform_col_merge <- function(col_merge, create_summary_dfs <- function(summary_list, data_df, stub_df, - output_df) { + output_df, + context) { + # If the `summary_list` object is an empty list, + # return an empty list as the `list_of_summaries` if (length(summary_list) == 0) { return(list()) } + # Create empty lists that are to contain summary + # data frames for display and for data collection + # purposes summary_df_display_list <- list() summary_df_data_list <- list() @@ -328,94 +344,127 @@ create_summary_dfs <- function(summary_list, summary_attrs <- summary_list[[i]] + groups <- summary_attrs$groups + columns <- summary_attrs$columns + fns <- summary_attrs$fns + missing_text <- summary_attrs$missing_text + formatter <- summary_attrs$formatter + formatter_options <- summary_attrs$formatter_options + labels <- summary_attrs$summary_labels + + if (length(labels) != length(unique(labels))) { + + stop("All summary labels must be unique:\n", + " * Review the names provided in `fns`\n", + " * These labels are in conflict: ", + paste0(labels, collapse = ", "), ".", + call. = FALSE) + } + # Resolve the `missing_text` - if (summary_attrs$missing_text == "---") { - summary_attrs$missing_text <- "\u2014" - } else if (missing_text == "--") { - summary_attrs$missing_text <- "\u2013" + missing_text <- + context_missing_text(missing_text = missing_text, context = context) + + assert_rowgroups <- function() { + + if (all(is.na(stub_df$groupname))) { + stop("There are no row groups in the gt object:\n", + " * Use `groups = NULL` to create a grand summary\n", + " * Define row groups using `gt()` or `tab_row_group()`", + call. = FALSE) + } } - # Resolve the groups to consider - if (isTRUE(summary_attrs$groups)) { + # Resolve the groups to consider; if + # `groups` is TRUE then we are to obtain + # summary row data for all groups + if (isTRUE(groups)) { + + assert_rowgroups() + groups <- unique(stub_df$groupname) - } else { - groups <- summary_attrs$groups - } - # Resolve the columns to exclude - if (isTRUE(summary_attrs$columns)) { - columns <- character(0) - } else { - columns <- base::setdiff(colnames(output_df), summary_attrs$columns) - } + } else if (!is.null(groups) && is.character(groups)) { - # Combine `groupname` with the table body data in order to - # process data by groups - groups_data_df <- - cbind( - stub_df[ - seq(nrow(stub_df)), - c("groupname", "rowname")], - data_df)[, -2] + assert_rowgroups() - # Get the registered function calls - agg_funs <- summary_attrs$fns %>% lapply(rlang::as_function) + # Get the names of row groups available + # in the gt object + groups_available <- unique(stub_df$groupname) + + if (any(!(groups %in% groups_available))) { + + # Stop function if one or more `groups` + # are not present in the gt table + stop("All `groups` should be available in the gt object:\n", + " * The following groups aren't present: ", + paste0( + base::setdiff(groups, groups_available), + collapse = ", " + ), "\n", + call. = FALSE) + } - # Get the names if any were provided - labels <- names(summary_attrs$fns) %>% process_text() + } else if (is.null(groups)) { - # If names weren't provided at all, handle this case by - # creating a vector of NAs that will be replaced later with - # derived names - if (length(labels) < 1) { - labels <- rep(NA_character_, length(summary_attrs$fns)) + # If groups is given as NULL (the default) + # then use a special group (`::GRAND_SUMMARY`) + groups <- grand_summary_col } - # If one or more names not provided then replace the empty - # string with NA - labels[labels == ""] <- NA_character_ + # Resolve the columns to exclude + columns_excl <- base::setdiff(colnames(output_df), columns) - # Get the labels for each of the function calls - derived_labels <- - summary_attrs$fns %>% - lapply(derive_summary_label) %>% - unlist() %>% - unname() %>% - make.names(unique = TRUE) + # Combine `groupname` with the table body data in order to + # process data by groups + if (identical(groups, grand_summary_col)) { - # Replace missing labels with derived labels - labels[is.na(labels)] <- derived_labels[is.na(labels)] + select_data_df <- + cbind( + stub_df[c("groupname", "rowname")], + data_df)[, -2] %>% + dplyr::mutate(groupname = grand_summary_col) %>% + dplyr::select(groupname, columns) - # Initialize an empty tibble to bind to - summary_dfs <- dplyr::tibble() + } else { - for (j in seq(agg_funs)) { + select_data_df <- + cbind( + stub_df[c("groupname", "rowname")], + data_df)[, -2] %>% + dplyr::select(groupname, columns) + } - # Get aggregation rows for each of the `agg_funs` - summary_dfs <- - dplyr::bind_rows( - summary_dfs, - groups_data_df %>% - dplyr::select(c("groupname", colnames(output_df))) %>% + # Get the registered function calls + agg_funs <- fns %>% lapply(rlang::as_closure) + + summary_dfs_data <- + lapply( + seq(agg_funs), function(j) { + select_data_df %>% dplyr::filter(groupname %in% groups) %>% dplyr::group_by(groupname) %>% dplyr::summarize_all(.funs = agg_funs[[j]]) %>% dplyr::ungroup() %>% dplyr::mutate(rowname = labels[j]) %>% - dplyr::select(groupname, rowname, dplyr::everything())) - } + dplyr::select(groupname, rowname, dplyr::everything()) + } + ) %>% + dplyr::bind_rows() + + # Add those columns that were not part of + # the aggregation, filling those with NA values + summary_dfs_data[, columns_excl] <- NA_real_ - # Exclude columns that are not requested by - # filling those with NA values summary_dfs_data <- - summary_dfs %>% - dplyr::mutate_at(.vars = columns, .funs = function(x) {NA_real_}) + summary_dfs_data %>% + dplyr::select(groupname, rowname, colnames(output_df)) # Format the displayed summary lines summary_dfs_display <- - summary_dfs %>% + summary_dfs_data %>% dplyr::mutate_at( - .vars = summary_attrs$columns, + .vars = columns, .funs = function(x) { format_data <- @@ -427,16 +476,12 @@ create_summary_dfs <- function(summary_list, summary_attrs$formatter_options)) formatter <- attr(format_data, "formats")[[1]]$func - - if ("html" %in% names(formatter)) { - formatter$html(x) - } else { - formatter$default(x) - } + fmt <- formatter[[context]] %||% formatter$default + fmt(x) } ) %>% dplyr::mutate_at( - .vars = columns, + .vars = columns_excl, .funs = function(x) {NA_character_}) for (group in groups) { @@ -462,14 +507,14 @@ create_summary_dfs <- function(summary_list, } } - # Condense data in summary_df_display_list in a + # Condense data in `summary_df_display_list` in a # groupwise manner summary_df_display_list <- tapply( summary_df_display_list, names(summary_df_display_list), - dplyr::bind_rows) - + dplyr::bind_rows + ) for (i in seq(summary_df_display_list)) { @@ -487,12 +532,16 @@ create_summary_dfs <- function(summary_list, summary_df_display_list[[i]] <- summary_df_display_list[[i]][ match(arrangement, summary_df_display_list[[i]]$rowname), ] %>% - replace(is.na(.), summary_attrs$missing_text) + replace(is.na(.), missing_text) } + # Return a list of lists, each of which have + # summary data frames for display and for data + # collection purposes list( summary_df_data_list = summary_df_data_list, - summary_df_display_list = summary_df_display_list) + summary_df_display_list = summary_df_display_list + ) } migrate_labels <- function(row_val) { @@ -730,11 +779,6 @@ create_summary_rows <- function(n_rows, body_content_summary <- as.vector(t(summary_df)) - if (context == "latex") { - body_content_summary <- body_content_summary %>% - tidy_gsub("\u2014", "---") - } - row_splits_summary <- split_body_content( body_content = body_content_summary, diff --git a/R/utils_render_footnotes.R b/R/utils_render_footnotes.R index a5b0cab978..210caae634 100644 --- a/R/utils_render_footnotes.R +++ b/R/utils_render_footnotes.R @@ -170,6 +170,31 @@ resolve_footnotes_styles <- function(output_df, ) } + # For the grand summary cells, insert a `colnum` based + # on `groups_rows_df` + if (6 %in% tbl[["locnum"]]) { + + tbl_not_g_summary_cells <- + tbl %>% + dplyr::filter(locnum != 6) + + tbl_g_summary_cells <- + tbl %>% + dplyr::filter(locnum == 6) %>% + dplyr::mutate( + colnum = colname_to_colnum( + boxh_df = boxh_df, colname = colname + ) + ) + + # Re-combine `tbl_not_g_summary_cells` + # with `tbl_g_summary_cells` + tbl <- + dplyr::bind_rows( + tbl_not_g_summary_cells, tbl_g_summary_cells + ) + } + # For the column label cells, insert a `colnum` # based on `boxh_df` if ("columns_columns" %in% tbl[["locname"]]) { @@ -536,37 +561,72 @@ apply_footnotes_to_summary <- function(list_of_summaries, summary_df_list <- list_of_summaries$summary_df_display_list - if (!("summary_cells" %in% footnotes_resolved$locname)) { + if (!("summary_cells" %in% footnotes_resolved$locname | + "grand_summary_cells" %in% footnotes_resolved$locname)) { return(list_of_summaries) } - footnotes_tbl_data <- - footnotes_resolved %>% - dplyr::filter(locname == "summary_cells") + if ("summary_cells" %in% footnotes_resolved$locname) { + + footnotes_tbl_data <- + footnotes_resolved %>% + dplyr::filter(locname == "summary_cells") - footnotes_data_glpyhs <- - footnotes_tbl_data %>% - dplyr::mutate(row = as.integer(round((rownum - floor(rownum)) * 100, 0))) %>% - dplyr::group_by(grpname, row, colnum) %>% - dplyr::mutate(fs_id_coalesced = paste(fs_id, collapse = ",")) %>% - dplyr::ungroup() %>% - dplyr::select(grpname, colname, row, fs_id_coalesced) %>% - dplyr::distinct() + footnotes_data_glpyhs <- + footnotes_tbl_data %>% + dplyr::mutate(row = as.integer(round((rownum - floor(rownum)) * 100, 0))) %>% + dplyr::group_by(grpname, row, colnum) %>% + dplyr::mutate(fs_id_coalesced = paste(fs_id, collapse = ",")) %>% + dplyr::ungroup() %>% + dplyr::select(grpname, colname, row, fs_id_coalesced) %>% + dplyr::distinct() - for (i in seq(nrow(footnotes_data_glpyhs))) { + for (i in seq(nrow(footnotes_data_glpyhs))) { - text <- - summary_df_list[[footnotes_data_glpyhs[i, ][["grpname"]]]][[ - footnotes_data_glpyhs$row[i], footnotes_data_glpyhs$colname[i]]] + text <- + summary_df_list[[footnotes_data_glpyhs[i, ][["grpname"]]]][[ + footnotes_data_glpyhs$row[i], footnotes_data_glpyhs$colname[i]]] + + text <- + paste0(text, footnote_glyph_to_html(footnotes_data_glpyhs$fs_id_coalesced[i])) - text <- - paste0(text, footnote_glyph_to_html(footnotes_data_glpyhs$fs_id_coalesced[i])) + summary_df_list[[footnotes_data_glpyhs[i, ][["grpname"]]]][[ + footnotes_data_glpyhs$row[i], footnotes_data_glpyhs$colname[i]]] <- text + } - summary_df_list[[footnotes_data_glpyhs[i, ][["grpname"]]]][[ - footnotes_data_glpyhs$row[i], footnotes_data_glpyhs$colname[i]]] <- text + list_of_summaries$summary_df_display_list <- summary_df_list } - list_of_summaries$summary_df_display_list <- summary_df_list + if ("grand_summary_cells" %in% footnotes_resolved$locname) { + + footnotes_tbl_data <- + footnotes_resolved %>% + dplyr::filter(locname == "grand_summary_cells") + + footnotes_data_glpyhs <- + footnotes_tbl_data %>% + dplyr::group_by(rownum, colnum) %>% + dplyr::mutate(fs_id_coalesced = paste(fs_id, collapse = ",")) %>% + dplyr::ungroup() %>% + dplyr::select(colname, rownum, fs_id_coalesced) %>% + dplyr::distinct() + + for (i in seq(nrow(footnotes_data_glpyhs))) { + + text <- + summary_df_list[[grand_summary_col]][[ + footnotes_data_glpyhs$rownum[i], footnotes_data_glpyhs$colname[i]]] + + text <- + paste0(text, footnote_glyph_to_html(footnotes_data_glpyhs$fs_id_coalesced[i])) + + summary_df_list[[grand_summary_col]][[ + footnotes_data_glpyhs$rownum[i], footnotes_data_glpyhs$colname[i]]] <- text + } + + list_of_summaries$summary_df_display_list[[grand_summary_col]] <- + summary_df_list[[grand_summary_col]] + } list_of_summaries } diff --git a/R/utils_render_html.R b/R/utils_render_html.R index da9453fa07..f3314ce51a 100644 --- a/R/utils_render_html.R +++ b/R/utils_render_html.R @@ -139,10 +139,9 @@ apply_styles_to_summary_output <- function(summary_df, styles_summary_df <- summary_df styles_summary_df[] <- NA_character_ - styles_tbl_summary <- styles_resolved %>% - dplyr::filter(locname == "summary_cells") %>% + dplyr::filter(locname %in% "summary_cells") %>% dplyr::filter(grpname == group) if (nrow(styles_tbl_summary) > 0) { @@ -173,10 +172,49 @@ apply_styles_to_summary_output <- function(summary_df, split_body_content(body_content = summary_styles, n_cols) } -#' Create the opening HTML element of a table -#' +#' Apply styles to summary rows +#' @importFrom dplyr filter group_by mutate ungroup select distinct #' @noRd -create_table_start_h <- function() { +apply_styles_to_grand_summary_output <- function(summary_df, + styles_resolved, + n_cols) { + + styles_summary_df <- summary_df + styles_summary_df[] <- NA_character_ + + styles_tbl_summary <- + styles_resolved %>% + dplyr::filter(locname %in% "grand_summary_cells") + + if (nrow(styles_tbl_summary) > 0) { + + styles_summary <- + styles_tbl_summary %>% + dplyr::group_by(colname, rownum) %>% + dplyr::mutate(styles_appended = paste(text, collapse = "")) %>% + dplyr::ungroup() %>% + dplyr::select(colname, rownum, styles_appended) %>% + dplyr::distinct() + + + for (i in seq(nrow(styles_summary))) { + + styles_summary_df[ + styles_summary$rownum[i], styles_summary$colname[i]] <- + styles_summary$styles_appended[i] + } + } + + # Extract `summary_styles` as a vector + summary_styles <- as.vector(t(styles_summary_df)) + + # Split `summary_styles` by slices of rows + split_body_content(body_content = summary_styles, n_cols) +} + +# Create the opening HTML element of a table +create_table_start_h <- function(groups_rows_df) { + "\n\n" } @@ -782,6 +820,62 @@ create_body_component_h <- function(row_splits_body, } } + # If there is a grand summary, include that at the end + if (summaries_present && + grand_summary_col %in% names(list_of_summaries$summary_df_display_list)) { + + grand_summary_df <- + list_of_summaries$summary_df_display_list[[grand_summary_col]] %>% + as.data.frame(stringsAsFactors = FALSE) + + row_splits_summary_styles <- + apply_styles_to_grand_summary_output( + summary_df = grand_summary_df, + styles_resolved = styles_resolved, + n_cols = n_cols + ) + + grand_summary <- as.vector(t(grand_summary_df)) + + row_splits_grand_summary <- + split_body_content( + body_content = grand_summary, + n_cols = n_cols) + + # Provide CSS classes for leading and + # non-leading grand summary rows + gs_row_classes_first <- "gt_grand_summary_row gt_first_grand_summary_row " + gs_row_classes <- "gt_grand_summary_row " + + grand_summary_row_lines <- c() + + for (j in seq(length(row_splits_grand_summary))) { + + grand_summary_row_lines <- + c(grand_summary_row_lines, + paste0( + "\n", + paste0( + ""), "\n", + paste0( + "", collapse = "\n"), + "\n\n") + ) + } + + body_rows <- c(body_rows, grand_summary_row_lines) + } + # Create a single-length vector by collapsing all vector components body_rows <- body_rows %>% paste(collapse = "") diff --git a/inst/css/gt_colors.scss b/inst/css/gt_colors.scss index ed930fb169..e6277ebdf2 100644 --- a/inst/css/gt_colors.scss +++ b/inst/css/gt_colors.scss @@ -3,6 +3,7 @@ $heading_background_color: $table_background_color !default; $column_labels_background_color: $table_background_color !default; $row_group_background_color: $table_background_color !default; $summary_row_background_color: $table_background_color !default; +$grand_summary_row_background_color: $table_background_color !default; @function font-color($color) { @return if( diff --git a/inst/css/gt_styles_default.scss b/inst/css/gt_styles_default.scss index be32ebfb01..5da6844ce8 100644 --- a/inst/css/gt_styles_default.scss +++ b/inst/css/gt_styles_default.scss @@ -118,23 +118,34 @@ border-right-width: 2px; border-right-color: #A8A8A8; padding-left: 12px; - &.gt_row { - background-color: $table_background_color; - } } .gt_summary_row { + color: font-color($summary_row_background_color); background-color: $summary_row_background_color; /* summary_row.background.color */ padding: $summary_row_padding; /* summary_row.padding */ text-transform: $summary_row_text_transform; /* summary_row.text_transform */ } + .gt_grand_summary_row { + color: font-color($grand_summary_row_background_color); + background-color: $grand_summary_row_background_color; /* grand_summary_row.background.color */ + padding: $grand_summary_row_padding; /* grand_summary_row.padding */ + text-transform: $grand_summary_row_text_transform; /* grand_summary_row.text_transform */ + } + .gt_first_summary_row { border-top-style: solid; border-top-width: 2px; border-top-color: #A8A8A8; } + .gt_first_grand_summary_row { + border-top-style: double; + border-top-width: 6px; + border-top-color: #A8A8A8; + } + .gt_table_body { border-top-style: $table_body_border_top_style; /* table_body.border.top.style */ border-top-width: $table_body_border_top_width; /* table_body.border.top.width */ diff --git a/man/location_cells.Rd b/man/location_cells.Rd index bb6d154a7e..fec9154807 100644 --- a/man/location_cells.Rd +++ b/man/location_cells.Rd @@ -8,6 +8,7 @@ \alias{cells_stub} \alias{cells_data} \alias{cells_summary} +\alias{cells_grand_summary} \title{Helpers for targeting multiple cells in different locations} \usage{ cells_title(groups = c("title", "subtitle")) @@ -21,6 +22,8 @@ cells_stub(rows = NULL) cells_data(columns = NULL, rows = NULL) cells_summary(groups = NULL, columns = NULL, rows = NULL) + +cells_grand_summary(columns = NULL, rows = NULL) } \arguments{ \item{columns, rows, groups}{either a vector of names, a vector of diff --git a/man/summary_rows.Rd b/man/summary_rows.Rd index 61d9b09870..2ce924814c 100644 --- a/man/summary_rows.Rd +++ b/man/summary_rows.Rd @@ -4,54 +4,64 @@ \alias{summary_rows} \title{Add summary rows using aggregation functions} \usage{ -summary_rows(data, groups = NULL, columns = NULL, fns, +summary_rows(data, groups = NULL, columns = TRUE, fns, missing_text = "---", formatter = fmt_number, ...) } \arguments{ -\item{data}{a table object that is created using the \code{gt()} function.} +\item{data}{A table object that is created using the \code{gt()} function.} -\item{groups}{the row groups labels that identify which summary rows will be -added.} +\item{groups}{The groups to consider for generation of groupwise summary +rows. By default this is set to \code{NULL}, which results in the formation +of grand summary rows (a grand summary operates on all table data). +Providing the names of row groups in \code{c()} will create a groupwise +summary and generate summary rows for the specified groups. Setting this to +\code{TRUE} indicates that all available groups will receive groupwise +summary rows.} -\item{columns}{the columns for which the summaries should be calculated. If -nothing is provided, then the supplied aggregation functions will be -applied to all columns.} +\item{columns}{The columns for which the summaries should be calculated.} -\item{fns}{functions used for aggregations. This can include base functions +\item{fns}{Functions used for aggregations. This can include base functions like \code{mean}, \code{min}, \code{max}, \code{median}, \code{sd}, or \code{sum} or any other user-defined aggregation function. The function(s) should be supplied within a \code{list()}. Within that list, we can specify -the functions by use of function names (e.g., \code{"sum"}), the functions -themselves (e.g., \code{sum}), or one-sided R formulas by prefacing with a -\code{~} where \code{.} serves as the data to be summarized (e.g., -\code{sum(., na.rm = TRUE)}). By using named arguments, the names will -serve as row labels for the corresponding summary rows (otherwise the -labels will be derived from the function names).} +the functions by use of function names in quotes (e.g., \code{"sum"}), as +bare functions (e.g., \code{sum}), or as one-sided R formulas using a +leading \code{~}. In the formula representation, a \code{.} serves as the +data to be summarized (e.g., \code{sum(., na.rm = TRUE)}). The use of named +arguments is recommended as the names will serve as summary row labels for +the corresponding summary rows data (the labels can derived from the +function names but only when not providing bare function names).} -\item{missing_text}{the text to be used in place of \code{NA} values in +\item{missing_text}{The text to be used in place of \code{NA} values in summary cells with no data outputs.} -\item{formatter}{a formatter function name. These can be any of the +\item{formatter}{A formatter function name. These can be any of the \code{fmt_*()}functions available in the package (e.g., \code{\link{fmt_number}()}, \code{link{fmt_percent}()}, etc.), or a custom function using \code{\link{fmt}()}. The default function is \code{\link{fmt_number}()} and its options can be accessed through \code{...}.} -\item{...}{values passed to the \code{formatter} function, where the provided +\item{...}{Values passed to the \code{formatter} function, where the provided values are to be in the form of named vectors. For example, when using the default \code{formatter} function, \code{\link{fmt_number}()}, options such as \code{decimals}, \code{use_seps}, and \code{locale} can be used.} } \value{ -an object of class \code{gt_tbl}. +An object of class \code{gt_tbl}. } \description{ -Add summary rows to one or more row groups by using the input data already -provided in the \code{\link{gt}()} function alongside any suitable -aggregation functions. Should we need to obtain the summary data for external -purposes, the \code{\link{extract_summary}()} can be used with a -\code{gt_tbl} object where summary rows were added via \code{summary_rows()}. +Add groupwise summary rows to one or more row groups by using the input data +already provided in the \code{\link{gt}()} function alongside any suitable +aggregation functions. Or, add a grand summary that incorporates all +available data, regardless of grouping. You choose how to format the values +in the resulting summary cells by use of a \code{formatter} function (e.g, +\code{\link{fmt_number}()}) and any relevant options. +} +\details{ +Should we need to obtain the summary data for external purposes, the +\code{\link{extract_summary}()} function can be used with a \code{gt_tbl} +object where summary rows were added via \code{summary_rows()}. } \section{Figures}{ diff --git a/man/tab_options.Rd b/man/tab_options.Rd index d7e22f59ec..e0ea146906 100644 --- a/man/tab_options.Rd +++ b/man/tab_options.Rd @@ -28,7 +28,10 @@ tab_options(data, table.width = NULL, table.font.size = NULL, table_body.border.bottom.width = NULL, table_body.border.bottom.color = NULL, row.padding = NULL, summary_row.background.color = NULL, summary_row.padding = NULL, - summary_row.text_transform = NULL, footnote.sep = NULL, + summary_row.text_transform = NULL, + grand_summary_row.background.color = NULL, + grand_summary_row.padding = NULL, + grand_summary_row.text_transform = NULL, footnote.sep = NULL, footnote.glyph = NULL, footnote.font.size = NULL, footnote.padding = NULL, sourcenote.font.size = NULL, sourcenote.padding = NULL, row.striping.include_stub = NULL, @@ -55,7 +58,7 @@ units of pixels. The \code{\link{px}()} and \code{\link{pct}()} helper functions can also be used to pass in numeric values and obtain values as pixel or percent units.} -\item{table.background.color, heading.background.color, column_labels.background.color, row_group.background.color, summary_row.background.color}{background colors for the parent element \code{table} and the following +\item{table.background.color, heading.background.color, column_labels.background.color, row_group.background.color, summary_row.background.color, grand_summary_row.background.color}{background colors for the parent element \code{table} and the following child elements: \code{heading}, \code{columns}, \code{row_group}, \code{summary_row}, and \code{table_body}. A color name or a hexadecimal color code should be provided.} @@ -64,8 +67,8 @@ color code should be provided.} \item{heading.border.bottom.style, heading.border.bottom.width, heading.border.bottom.color}{the style, width, and color of the heading's bottom border.} -\item{column_labels.font.weight, row_group.font.weight}{the font weight of the -\code{columns} and \code{row_group} text element.} +\item{column_labels.font.weight, row_group.font.weight}{the font weight of +the \code{columns} and \code{row_group} text element.} \item{column_labels.hidden}{an option to hide the column labels.} @@ -77,11 +80,11 @@ color code should be provided.} \item{table_body.border.bottom.style, table_body.border.bottom.width, table_body.border.bottom.color}{the style, width, and color of the table body's bottom border.} -\item{row.padding, summary_row.padding}{the amount of padding in each row and -in each summary row.} +\item{row.padding, summary_row.padding, grand_summary_row.padding}{the amount +of padding in each row and in each type of summary row.} -\item{summary_row.text_transform}{an option to apply text transformations to -the label text in each summary row.} +\item{summary_row.text_transform, grand_summary_row.text_transform}{an option +to apply text transformations to the label text in each summary row.} \item{footnote.sep}{the separating characters between adjacent footnotes in the footnotes section. The default value produces a linebreak.} diff --git a/tests/testthat/test-cols_merge.R b/tests/testthat/test-cols_merge.R index ac1c724510..96a928e71a 100644 --- a/tests/testthat/test-cols_merge.R +++ b/tests/testthat/test-cols_merge.R @@ -203,7 +203,7 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect that merging statements are stored in `col_merge` attr(tbl_html, "col_merge", exact = TRUE)$pattern %>% - expect_equal("{1} — {2}") + expect_equal("{1} {sep} {2}") attr(tbl_html, "col_merge", exact = TRUE)$col_1 %>% names() %>% @@ -223,7 +223,7 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect that merging statements are stored in `col_merge` attr(tbl_html, "col_merge", exact = TRUE)$pattern %>% - expect_equal("{1} — {2}") + expect_equal("{1} {sep} {2}") attr(tbl_html, "col_merge", exact = TRUE)$col_1 %>% names() %>% @@ -246,7 +246,10 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect that merging statements are stored in `col_merge` attr(tbl_html, "col_merge", exact = TRUE)$pattern[[1]] %>% - expect_equal("{1} — {2}") + expect_equal("{1} {sep} {2}") + + attr(tbl_html, "col_merge", exact = TRUE)$sep[[1]] %>% + expect_equal("---") attr(tbl_html, "col_merge", exact = TRUE)$col_1[1] %>% names() %>% @@ -257,7 +260,10 @@ test_that("the `cols_merge_range()` function works correctly", { expect_equal("col_1") attr(tbl_html, "col_merge", exact = TRUE)$pattern[[2]] %>% - expect_equal("{1} — {2}") + expect_equal("{1} {sep} {2}") + + attr(tbl_html, "col_merge", exact = TRUE)$sep[[2]] %>% + expect_equal("---") attr(tbl_html, "col_merge", exact = TRUE)$col_1[2] %>% names() %>% diff --git a/tests/testthat/test-conditional_fmt.R b/tests/testthat/test-conditional_fmt.R index fb865d5731..89b95c4e31 100644 --- a/tests/testthat/test-conditional_fmt.R +++ b/tests/testthat/test-conditional_fmt.R @@ -234,7 +234,7 @@ test_that("the `fmt_missing()` function works with conditional `rows`", { columns = vars(num_2), rows = num_1 <= 0) %>% render_formats_test(context = "html"))[["num_2"]], - c("34", "74", "23", "NA", "35", rep("—", 2)) + c("34", "74", "23", "NA", "35", rep("—", 2)) ) }) diff --git a/tests/testthat/test-fmt_missing.R b/tests/testthat/test-fmt_missing.R index 004665734d..9108374732 100644 --- a/tests/testthat/test-fmt_missing.R +++ b/tests/testthat/test-fmt_missing.R @@ -54,13 +54,13 @@ test_that("the `fmt_missing()` function works correctly", { (tab %>% fmt_missing(columns = "num_1") %>% render_formats_test(context = "html"))[["num_1"]], - c("—", "74", "—", "93", "—", "76", "—")) + c("—", "74", "—", "93", "—", "76", "—")) expect_equal( (tab %>% fmt_missing(columns = "num_1", missing_text = "--") %>% render_formats_test(context = "html"))[["num_1"]], - c("–", "74", "–", "93", "–", "76", "–")) + c("–", "74", "–", "93", "–", "76", "–")) expect_equal( (tab %>% @@ -84,7 +84,7 @@ test_that("the `fmt_missing()` function works correctly", { (tab %>% fmt_missing(columns = "num_1", rows = num_2 < 50) %>% render_formats_test(context = "html"))[["num_1"]], - c("—", "74", "—", "93", "—", "76", "NA")) + c("—", "74", "—", "93", "—", "76", "NA")) # Format columns with `fmt_number()` then use # `fmt_missing()` on all columns (the two functions @@ -97,7 +97,7 @@ test_that("the `fmt_missing()` function works correctly", { ) %>% fmt_missing(columns = TRUE) %>% render_formats_test(context = "html"))[["num_1"]], - c("—", "74.000", "—", "93.000", "—", "76.000", "—")) + c("—", "74.000", "—", "93.000", "—", "76.000", "—")) # Reverse the ordering: use `fmt_missing()` first # then `fmt_number()`; expect the same output as before @@ -109,5 +109,5 @@ test_that("the `fmt_missing()` function works correctly", { decimals = 3 ) %>% render_formats_test(context = "html"))[["num_1"]], - c("—", "74.000", "—", "93.000", "—", "76.000", "—")) + c("—", "74.000", "—", "93.000", "—", "76.000", "—")) }) diff --git a/tests/testthat/test-l_cols_merge.R b/tests/testthat/test-l_cols_merge.R index 6ad98ee5d6..3ba26209b6 100644 --- a/tests/testthat/test-l_cols_merge.R +++ b/tests/testthat/test-l_cols_merge.R @@ -182,16 +182,16 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect a characteristic pattern grepl( paste0( - ".*767.6 — 928.1 & 382.0 & 674.5", - ".*403.3 — 461.5 & 15.1 & 242.8", - ".*686.4 — 54.1 & 282.7 & 56.3", - ".*662.6 — 148.8 & 984.6 & 928.1", - ".*198.5 — 65.1 & 127.4 & 219.3", - ".*132.1 — 118.1 & 91.2 & 874.3", - ".*349.7 — 307.1 & 566.7 & 542.9", - ".*63.7 — 504.3 & 152.0 & 724.5", - ".*105.4 — 729.8 & 962.4 & 336.4", - ".*924.2 — 424.6 & 740.8 & 104.2.*"), + ".*767.6 --- 928.1 & 382.0 & 674.5", + ".*403.3 --- 461.5 & 15.1 & 242.8", + ".*686.4 --- 54.1 & 282.7 & 56.3", + ".*662.6 --- 148.8 & 984.6 & 928.1", + ".*198.5 --- 65.1 & 127.4 & 219.3", + ".*132.1 --- 118.1 & 91.2 & 874.3", + ".*349.7 --- 307.1 & 566.7 & 542.9", + ".*63.7 --- 504.3 & 152.0 & 724.5", + ".*105.4 --- 729.8 & 962.4 & 336.4", + ".*924.2 --- 424.6 & 740.8 & 104.2.*"), tbl_latex %>% as_latex() %>% as.character()) %>% expect_true() @@ -207,16 +207,16 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect a characteristic pattern grepl( paste0( - ".*767.6 — 928.1 & 382.0 & 674.5", - ".*403.3 — 461.5 & 15.1 & 242.8", - ".*686.4 — 54.1 & 282.7 & 56.3", - ".*662.6 — 148.8 & 984.6 & 928.1", - ".*198.5 — 65.1 & 127.4 & 219.3", - ".*132.1 — 118.1 & 91.2 & 874.3", - ".*349.7 — 307.1 & 566.7 & 542.9", - ".*63.7 — 504.3 & 152.0 & 724.5", - ".*105.4 — 729.8 & 962.4 & 336.4", - ".*924.2 — 424.6 & 740.8 & 104.2.*"), + ".*767.6 --- 928.1 & 382.0 & 674.5", + ".*403.3 --- 461.5 & 15.1 & 242.8", + ".*686.4 --- 54.1 & 282.7 & 56.3", + ".*662.6 --- 148.8 & 984.6 & 928.1", + ".*198.5 --- 65.1 & 127.4 & 219.3", + ".*132.1 --- 118.1 & 91.2 & 874.3", + ".*349.7 --- 307.1 & 566.7 & 542.9", + ".*63.7 --- 504.3 & 152.0 & 724.5", + ".*105.4 --- 729.8 & 962.4 & 336.4", + ".*924.2 --- 424.6 & 740.8 & 104.2.*"), tbl_latex %>% as_latex() %>% as.character()) %>% expect_true() @@ -235,16 +235,16 @@ test_that("the `cols_merge_range()` function works correctly", { # Expect a characteristic pattern grepl( paste0( - ".*767.6 — 928.1 & 382.0 — 674.5", - ".*403.3 — 461.5 & 15.1 — 242.8", - ".*686.4 — 54.1 & 282.7 — 56.3", - ".*662.6 — 148.8 & 984.6 — 928.1", - ".*198.5 — 65.1 & 127.4 — 219.3", - ".*132.1 — 118.1 & 91.2 — 874.3", - ".*349.7 — 307.1 & 566.7 — 542.9", - ".*63.7 — 504.3 & 152.0 — 724.5", - ".*105.4 — 729.8 & 962.4 — 336.4", - ".*924.2 — 424.6 & 740.8 — 104.2.*"), + ".*767.6 --- 928.1 & 382.0 --- 674.5", + ".*403.3 --- 461.5 & 15.1 --- 242.8", + ".*686.4 --- 54.1 & 282.7 --- 56.3", + ".*662.6 --- 148.8 & 984.6 --- 928.1", + ".*198.5 --- 65.1 & 127.4 --- 219.3", + ".*132.1 --- 118.1 & 91.2 --- 874.3", + ".*349.7 --- 307.1 & 566.7 --- 542.9", + ".*63.7 --- 504.3 & 152.0 --- 724.5", + ".*105.4 --- 729.8 & 962.4 --- 336.4", + ".*924.2 --- 424.6 & 740.8 --- 104.2.*"), tbl_latex %>% as_latex() %>% as.character()) %>% expect_true() diff --git a/tests/testthat/test-location_cells.R b/tests/testthat/test-location_cells.R index d8aedc2717..1643a359f6 100644 --- a/tests/testthat/test-location_cells.R +++ b/tests/testthat/test-location_cells.R @@ -238,7 +238,8 @@ test_that("the `cells_summary()` function works correctly", { helper_cells_summary <- cells_summary( groups = "group_a", - columns = c("col_1", "col_2")) + columns = c("col_1", "col_2") + ) # Expect this has the `cells_summary` and `location_cells` classes helper_cells_summary %>% @@ -274,7 +275,67 @@ test_that("the `cells_summary()` function works correctly", { helper_cells_summary[[2]][2] %>% as.character() %>% expect_equal("c(\"col_1\", \"col_2\")") + + # Create a `cells_summary` object with + # columns in `vars()` provided to `columns` + helper_cells_summary <- + cells_summary( + groups = "group_a", + columns = vars(col_1, col_2) + ) + + # Expect the RHS of the second component formula to contain + # the vector provided + helper_cells_summary[[2]][2] %>% + as.character() %>% + expect_equal("vars(col_1, col_2)") }) +test_that("the `cells_grand_summary()` function works correctly", { + + # Create a `cells_grand_summary` object with names provided to `columns` + helper_cells_grand_summary <- + cells_grand_summary( + columns = c("col_1", "col_2") + ) + + # Expect this has the `cells_summary` and `location_cells` classes + helper_cells_grand_summary %>% + expect_is(c("cells_grand_summary", "location_cells")) + + # Expect the length of the object to be `2` + helper_cells_grand_summary %>% + length() %>% + expect_equal(2) + + # Expect that the object has the names `columns` and `rows` + helper_cells_grand_summary %>% + names() %>% + expect_equal(c("columns", "rows")) + + # Expect the first list component to have the `quosure` and `formula` classes + helper_cells_grand_summary[[1]] %>% expect_is(c("quosure", "formula")) + # Expect the second list component to have the `quosure` and `formula` classes + helper_cells_grand_summary[[2]] %>% expect_is(c("quosure", "formula")) + + # Expect the RHS of the first component formula to contain + # the vector provided + helper_cells_grand_summary[[1]][2] %>% + as.character() %>% + expect_equal("c(\"col_1\", \"col_2\")") + + # Create a `cells_grand_summary` object with + # columns in `vars()` provided to `columns` + helper_cells_grand_summary <- + cells_grand_summary( + columns = vars(col_1, col_2) + ) + + # Expect the RHS of the first component formula to contain + # the vector provided + helper_cells_grand_summary[[1]][2] %>% + as.character() %>% + expect_equal("vars(col_1, col_2)") +}) diff --git a/tests/testthat/test-summary_rows.R b/tests/testthat/test-summary_rows.R index 4a6a66d2f9..453c8d19cd 100644 --- a/tests/testthat/test-summary_rows.R +++ b/tests/testthat/test-summary_rows.R @@ -1,30 +1,37 @@ context("Ensuring that the `summary_rows()` function works as expected") -# Create a table with group names, rownames, and two columns of values +# Create a table based on `sp500`, with +# group names, rownames, and four +# columns of values tbl <- - dplyr::tribble( - ~groupname, ~rowname, ~value_1, ~value_2, - "A", "1", NA, 260.1, - "A", "2", 184.3, 84.4, - "A", "3", 342.3, 126.3, - "A", "4", 234.9, NA, - "B", "1", 190.9, 832.5, - "B", "2", 743.3, 281.2, - "B", "3", 252.3, 732.5, - "B", "4", 344.7, NA, - "C", "1", 197.2, 818.0, - "C", "2", 284.3, 394.4) - -test_that("the `summary_rows()` function works correctly", { - - # Create a table with summary rows for the `A` and `C` groups; - # the 3 summary rows for these groups represent the mean, sum, - # and standard deviation of `value` + sp500 %>% + dplyr::filter( + date >= "2015-01-05" & + date <="2015-01-16" + ) %>% + dplyr::arrange(date) %>% + dplyr::mutate( + week = paste0( + "W", strftime(date, format = "%V")) + ) %>% + dplyr::select(-adj_close, -volume) %>% + gt( + rowname_col = "date", + groupname_col = "week" + ) + +test_that("the `summary_rows()` can make groupwise summaries", { + + # Create a table with summary rows for + # the `W02` group; the 3 summary rows for + # this group represent the mean, sum, + # and standard deviation of all numeric + # columns gt_tbl <- - gt(tbl) %>% + tbl %>% summary_rows( - groups = c("A", "C"), - columns = vars(value_1), + groups = "W02", + columns = vars(open, high, low, close), fns = list( average = ~mean(., na.rm = TRUE), total = ~sum(., na.rm = TRUE), @@ -33,8 +40,9 @@ test_that("the `summary_rows()` function works correctly", { # Extract the internal `summary` object summary <- attr(gt_tbl, "summary", exact = TRUE) - # Expect that the internal `summary` list object has - # a length of `1` since there was only one call of `summary_rows()` + # Expect that the internal `summary` list + # object has a length of `1` since there was + # only one call of `summary_rows()` length(summary) %>% expect_equal(1) @@ -43,22 +51,23 @@ test_that("the `summary_rows()` function works correctly", { summary[[1]] %>% names() %>% expect_equal( - c("groups", "columns", "fns", "missing_text", - "formatter", "formatter_options")) + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) # Expect the `groups` provided in `summary[[1]]$groups` summary[[1]]$groups %>% - expect_equal(c("A", "C")) + expect_equal("W02") # Expect the `columns` provided in `summary[[1]]$columns` summary[[1]]$columns %>% - expect_equal("value_1") + expect_equal(c("open", "high", "low", "close")) - # Expect that `summary[[1]]$fns` is a `fun_list` object + # Expect that `summary[[1]]$fns` is a `list` object summary[[1]]$fns %>% expect_is("list") - # Expect that the components of `summary[[1]]$fns` are quosures + # Expect that the components of `summary[[1]]$fns` are formulas summary[[1]]$fns$average %>% expect_is("formula") summary[[1]]$fns$total %>% expect_is("formula") summary[[1]]$fns$`std dev` %>% expect_is("formula") @@ -75,13 +84,22 @@ test_that("the `summary_rows()` function works correctly", { summary[[1]]$formatter_options %>% expect_is("list") - # Create a table with summary rows for all groups and for `value_1`; - # the 3 summary rows for these groups represent the mean, sum, - # and the standard deviation + # Expect that `summary[[1]]$formatter_options` is + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + # Create a table with summary rows for + # the `W02` group; the 3 summary rows for + # this group represent the mean, sum, + # and standard deviation of only the + # `open` column gt_tbl <- - gt(tbl) %>% + tbl %>% summary_rows( - columns = vars(value_1), + groups = "W02", + columns = vars(open), fns = list( average = ~mean(., na.rm = TRUE), total = ~sum(., na.rm = TRUE), @@ -90,32 +108,127 @@ test_that("the `summary_rows()` function works correctly", { # Extract the internal `summary` object summary <- attr(gt_tbl, "summary", exact = TRUE) - # Expect that the internal `summary` list object has - # a length of `1` since there was only one call of `summary_rows()` - length(summary) %>% - expect_equal(1) + # Expect the `groups` provided in `summary[[1]]$groups` + summary[[1]]$groups %>% + expect_equal("W02") - # For the single list component in `summary`, expect specific - # names within it - summary[[1]] %>% - names() %>% - expect_equal( - c("groups", "columns", "fns", "missing_text", - "formatter", "formatter_options")) + # Expect the `columns` provided in `summary[[1]]$columns` + summary[[1]]$columns %>% + expect_equal("open") - # Expect that `summary[[1]]$groups` is TRUE + # Expect that `summary[[1]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") + + # Expect that the components of `summary[[1]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + # Expect that `summary[[1]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + # Expect that `summary[[1]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + # Expect that `summary[[1]]$formatter_options` is + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + # Create a table with summary rows for + # the `W02` and `W03` groups; the 3 summary + # rows for these groups represent the mean, + # sum, and standard deviation of only the + # `open` column + gt_tbl <- + tbl %>% + summary_rows( + groups = c("W02", "W03"), + columns = vars(open), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE))) + + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect the `groups` provided in `summary[[1]]$groups` + summary[[1]]$groups %>% + expect_equal(c("W02", "W03")) + + # Expect the `columns` provided in `summary[[1]]$columns` + summary[[1]]$columns %>% + expect_equal("open") + + # Expect that `summary[[1]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") + + # Expect that the components of `summary[[1]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + # Expect that `summary[[1]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + # Expect that `summary[[1]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + # Expect that `summary[[1]]$formatter_options` is + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + # Create a table with summary rows for + # the `W02` and `W03` groups (using + # `groups = TRUE`); the 3 summary rows for + # these groups represent the mean, + # sum, and standard deviation of only the + # `open` column + gt_tbl <- + tbl %>% + summary_rows( + groups = TRUE, + columns = vars(open), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE))) + + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect the `groups` provided in `summary[[1]]$groups` + # to be `TRUE` summary[[1]]$groups %>% expect_true() - # Expect that `summary[[1]]$columns` is `value_1` + # Expect the `columns` provided in `summary[[1]]$columns` summary[[1]]$columns %>% - expect_equal("value_1") + expect_equal("open") - # Expect that `summary[[1]]$fns` is a `fun_list` object + # Expect that `summary[[1]]$fns` is a `list` object summary[[1]]$fns %>% expect_is("list") - # Expect that the components of `summary[[1]]$fns` are quosures + # Expect that the components of `summary[[1]]$fns` are formulas summary[[1]]$fns$average %>% expect_is("formula") summary[[1]]$fns$total %>% expect_is("formula") summary[[1]]$fns$`std dev` %>% expect_is("formula") @@ -132,26 +245,39 @@ test_that("the `summary_rows()` function works correctly", { summary[[1]]$formatter_options %>% expect_is("list") + # Expect that `summary[[1]]$formatter_options` is + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + # Create a table with two sets of summary rows for all groups # and all columns gt_tbl <- - gt(tbl) %>% + tbl %>% summary_rows( - columns = vars(value_1, value_2), + groups = TRUE, + columns = vars(open, high, low, close), fns = list( average = ~mean(., na.rm = TRUE), total = ~sum(., na.rm = TRUE), - `std dev` = ~sd(., na.rm = TRUE))) %>% + `std dev` = ~sd(., na.rm = TRUE) + ) + ) %>% summary_rows( - columns = vars(value_1, value_2), + groups = TRUE, + columns = vars(open, high, low, close), fns = list( - max = ~max(., na.rm = TRUE))) + max = ~max(., na.rm = TRUE) + ) + ) # Extract the internal `summary` object summary <- attr(gt_tbl, "summary", exact = TRUE) - # Expect that the internal `summary` list object has - # a length of `2 since there are two calls of `summary_rows()` + # Expect that the internal `summary` list + # object has a length of `2` since there + # were two calls of `summary_rows()` length(summary) %>% expect_equal(2) @@ -160,14 +286,16 @@ test_that("the `summary_rows()` function works correctly", { summary[[1]] %>% names() %>% expect_equal( - c("groups", "columns", "fns", "missing_text", - "formatter", "formatter_options")) + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) summary[[2]] %>% names() %>% expect_equal( - c("groups", "columns", "fns", "missing_text", - "formatter", "formatter_options")) + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) # Expect that `summary[[1|2]]$groups` is TRUE summary[[1]]$groups %>% @@ -178,19 +306,19 @@ test_that("the `summary_rows()` function works correctly", { # Expect that `summary[[1|2]]$columns` has specific values summary[[1]]$columns %>% - expect_equal(c("value_1", "value_2")) + expect_equal(c("open", "high", "low", "close")) summary[[2]]$columns %>% - expect_equal(c("value_1", "value_2")) + expect_equal(c("open", "high", "low", "close")) - # Expect that `summary[[1|2]]$fns` is a `fun_list` object + # Expect that `summary[[1|2]]$fns` is a `list` object summary[[1]]$fns %>% expect_is("list") summary[[2]]$fns %>% expect_is("list") - # Expect that the components of `summary[[1|2]]$fns` are quosures + # Expect that the components of `summary[[1|2]]$fns` are formulas summary[[1]]$fns$average %>% expect_is("formula") summary[[1]]$fns$total %>% expect_is("formula") summary[[1]]$fns$`std dev` %>% expect_is("formula") @@ -217,45 +345,661 @@ test_that("the `summary_rows()` function works correctly", { summary[[2]]$formatter_options %>% expect_is("list") - # Create a table with summary rows for the `A` and `C` groups; - # the 3 summary rows for these groups represent the mean, sum, - # and standard deviation of `value` + # Expect that `summary[[1|2]]$formatter_options` are both + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + summary[[2]]$formatter_options %>% + length() %>% + expect_equal(0) + + # Create a table with two sets of summary rows for all groups + # and all columns + gt_tbl <- + tbl %>% + summary_rows( + groups = TRUE, + columns = vars(open, high), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE) + ) + ) %>% + summary_rows( + groups = TRUE, + columns = vars(low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE) + ) + ) + + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect that the internal `summary` list + # object has a length of `2` since there + # were two calls of `summary_rows()` + length(summary) %>% + expect_equal(2) + + # For the two list components in `summary`, expect specific + # names within them + summary[[1]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + summary[[2]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + # Expect that `summary[[1|2]]$groups` is TRUE + summary[[1]]$groups %>% + expect_true() + + summary[[2]]$groups %>% + expect_true() + + # Expect that `summary[[1|2]]$columns` has specific values + summary[[1]]$columns %>% + expect_equal(c("open", "high")) + + summary[[2]]$columns %>% + expect_equal(c("low", "close")) + + # Expect that `summary[[1|2]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") + + summary[[2]]$fns %>% + expect_is("list") + + # Expect that the components of `summary[[1|2]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + summary[[2]]$fns$average %>% expect_is("formula") + summary[[2]]$fns$total %>% expect_is("formula") + summary[[2]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1|2]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + summary[[2]]$missing_text %>% + expect_equal("---") + + # Expect that `summary[[1|2]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + summary[[2]]$formatter %>% + expect_is("function") + + # Expect that `summary[[1|2]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + summary[[2]]$formatter_options %>% + expect_is("list") + + # Expect that `summary[[1|2]]$formatter_options` are both + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + summary[[2]]$formatter_options %>% + length() %>% + expect_equal(0) +}) + +test_that("the `summary_rows()` can make grand summaries", { + + # Create a table with a grand summary; + # the 3 summary rows for represent the + # mean, sum, and standard deviation of + # all numeric columns gt_tbl <- - gt(tbl) %>% + tbl %>% summary_rows( - groups = c("A", "C"), - columns = vars(value_1), + groups = NULL, + columns = vars(open, high, low, close), fns = list( average = ~mean(., na.rm = TRUE), total = ~sum(., na.rm = TRUE), `std dev` = ~sd(., na.rm = TRUE))) - # Extract the summary data from `gt_tbl` - summaries <- extract_summary(gt_tbl) + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect that the internal `summary` list + # object has a length of `1` since there was + # only one call of `summary_rows()` + length(summary) %>% + expect_equal(1) + + # For the single list component in `summary`, expect specific + # names within it + summary[[1]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + # Expect the `groups` provided in `summary[[1]]$groups` + # is NULL + summary[[1]]$groups %>% + expect_null() + + # Expect the `columns` provided in `summary[[1]]$columns` + # provide names for all columns + summary[[1]]$columns %>% + expect_equal(c("open", "high", "low", "close")) - # Expect that `summaries` is a list object - summaries %>% expect_is("list") + # Expect that `summary[[1]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") - # Expect that `summaries` has a length of `2` - summaries %>% + # Expect that the components of `summary[[1]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + # Expect that `summary[[1]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + # Expect that `summary[[1]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + # Create a table with a grand summary; + # the 3 summary rows for represent the + # mean, sum, and standard deviation of + # all numeric columns; split into 2 calls + # that allow for different formatting + # options + gt_tbl <- + tbl %>% + summary_rows( + groups = NULL, + columns = vars(open, high), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE)), + formatter = fmt_number, + decimals = 3) %>% + summary_rows( + groups = NULL, + columns = vars(low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE)), + formatter = fmt_number, + decimals = 5) + + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect that the internal `summary` list + # object has a length of `2` since there + # were two calls of `summary_rows()` + length(summary) %>% + expect_equal(2) + + # For the two list components in `summary`, expect specific + # names within them + summary[[1]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + summary[[2]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + # Expect that `summary[[1|2]]$groups` is TRUE + summary[[1]]$groups %>% + expect_null() + + summary[[2]]$groups %>% + expect_null() + + # Expect that `summary[[1|2]]$columns` has specific values + summary[[1]]$columns %>% + expect_equal(c("open", "high")) + + summary[[2]]$columns %>% + expect_equal(c("low", "close")) + + # Expect that `summary[[1|2]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") + + summary[[2]]$fns %>% + expect_is("list") + + # Expect that the functions used in each call + # are the same + expect_identical(summary[[1]]$fns, summary[[1]]$fns) + + # Expect that the components of `summary[[1|2]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + summary[[2]]$fns$average %>% expect_is("formula") + summary[[2]]$fns$total %>% expect_is("formula") + summary[[2]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1|2]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + summary[[2]]$missing_text %>% + expect_equal("---") + + # Expect that `summary[[1|2]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + summary[[2]]$formatter %>% + expect_is("function") + + # Expect that the formatters used in each call + # are the same + expect_identical(summary[[1]]$formatter, summary[[2]]$formatter) + + # Expect that `summary[[1|2]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + summary[[2]]$formatter_options %>% + expect_is("list") + + # Expect that `summary[[1|2]]$formatter_options` are both + # of length 1 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(1) + + summary[[2]]$formatter_options %>% length() %>% + expect_equal(1) + + # Expect that `summary[[1|2]]$formatter_options` + # are both named `decimals` + summary[[1]]$formatter_options %>% + names() %>% + expect_equal("decimals") + + summary[[2]]$formatter_options %>% + names() %>% + expect_equal("decimals") + + # Expect that the `summary[[1|2]]$formatter_options` + # `decimals` options have specific values + summary[[1]]$formatter_options[[1]] %>% + expect_equal(3) + + summary[[2]]$formatter_options[[1]] %>% + expect_equal(5) + + # Create a table with groupwsie summaries + # and a grand summary; all summary rows + # represent the mean, sum, and standard + # deviation of all numeric columns; + gt_tbl <- + tbl %>% + summary_rows( + groups = TRUE, + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE)) + ) %>% + summary_rows( + groups = NULL, + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE)) + ) + + # Extract the internal `summary` object + summary <- attr(gt_tbl, "summary", exact = TRUE) + + # Expect that the internal `summary` list + # object has a length of `2` since there + # were two calls of `summary_rows()` + length(summary) %>% expect_equal(2) - # Expect that `summaries` the names `A` and `C` - summaries %>% + # For the two list components in `summary`, expect specific + # names within them + summary[[1]] %>% + names() %>% + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) + + summary[[2]] %>% names() %>% - expect_equal(c("A", "C")) + expect_equal( + c("groups", "columns", "fns", "summary_labels", + "missing_text", "formatter", "formatter_options") + ) - # Expect that each of the components contains a `tibble` - summaries[[1]] %>% - expect_is(c("tbl_df", "tbl", "data.frame")) + # Expect that `summary[[1]]$groups` is TRUE + summary[[1]]$groups %>% + expect_true() - summaries[[2]] %>% - expect_is(c("tbl_df", "tbl", "data.frame")) + # Expect that `summary[[1]]$groups` is NULL + summary[[2]]$groups %>% + expect_null() + + # Expect that `summary[[1|2]]$columns` has specific values + summary[[1]]$columns %>% + expect_equal(c("open", "high", "low", "close")) + + summary[[2]]$columns %>% + expect_equal(c("open", "high", "low", "close")) + + # Expect that `summary[[1|2]]$fns` is a `list` object + summary[[1]]$fns %>% + expect_is("list") + + summary[[2]]$fns %>% + expect_is("list") + + # Expect that the functions used in each call + # are the same + expect_identical(summary[[1]]$fns, summary[[1]]$fns) + + # Expect that the components of `summary[[1|2]]$fns` are formulas + summary[[1]]$fns$average %>% expect_is("formula") + summary[[1]]$fns$total %>% expect_is("formula") + summary[[1]]$fns$`std dev` %>% expect_is("formula") + summary[[2]]$fns$average %>% expect_is("formula") + summary[[2]]$fns$total %>% expect_is("formula") + summary[[2]]$fns$`std dev` %>% expect_is("formula") + + # Expect that `summary[[1|2]]$missing_text` has a specific value + summary[[1]]$missing_text %>% + expect_equal("---") + + summary[[2]]$missing_text %>% + expect_equal("---") - # Expect an error in the case where `extract_summary()` - # is called on a gt table object where there is no summary - expect_error( - gt(mtcars) %>% - extract_summary()) + # Expect that `summary[[1|2]]$formatter` is a `function` object + summary[[1]]$formatter %>% + expect_is("function") + + summary[[2]]$formatter %>% + expect_is("function") + + # Expect that the formatters used in each call + # are the same + expect_identical(summary[[1]]$formatter, summary[[2]]$formatter) + + # Expect that `summary[[1|2]]$formatter_options` is a list + summary[[1]]$formatter_options %>% + expect_is("list") + + summary[[2]]$formatter_options %>% + expect_is("list") + + # Expect that `summary[[1|2]]$formatter_options` are both + # of length 0 + summary[[1]]$formatter_options %>% + length() %>% + expect_equal(0) + + summary[[2]]$formatter_options %>% + length() %>% + expect_equal(0) +}) + +test_that("`groups = FALSE` returns data unchanged", { + + # Expect that using `groups = FALSE` with + # `summary_rows()` creates no summary rows + expect_equal( + tbl %>% as_raw_html(), + tbl %>% + summary_rows( + groups = FALSE, + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE))) %>% + as_raw_html() + ) +}) + +test_that("summary rows can be created when there is no stub", { + + # Create a table based on `sp500`, with + # four columns of values + tbl_2 <- + sp500 %>% + dplyr::filter( + date >= "2015-01-05" & + date <="2015-01-09" + ) %>% + dplyr::arrange(date) %>% + dplyr::select(-adj_close, -volume) %>% + gt() + + # Create a gt table with a grand summary; + # the table doesn't have a stub (and there + # are no row groups) + gt_tbl <- + tbl_2 %>% + summary_rows( + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE)) + ) + + # Extract `output_df` in the HTML context and + # expect that the `rowname` column is entirely + # filled with empty strings + expect_equal( + (gt_tbl %>% render_formats_test("html"))[["rowname"]], + rep("", 5) + ) + + # Expect that the grand summary row labels are + # available in the rendered output table + expect_match( + gt_tbl %>% + as_raw_html(inline_css = FALSE), + "") + + expect_match( + gt_tbl %>% + as_raw_html(inline_css = FALSE), + "") + + expect_match( + gt_tbl %>% + as_raw_html(inline_css = FALSE), + "") +}) + +test_that("extracting a summary from a gt table is possible", { + + # Create a table with summary rows for + # the `W02` and `W03` groups; the 3 summary + # rows represent the mean, sum, and standard + # deviation of all numeric columns; extract + # the internal summary with `extract_summary()` + gt_tbl_summary_groupwise <- + tbl %>% + summary_rows( + groups = c("W02", "W03"), + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE))) %>% + extract_summary() + + # Expect that the summary object is a list + expect_is(gt_tbl_summary_groupwise, "list") + + # Expect that the length of the list is `2` + expect_equal(length(gt_tbl_summary_groupwise), 2) + + # Expect specific names for the list components + expect_equal( + names(gt_tbl_summary_groupwise), + c("W02", "W03") + ) + + # Expect that each component of the list inherits + # from `tbl_df` + expect_is(gt_tbl_summary_groupwise[[1]], "tbl_df") + expect_is(gt_tbl_summary_groupwise[[2]], "tbl_df") + + # Expect specific column names for each of the + # tibbles in `gt_tbl_summary_groupwise` + expect_equal( + names(gt_tbl_summary_groupwise[[1]]), + c("groupname", "rowname", "open", "high", "low", "close") + ) + + expect_equal( + names(gt_tbl_summary_groupwise[[2]]), + c("groupname", "rowname", "open", "high", "low", "close") + ) + + # Expect specific values in each of the tibbles + expect_equal( + gt_tbl_summary_groupwise[[1]]$open, + c(2035.23998, 10176.19990, 23.65756), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[1]]$high, + c(2048.56198, 10242.80990, 17.47612), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[1]]$low, + c(2016.8540, 10084.2699, 18.5372), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[1]]$close, + c(2031.2080, 10156.0400, 22.9171), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[2]]$open, + c(2020.42200, 10102.11000, 20.17218), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[2]]$high, + c(2033.28798, 10166.43990, 18.33064), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[2]]$low, + c(1999.77198, 9998.85990, 15.20847), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_groupwise[[2]]$close, + c(2014.9300, 10074.6500, 13.8957), tolerance = .002 + ) + + # Create a table with a grand summary; the 3 + # summary rows represent the mean, sum, and + # standard deviation of all numeric columns; + # extract the internal summary with `extract_summary()` + gt_tbl_summary_grand <- + tbl %>% + summary_rows( + columns = vars(open, high, low, close), + fns = list( + average = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + `std dev` = ~sd(., na.rm = TRUE))) %>% + extract_summary() + + # Expect that the summary object is a list + expect_is(gt_tbl_summary_grand, "list") + + # Expect that the length of the list is `1` + expect_equal(length(gt_tbl_summary_grand), 1) + + # Expect a specific name for the one list component + expect_equal(names(gt_tbl_summary_grand), "::GRAND_SUMMARY") + + # Expect that the single component of the list inherits + # from `tbl_df` + expect_is(gt_tbl_summary_grand[[1]], "tbl_df") + + # Expect specific column names for the + # tibble in `gt_tbl_summary_grand` + expect_equal( + names(gt_tbl_summary_grand[[1]]), + c("groupname", "rowname", "open", "high", "low", "close") + ) + + # Expect specific values in the tibble + expect_equal( + gt_tbl_summary_grand[[1]]$open, + c(2027.83099, 20278.30990, 22.14929), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_grand[[1]]$high, + c(2040.92498, 20409.24980, 18.70516), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_grand[[1]]$low, + c(2008.31298, 20083.12980, 18.34602), tolerance = .002 + ) + + expect_equal( + gt_tbl_summary_grand[[1]]$close, + c(2023.06900, 20230.69000, 19.82022), tolerance = .002 + ) + + # Expect an error with `extract_summary()` if there + # are no summaries (i.e., `summary_rows()` wasn't used) + expect_error(tbl %>% extract_summary()) }) diff --git a/tests/testthat/test-tab_footnote.R b/tests/testthat/test-tab_footnote.R index f399213704..29de64fce2 100644 --- a/tests/testthat/test-tab_footnote.R +++ b/tests/testthat/test-tab_footnote.R @@ -10,11 +10,14 @@ data <- cols_hide(columns = "vs") %>% tab_row_group( group = "Mercs", - rows = contains("Merc") + rows = contains("Merc"), ) %>% tab_row_group( group = "Mazdas", - rows = contains("Mazda") + rows = contains("Mazda"), + ) %>% + tab_row_group( + others = "Others" ) %>% tab_spanner( label = "gear_carb_cyl", @@ -36,6 +39,12 @@ data <- fns = list( ~mean(., na.rm = TRUE), ~sum(., na.rm = TRUE)) + ) %>% + summary_rows( + columns = vars(hp, wt), + fns = list( + ~mean(., na.rm = TRUE), + ~sum(., na.rm = TRUE)) ) # Create a table from `gtcars` that has footnotes @@ -67,6 +76,50 @@ data_2 <- tab_spanner(label = "make and model", columns = vars(mfr, model)) %>% tab_spanner(label = "specs and pricing", columns = vars(drivetrain, msrp)) + +# Create a table from `gtcars` that has footnotes +# in group summary and grand summary cells +data_3 <- + gtcars %>% + dplyr::filter(ctry_origin == "Germany") %>% + dplyr::group_by(mfr) %>% + dplyr::top_n(3, msrp) %>% + dplyr::ungroup() %>% + dplyr::select(mfr, model, drivetrain, msrp) %>% + gt(rowname_col = "model", groupname_col = "mfr") %>% + summary_rows( + groups = c("BMW", "Audi"), + columns = vars(msrp), + fns = list( + ~mean(., na.rm = TRUE), + ~min(., na.rm = TRUE)) + ) %>% + summary_rows( + columns = vars(msrp), + fns = list( + ~min(., na.rm = TRUE), + ~max(., na.rm = TRUE)) + ) %>% + tab_footnote( + footnote = "Average price for BMW and Audi.", + locations = cells_summary( + groups = c("BMW", "Audi"), + columns = vars(msrp), + rows = starts_with("me")) + ) %>% + tab_footnote( + footnote = "Maximum price across all cars.", + locations = cells_grand_summary( + columns = vars(msrp), + rows = starts_with("ma")) + ) %>% + tab_footnote( + footnote = "Minimum price across all cars.", + locations = cells_grand_summary( + columns = vars(msrp), + rows = starts_with("mi")) + ) + # Function to skip tests if Suggested packages not available on system check_suggests <- function() { skip_if_not_installed("rvest") @@ -103,7 +156,9 @@ test_that("the `tab_footnote()` function works correctly", { footnote = "Column labels and stub footnote.", locations = list( cells_column_labels(columns = TRUE), - cells_stub(rows = TRUE))) + cells_stub(rows = TRUE) + ) + ) # Expect that the internal `footnotes_df` data frame will have # its `locname` column entirely populated with `columns_columns` @@ -125,7 +180,8 @@ test_that("the `tab_footnote()` function works correctly", { data %>% tab_footnote( footnote = "Stub cell footnote.", - locations = cells_stub(rows = "Merc 240D")) + locations = cells_stub(rows = "Merc 240D") + ) # Expect that the internal `footnotes_df` data frame will have # a single row @@ -138,14 +194,16 @@ test_that("the `tab_footnote()` function works correctly", { expect_attr_equal( tab, "footnotes_df", c("stub", "5", NA_character_, NA_character_, "8", - "Stub cell footnote.")) + "Stub cell footnote.") + ) # Apply a footnote to the table title tab <- data %>% tab_footnote( footnote = "Title footnote.", - locations = cells_title(groups = "title")) + locations = cells_title(groups = "title") + ) # Expect that the internal `footnotes_df` data frame will have # a single row @@ -158,14 +216,16 @@ test_that("the `tab_footnote()` function works correctly", { expect_attr_equal( tab, "footnotes_df", c("title", "1", NA_character_, NA_character_, NA_character_, - "Title footnote.")) + "Title footnote.") + ) # Apply a footnote to the table subtitle tab <- data %>% tab_footnote( footnote = "Subtitle footnote.", - locations = cells_title(groups = "subtitle")) + locations = cells_title(groups = "subtitle") + ) # Expect that the internal `footnotes_df` data frame will have # a single row @@ -178,7 +238,8 @@ test_that("the `tab_footnote()` function works correctly", { expect_attr_equal( tab, "footnotes_df", c("subtitle", "2", NA_character_, NA_character_, NA_character_, - "Subtitle footnote.")) + "Subtitle footnote.") + ) # Apply a footnote to a single cell in a group summary section tab <- @@ -186,7 +247,8 @@ test_that("the `tab_footnote()` function works correctly", { tab_footnote( footnote = "Summary cell footnote.", locations = cells_summary( - groups = "Mercs", columns = "hp", rows = 2)) + groups = "Mercs", columns = "hp", rows = 2) + ) # Expect that the internal `footnotes_df` data frame will have # a single row @@ -199,7 +261,106 @@ test_that("the `tab_footnote()` function works correctly", { expect_attr_equal( tab, "footnotes_df", c("summary_cells", "5", "Mercs", "hp", "2", - "Summary cell footnote.")) + "Summary cell footnote.") + ) + + # Expect an error if columns couldn't be resolved + expect_error( + data %>% + tab_footnote( + footnote = "Summary cell footnote.", + locations = cells_summary( + groups = "Mercs", columns = starts_with("x"), rows = 2) + ) + ) + + # Expect an error if rows couldn't be resolved + expect_error( + data %>% + tab_footnote( + footnote = "Summary cell footnote.", + locations = cells_summary( + groups = "Mercs", columns = starts_with("m"), rows = starts_with("x")) + ) + ) + + # Apply a footnote to a single cell in a grand + # summary section + tab <- + data %>% + tab_footnote( + footnote = "Grand summary cell footnote.", + locations = cells_grand_summary( + columns = vars(wt), rows = starts_with("s") + ) + ) + + # Expect that the internal `footnotes_df` data frame + # will have a single row + attr(tab, "footnotes_df", exact = TRUE) %>% + nrow() %>% + expect_equal(1) + + # Expect certain values for each of the columns in the + # single-row `footnotes_df` data frame + expect_attr_equal( + tab, "footnotes_df", + c("grand_summary_cells", "6", NA, "wt", "2", + "Grand summary cell footnote.") + ) + + # Expect an error if columns couldn't be resolved + expect_error( + data %>% + tab_footnote( + footnote = "Grand summary cell footnote.", + locations = cells_grand_summary( + columns = starts_with("x"), rows = 2) + ) + ) + + # Expect an error if rows couldn't be resolved + expect_error( + data %>% + tab_footnote( + footnote = "Grand summary cell footnote.", + locations = cells_grand_summary( + columns = starts_with("m"), rows = starts_with("x")) + ) + ) + + # Apply a footnote to a single cell in a group + # summary section, and, to a single cell in a grand + # summary section + tab <- + data %>% + tab_footnote( + footnote = "Summary cell footnote.", + locations = cells_summary( + groups = "Mercs", columns = "hp", rows = 2) + ) %>% + tab_footnote( + footnote = "Grand summary cell footnote.", + locations = cells_grand_summary( + columns = vars(wt), rows = starts_with("s") + ) + ) + + # Expect that the internal `footnotes_df` data frame + # will have two rows + attr(tab, "footnotes_df", exact = TRUE) %>% + nrow() %>% + expect_equal(2) + + # Expect certain values for each of the columns in the + # double-row `footnotes_df` data frame + expect_attr_equal( + tab, "footnotes_df", + c("summary_cells", "grand_summary_cells", + "5", "6", "Mercs", NA, "hp", "wt", "2", "2", + "Summary cell footnote.", + "Grand summary cell footnote.") + ) # Apply a footnote to the `Mazdas` stub group cell tab <- @@ -208,8 +369,8 @@ test_that("the `tab_footnote()` function works correctly", { footnote = "Group cell footnote.", locations = cells_group(groups = "Mazdas")) - # Expect that the internal `footnotes_df` data frame will have - # a single row + # Expect that the internal `footnotes_df` data frame + # will have a single row attr(tab, "footnotes_df", exact = TRUE) %>% nrow() %>% expect_equal(1) @@ -392,5 +553,122 @@ test_that("the `tab_footnote()` function works correctly", { tbl_html %>% selection_text(selection = "[class='gt_footnote_glyph']") %>% expect_equal(rep(as.character(1:4), 2)) +}) + +test_that("the `apply_footnotes_to_output()` function works correctly", { + + # Build the `data_3` object (using the `html` context) + # and obtain the `built_data` list object + built_data <- build_data(data_3, context = "html") + + # Extract `footnotes_resolved` and `list_of_summaries` + footnotes_resolved <- built_data$footnotes_resolved + list_of_summaries <- built_data$list_of_summaries + + # Expect that the `footnotes_resolved` object inherits + # from `tbl_df` + expect_is(footnotes_resolved, "tbl_df") + + # Expect that there are specific column names in + # this tibble + expect_equal( + colnames(footnotes_resolved), + c("locname", "locnum", "grpname", "colname", "rownum", + "text", "colnum", "fs_id") + ) + + # Expect that there are 4 rows in this tibble + expect_equal(nrow(footnotes_resolved), 4) + + # Expect specific values to be in `footnotes_resolved` + expect_equal( + footnotes_resolved$locname, + c("summary_cells", "summary_cells", + "grand_summary_cells", "grand_summary_cells") + ) + expect_equal(footnotes_resolved$locnum, c(5, 5, 6, 6)) + expect_equal(footnotes_resolved$grpname, c("BMW", "Audi", NA, NA)) + expect_equal(footnotes_resolved$colname, rep("msrp", 4)) + expect_equal(footnotes_resolved$rownum, c(3.01, 6.01, 1.00, 2.00)) + expect_equal( + footnotes_resolved$text, + c("Average price for BMW and Audi.", "Average price for BMW and Audi.", + "Minimum price across all cars.", "Maximum price across all cars.") + ) + expect_equal(footnotes_resolved$colnum, rep(2, 4)) + expect_equal(footnotes_resolved$fs_id, c("1", "1", "2", "3")) + + # Expect that the list of summaries has length `2` + expect_equal(length(list_of_summaries), 2) + + # Expect specific names in the `list_of_summaries` list + expect_equal( + names(list_of_summaries), + c("summary_df_data_list", "summary_df_display_list") + ) + + # Expect three tibbles in the `summary_df_data_list` component + expect_equal(length(list_of_summaries$summary_df_data_list), 3) + + # Expect three tibbles in the `summary_df_display_list` component + expect_equal(length(list_of_summaries$summary_df_display_list), 3) + + # Expect specific names for the subcomponents of the + # `summary_df_data_list` and `summary_df_data_list` + # parent components + expect_equal( + names(list_of_summaries$summary_df_data_list), + c("BMW", "Audi", "::GRAND_SUMMARY") + ) + expect_equal( + names(list_of_summaries$summary_df_display_list), + c("::GRAND_SUMMARY", "Audi", "BMW") + ) + + # Expect formatted cell values with no HTML footnote markup + expect_equal( + list_of_summaries$summary_df_display_list$`::GRAND_SUMMARY`$msrp, + c("56,000.00", "140,700.00") + ) + + expect_equal( + list_of_summaries$summary_df_display_list$Audi$msrp, + c("113,233.33", "108,900.00") + ) + + expect_equal( + list_of_summaries$summary_df_display_list$BMW$msrp, + c("116,066.67", "94,100.00") + ) + + # Use the `apply_footnotes_to_summary()` function to modify + # the cell values in the `list_of_summaries$summary_df_display_list` + # subcomponent of `list_of_summaries` + applied_footnotes <- + apply_footnotes_to_summary(list_of_summaries, footnotes_resolved) + + # Expect no change in the `summary_df_data_list` subcomponent + # as a result of the transformation + expect_equivalent( + list_of_summaries$summary_df_data_list, + applied_footnotes$summary_df_data_list + ) + + # Expect formatted cell values with HTML footnote markup + expect_equal( + applied_footnotes$summary_df_display_list$`::GRAND_SUMMARY`$msrp, + c("56,000.002", + "140,700.003") + ) + + expect_equal( + applied_footnotes$summary_df_display_list$Audi$msrp, + c("113,233.331", "108,900.00") + ) + + expect_equal( + applied_footnotes$summary_df_display_list$BMW$msrp, + c("116,066.671", "94,100.00") + ) }) diff --git a/tests/testthat/test-tab_options.R b/tests/testthat/test-tab_options.R index 88d80d09bb..92674cf150 100644 --- a/tests/testthat/test-tab_options.R +++ b/tests/testthat/test-tab_options.R @@ -517,17 +517,17 @@ test_that("the internal `opts_df` table can be correctly modified", { dplyr::filter(parameter == "row_padding") %>% dplyr::pull(value), attr(tbl_html, "opts_df", exact = TRUE) %>% dplyr::filter(parameter == "row_padding") %>% dplyr::pull(value)) %>% - expect_equal(c("10px", "8px")) + expect_equal(c("8px", "8px")) # Modify the `row.padding` option using just a numeric value - tbl_html <- data %>% tab_options(row.padding = 8) + tbl_html <- data %>% tab_options(row.padding = 6) # Compare before and after values c(opts_df_1 %>% dplyr::filter(parameter == "row_padding") %>% dplyr::pull(value), attr(tbl_html, "opts_df", exact = TRUE) %>% dplyr::filter(parameter == "row_padding") %>% dplyr::pull(value)) %>% - expect_equal(c("10px", "8px")) + expect_equal(c("8px", "6px")) # Modify the `summary_row.background.color` tbl_html <- data %>% tab_options(summary_row.background.color = "pink") @@ -547,7 +547,7 @@ test_that("the internal `opts_df` table can be correctly modified", { dplyr::filter(parameter == "summary_row_padding") %>% dplyr::pull(value), attr(tbl_html, "opts_df", exact = TRUE) %>% dplyr::filter(parameter == "summary_row_padding") %>% dplyr::pull(value)) %>% - expect_equal(c("6px", "4px")) + expect_equal(c("8px", "4px")) # Modify the `summary_row.padding` option using just a numeric value tbl_html <- data %>% tab_options(summary_row.padding = 4) @@ -557,7 +557,7 @@ test_that("the internal `opts_df` table can be correctly modified", { dplyr::filter(parameter == "summary_row_padding") %>% dplyr::pull(value), attr(tbl_html, "opts_df", exact = TRUE) %>% dplyr::filter(parameter == "summary_row_padding") %>% dplyr::pull(value)) %>% - expect_equal(c("6px", "4px")) + expect_equal(c("8px", "4px")) # Modify the `summary_row.text_transform` tbl_html <- data %>% tab_options(summary_row.text_transform = "lowercase") @@ -569,6 +569,46 @@ test_that("the internal `opts_df` table can be correctly modified", { dplyr::filter(parameter == "summary_row_text_transform") %>% dplyr::pull(value)) %>% expect_equal(c("inherit", "lowercase")) + # Modify the `grand_summary_row.background.color` + tbl_html <- data %>% tab_options(grand_summary_row.background.color = "pink") + + # Compare before and after values + c(opts_df_1 %>% + dplyr::filter(parameter == "grand_summary_row_background_color") %>% dplyr::pull(value), + attr(tbl_html, "opts_df", exact = TRUE) %>% + dplyr::filter(parameter == "grand_summary_row_background_color") %>% dplyr::pull(value)) %>% + expect_equal(c(NA_character_, "pink")) + + # Modify the `grand_summary_row.padding` + tbl_html <- data %>% tab_options(grand_summary_row.padding = px(4)) + + # Compare before and after values + c(opts_df_1 %>% + dplyr::filter(parameter == "grand_summary_row_padding") %>% dplyr::pull(value), + attr(tbl_html, "opts_df", exact = TRUE) %>% + dplyr::filter(parameter == "grand_summary_row_padding") %>% dplyr::pull(value)) %>% + expect_equal(c("8px", "4px")) + + # Modify the `grand_summary_row.padding` option using just a numeric value + tbl_html <- data %>% tab_options(grand_summary_row.padding = 4) + + # Compare before and after values + c(opts_df_1 %>% + dplyr::filter(parameter == "grand_summary_row_padding") %>% dplyr::pull(value), + attr(tbl_html, "opts_df", exact = TRUE) %>% + dplyr::filter(parameter == "grand_summary_row_padding") %>% dplyr::pull(value)) %>% + expect_equal(c("8px", "4px")) + + # Modify the `grand_summary_row.text_transform` + tbl_html <- data %>% tab_options(grand_summary_row.text_transform = "lowercase") + + # Compare before and after values + c(opts_df_1 %>% + dplyr::filter(parameter == "grand_summary_row_text_transform") %>% dplyr::pull(value), + attr(tbl_html, "opts_df", exact = TRUE) %>% + dplyr::filter(parameter == "grand_summary_row_text_transform") %>% dplyr::pull(value)) %>% + expect_equal(c("inherit", "lowercase")) + # Modify the `footnote.font.size` tbl_html <- data %>% tab_options(footnote.font.size = px(12)) diff --git a/tests/testthat/test-tab_style.R b/tests/testthat/test-tab_style.R index 8af6951d61..62ea98b3db 100644 --- a/tests/testthat/test-tab_style.R +++ b/tests/testthat/test-tab_style.R @@ -15,6 +15,9 @@ data <- group = "Mazdas", rows = contains("Mazda") ) %>% + tab_row_group( + others = "Others" + ) %>% tab_spanner( label = "gear_carb_cyl", columns = vars(gear, carb, cyl) @@ -35,6 +38,12 @@ data <- fns = list( ~mean(., na.rm = TRUE), ~sum(., na.rm = TRUE)) + ) %>% + summary_rows( + columns = vars(hp, wt), + fns = list( + ~mean(., na.rm = TRUE), + ~sum(., na.rm = TRUE)) ) # Function to skip tests if Suggested packages not available on system @@ -176,6 +185,69 @@ test_that("a gt table can store the correct style statements", { c("summary_cells", "5", "Mercs", "hp", "2", "background-color:green;color:white;")) + # Expect an error if columns couldn't be resolved + expect_error( + data %>% + tab_style( + style = cells_styles(bkgd_color = "green", text_color = "white"), + locations = cells_summary( + groups = "Mercs", columns = starts_with("x"), rows = 2) + ) + ) + + # Expect an error if rows couldn't be resolved + expect_error( + data %>% + tab_style( + style = cells_styles(bkgd_color = "green", text_color = "white"), + locations = cells_summary( + groups = "Mercs", columns = starts_with("m"), rows = starts_with("x")) + ) + ) + + # Apply a red background with white text to a single cell in + # the grand summary section + tbl_html <- + data %>% + tab_style( + style = cells_styles(bkgd_color = "red", text_color = "white"), + locations = cells_grand_summary( + columns = "hp", rows = vars(sum)) + ) + + # Expect that the internal `styles_df` data frame will have + # a single row + attr(tbl_html, "styles_df", exact = TRUE) %>% + nrow() %>% + expect_equal(1) + + # Expect certain values for each of the columns in the + # single-row `styles_df` data frame + expect_attr_equal( + tbl_html, "styles_df", + c("grand_summary_cells", "6", NA, "hp", "2", + "background-color:red;color:white;")) + + # Expect an error if columns couldn't be resolved + expect_error( + data %>% + tab_style( + style = cells_styles(bkgd_color = "red", text_color = "white"), + locations = cells_grand_summary( + columns = starts_with("x"), rows = 2) + ) + ) + + # Expect an error if rows couldn't be resolved + expect_error( + data %>% + tab_style( + style = cells_styles(bkgd_color = "red", text_color = "white"), + locations = cells_grand_summary( + columns = starts_with("m"), rows = starts_with("x")) + ) + ) + # Apply a `yellow` background to the `Mazdas` stub group cell tbl_html <- data %>% diff --git a/tests/testthat/test-table_parts.R b/tests/testthat/test-table_parts.R index ac05505871..74b4a7a4a7 100644 --- a/tests/testthat/test-table_parts.R +++ b/tests/testthat/test-table_parts.R @@ -286,6 +286,12 @@ test_that("a gt table contains custom styles at the correct locations", { ~mean(., na.rm = TRUE), ~sum(., na.rm = TRUE)) ) %>% + summary_rows( + columns = vars(hp, wt, qsec), + fns = list( + ~mean(., na.rm = TRUE), + ~sum(., na.rm = TRUE)) + ) %>% tab_style( style = cells_styles(bkgd_color = "lightgray"), locations = list( @@ -309,6 +315,10 @@ test_that("a gt table contains custom styles at the correct locations", { locations = cells_summary( groups = "Mercs", columns = "hp", rows = 2) ) %>% + tab_style( + style = cells_styles(bkgd_color = "purple", text_color = "white"), + locations = cells_grand_summary(columns = "hp", rows = 2) + ) %>% tab_style( style = cells_styles(bkgd_color = "lightgreen"), locations = cells_column_labels(groups = "gear_carb_cyl") @@ -354,6 +364,12 @@ test_that("a gt table contains custom styles at the correct locations", { rvest::html_text("[class='gt_row gt_summary_row gt_center']") %>% expect_equal("943.00") + # Expect that the grand summary cell (`sum`/`hp`) is styled + tbl_html %>% + rvest::html_nodes("[style='background-color:purple;color:white;']") %>% + rvest::html_text("[class='gt_row gt_grand_summary_row gt_center']") %>% + expect_equal("4,694.00") + # Expect that some column labels (e.g., `disp`, `wt`, etc.) are # styled with a lightgrey background (tbl_html %>% diff --git a/tests/testthat/test-util_functions.R b/tests/testthat/test-util_functions.R index 8905fd7917..442f4feb84 100644 --- a/tests/testthat/test-util_functions.R +++ b/tests/testthat/test-util_functions.R @@ -361,7 +361,7 @@ test_that("the `get_css_tbl()` function works correctly", { css_tbl %>% expect_is(c("tbl_df", "tbl", "data.frame")) - css_tbl %>% dim() %>% expect_equal(c(103, 4)) + css_tbl %>% dim() %>% expect_equal(c(110, 4)) css_tbl %>% colnames() %>% @@ -410,7 +410,7 @@ test_that("the `inline_html_styles()` function works correctly", { # Expect that the style rule from `tab_style` is a listed value along with # the inlined rules derived from the CSS classes expect_true( - grepl("style=\"padding:10px;margin:10px;vertical-align:middle;text-align:right;font-variant-numeric:tabular-nums;font-size:10px;\"", inlined_html) + grepl("style=\"padding:8px;margin:10px;vertical-align:middle;text-align:right;font-variant-numeric:tabular-nums;font-size:10px;\"", inlined_html) ) # Create a gt table with a custom style in the title and subtitle
", + row_splits_grand_summary[[j]][1], + "", + row_splits_grand_summary[[j]][-1], + "
averagetotalstd dev