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
", + row_splits_grand_summary[[j]][1], + " | "), "\n", + paste0( + "", + row_splits_grand_summary[[j]][-1], + " | ", collapse = "\n"), + "\naverage | ") + + expect_match( + gt_tbl %>% + as_raw_html(inline_css = FALSE), + "total | ") + + expect_match( + gt_tbl %>% + as_raw_html(inline_css = FALSE), + "std dev | ") +}) + +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