How can I produce a table through xtable
with headings in groups, like the following?
Change your data frame so the grouped names are of the form <group><separator><name>
then use the function at the bottom of this answer.
\documentclass{article}
\usepackage{booktabs}
\begin{document}
.
<<two-row-headings, results='asis'>>=
library(xtable)
library(grattanCharts) # devtools::install_github('hughparsonage/grattanCharts')
example_df <-
data.frame(yr = 2001:2005,
Revenue__foo = 1:5,
Revenue__bar = 11:15,
Revenue__baz = 21:25,
ordinary = 1:5,
Expense__foo = 1:5,
Expense__bar = 11:15,
Expense__baz = 21:25,
Last__foo = 1:5,
Last__baz = 2:6)
print_2heading_xtable(example_df, separator = "__")
@
\end{document}
The package is exported from the grattanCharts
package. But you can use the function independently:
#' Print LaTeX tables with headings over two lines
#'
#' @param .data A data frame with at least some of the column names having a \code{separator}.
#' @param separator A regular expression that splits the top and bottom rows. If the separator is not found in the names of \code{.data}, the function returns an error (saying you should probably just use \code{print.xtable()})
#' @param xtable.align Passed to \code{xtable}: Character vector of length equal to the number of columns of the resulting table, indicating the alignment of the corresponding columns.
#' @param booktabs Should the tabular environment produced use booktabs? Set to TRUE for (my) convenience. This will cause an error if \verb{\usepackage{booktabs}} has not been called in LaTeX.
#' @param heading_command A (simple) LaTeX control sequence (properly escaped) to apply to each heading names.
#' @param ... Arguments passed to \code{print.xtable}. You cannot pass \code{add.to.row}, \code{include.rownames}, or \code{include.colnames} as we make use of these options in this function.
#' @return Output intended for LaTeX. A table produced using xtable where groups of column names are put in the top row.
#' @author Hugh Parsonage
#' @examples
#' example_df <-
#' data.frame(yr = 2001:2005,
#' Revenue__foo = 1:5,
#' Revenue__bar = 11:15,
#' Revenue__baz = 21:25,
#' ordinary = 1:5,
#' Expense__foo = 1:5,
#' Expense__bar = 11:15,
#' Expense__baz = 21:25,
#' Last__foo = 1:5,
#' Last__baz = 2:6,
#' last = 101:105)
#' print_2heading_xtable(example_df, separator = "__")
#' @export
print_2heading_xtable <- function(.data,
separator = "__",
xtable.align = NULL,
booktabs = TRUE,
heading_command = "\\textbf", ...){
orig_names <- names(.data)
if (!any(grepl(separator, orig_names))){
stop("No separator found in column names, so there is no point in using this function. Make sure you have specified the right separator; otherwise, just use print.xtable().")
}
if (any(c("add.to.row", "include.colnames", "include.rownames") %in% names(list(...)))){
stop("You should not pass add.to.row, include.colnames, or include.rownames to print.xtable() via this function.")
}
split_names <- grep(separator, orig_names, value = TRUE)
split_positions <- grep(separator, orig_names, value = FALSE)
# get the names before the separator
top_headers <- gsub(paste0("^(.*)", separator, ".*$"), "\\1", split_names)
# Where in the original table is there a new top header?
orig_names_no_suffix <-
gsub(paste0("^(.*)", separator, ".*$"), paste0("\\1", separator), orig_names)
# For cmidrule{}
position_of_header_instance <-
# Need to test first column
which(orig_names_no_suffix == dplyr::lead(orig_names_no_suffix) &
(orig_names_no_suffix != dplyr::lag(orig_names_no_suffix) | is.na(dplyr::lag(orig_names_no_suffix))))
position_of_header_final <-
# Need to test final column
which((orig_names_no_suffix != dplyr::lead(orig_names_no_suffix) | is.na(dplyr::lead(orig_names_no_suffix))) &
orig_names_no_suffix == dplyr::lag(orig_names_no_suffix))
if (length(position_of_header_instance) != length(position_of_header_final)){
stop("This is a bug. Sorry. Please provide your data frame to the grattan package maintainer.")
}
double_row_column_names <-
rbind(gsub("^(.*)__(.*)$", "\\1", orig_names), gsub("^(.*)__(.*)$", "\\2", orig_names))
# factor etc in table to preserve order
top_headers_widths <-
as.data.frame(table(factor(double_row_column_names[1,], levels = unique(double_row_column_names[1,]))))
first_row <-
unique(double_row_column_names[1,])
first_row_formatted <-
paste0(heading_command, "{", first_row, "}")
top_row <- character(length(first_row))
# Could do paste0() directly but decided that it would
# avoid the point which is to add \multicolumn only to the rows that call for it.
for (ii in seq_along(first_row)){
if (first_row[ii] %in% top_headers){
top_row[ii] <- paste0("\\multicolumn{", top_headers_widths$Freq[ii], "}{c}{", first_row_formatted[ii], "}")
}
}
rm(ii)
for_latex_top_row <-
paste0(paste0(top_row, collapse = " & "), "\\\\")
if (booktabs){
# (lr) to avoid cmidrule touching adjacent groups
between_row <- paste0("\\cmidrule(lr){", position_of_header_instance, "-", position_of_header_final, "}")
} else {
between_row <- paste0("\\cline{", position_of_header_instance, "-", position_of_header_final, "}")
}
for_latex_between_row <-
paste0(paste0(between_row, collapse = ""))
for_latex_second_row <-
paste0(heading_command, "{", double_row_column_names[2,], "}")
for_latex_second_row <-
paste0(paste0(for_latex_second_row, collapse = " & "), "\\\\")
addtorow <- list()
addtorow$pos <- list(0, 0, 0)
addtorow$command <-
paste0(paste0(c(for_latex_top_row, for_latex_between_row, for_latex_second_row)), "\n")
xtable::print.xtable(xtable::xtable(.data, align = xtable.align),
type = "latex",
add.to.row = addtorow,
include.colnames = FALSE,
include.rownames = FALSE,
booktabs = booktabs,
...)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With