blob: 64498790e06baf7f100d1ba9e446bdee6747a1f2 [file] [log] [blame]
Hao Zhu4f10f652017-06-16 14:03:44 -04001#' Add a heading column to the left side of the table
Hao Zhu96a50b52017-06-14 18:09:35 -04002#'
Hao Zhu4f10f652017-06-16 14:03:44 -04003#' @description This function uses the same syntax as add_header_above. It will
4#' add a heading column with grouped rows to the left side of the table. It can
5#' act as an alternative way to `group_rows` to show grouped information. As
6#' `add_header_above`, users can use this function to add multiple layers of
7#' heading columns one by one.
8#'
9#' @param kable_input Output of `knitr::kable()` with `format` specified
10#' @param header A (named) character vector with `rowspan` as values. For
11#' example, `c("xxx" = 1, "title" = 2)` can be used to create a new header column
12#' for a 3-row table with "xxx" spanning row 1 and "title" spanning row 2 & 3 (
13#' two rows). For convenience, when `rowspan` equals to 1, users can drop the
14#' ` = 1` part. As a result, `c("xxx", "title" = 2)` is the same as
15#' `c("xxx" = 1, "title" = 2)`.
16#' @param header_name Column name that that extra column
Hao Zhu32f43f72017-06-20 18:24:54 -040017#' @param width A character string for the width of the new column. Values
18#' could be "10cm", "3in" or "30em", etc..
Hao Zhu4f10f652017-06-16 14:03:44 -040019#' @param align Column alignment. you can choose from "c", "l" or "r"
Hao Zhu32f43f72017-06-20 18:24:54 -040020#' @param bold A T/F value to control whether the text should be bolded.
21#' @param italic A T/F value to control whether the text should to be emphasized.
Hao Zhubf5bfe22017-06-21 14:37:41 -040022#' @param ... Extra options to be passed into HTML or LaTeX. Right now there is
23#' only one for LaTeX. Option full_midline is a TRUE/FALSE option to control
24#' if the mid line needs to be extended to the end of row.
Hao Zhu96a50b52017-06-14 18:09:35 -040025#'
Hao Zhu5a7689e2017-06-26 15:37:24 -100026#' @examples x <- knitr::kable(head(mtcars), "html")
27#' add_header_left(x, c("A" = 2, "B" = 2, "C" = 2))
28#'
Hao Zhu96a50b52017-06-14 18:09:35 -040029#' @export
Hao Zhu2ce42b92017-06-15 17:15:33 -040030add_header_left <- function(kable_input, header = NULL, header_name = "",
Hao Zhu32f43f72017-06-20 18:24:54 -040031 align = "c", width = NULL, bold = F, italic = F,
Hao Zhubf5bfe22017-06-21 14:37:41 -040032 ...) {
Hao Zhu96a50b52017-06-14 18:09:35 -040033 if (is.null(header)) return(kable_input)
34 kable_format <- attr(kable_input, "format")
35 if (!kable_format %in% c("html", "latex")) {
36 stop("Please specify output format in your kable function. Currently ",
37 "generic markdown table using pandoc is not supported.")
38 }
39 if (kable_format == "html") {
Hao Zhu32f43f72017-06-20 18:24:54 -040040 return(add_header_left_html(kable_input, header, header_name, align,
41 width, bold, italic))
Hao Zhu96a50b52017-06-14 18:09:35 -040042 }
43 if (kable_format == "latex") {
Hao Zhu32f43f72017-06-20 18:24:54 -040044 return(add_header_left_latex(kable_input, header, header_name, align,
Hao Zhubf5bfe22017-06-21 14:37:41 -040045 width, bold, italic, ...))
Hao Zhu96a50b52017-06-14 18:09:35 -040046 }
47}
48
49# HTML
Hao Zhu32f43f72017-06-20 18:24:54 -040050add_header_left_html <- function(kable_input, header, header_name, align,
51 width, bold, italic) {
Hao Zhu96a50b52017-06-14 18:09:35 -040052 kable_attrs <- attributes(kable_input)
53 kable_xml <- read_xml(as.character(kable_input), options = "COMPACT")
54 kable_thead <- xml_tpart(kable_xml, "thead")
55 kable_tbody <- xml_tpart(kable_xml, "tbody")
56
Hao Zhu32f43f72017-06-20 18:24:54 -040057 align <- match.arg(align, c("c", "l", "r"))
Hao Zhu2ce42b92017-06-15 17:15:33 -040058 align <- switch(align, "c" = "center", "l" = "left", "r" = "right")
59
Hao Zhu32f43f72017-06-20 18:24:54 -040060 column_style <- paste0(
61 ifelse(!is.null(width), paste0("width: ", width, "; "), ""),
62 ifelse(bold, "font-weight: bold; ", ""),
63 ifelse(italic, "font-style: italic; ", "")
64 )
65
Hao Zhu96a50b52017-06-14 18:09:35 -040066 new_header <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -040067 '<th style="text-align:', align, '; vertical-align: bottom;', column_style,
68 '" rowspan="', length(xml_children(kable_thead)), '">', header_name, '</th>'
Hao Zhu96a50b52017-06-14 18:09:35 -040069 )
70 new_header <- read_xml(new_header, options = c("COMPACT"))
71 xml_add_child(xml_child(kable_thead, 1), new_header, .where = 0)
72
Hao Zhu2ce42b92017-06-15 17:15:33 -040073 header <- standardize_header(header, length(xml_children(kable_tbody)))
Hao Zhu96a50b52017-06-14 18:09:35 -040074 for (i in 1:nrow(header)) {
75 new_row_item <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -040076 '<td style="text-align:', align, '; vertical-align: middle;',
77 column_style, '" rowspan="',
Hao Zhu96a50b52017-06-14 18:09:35 -040078 header$rowspan[i], '">', header$header[i], '</td>')
79 new_row_item <- read_xml(new_row_item, options = "COMPACT")
80 target_row <- xml_child(kable_tbody, header$row[i])
81 xml_add_child(target_row, new_row_item, .where = 0)
82 }
83
84 out <- structure(as.character(kable_xml), format = "html",
85 class = "knitr_kable")
Hao Zhu32f43f72017-06-20 18:24:54 -040086
87 # Adjust for column_spec
88 if (is.null(kable_attrs$column_adjust)) {
89 table_nrow <- length(xml_children(kable_tbody))
90 # if (!is.null(kable_attrs$group_header_rows)) {
91 # table_nrow <- table_nrow - length(kable_attrs$group_header_rows)
92 # }
93 table_ncol <- length(xml_children(
94 xml_child(kable_thead, length(xml_children(kable_thead)))
95 ))
96 kable_attrs$column_adjust$matrix <- matrix(
97 rep(TRUE, table_nrow * table_ncol), ncol = table_nrow)
98 kable_attrs$column_adjust$count <- 1
99 new_row_index <- rep(FALSE, table_nrow)
100 } else {
101 new_row_index <- rep(FALSE, ncol(kable_attrs$column_adjust$matrix))
102 kable_attrs$column_adjust$count <- 1 + kable_attrs$column_adjust$count
103 }
104 new_row_index[header$row] <- TRUE
105 kable_attrs$column_adjust$matrix <- rbind(
106 new_row_index, kable_attrs$column_adjust$matrix
107 )
Hao Zhu96a50b52017-06-14 18:09:35 -0400108 attributes(out) <- kable_attrs
Hao Zhu32f43f72017-06-20 18:24:54 -0400109
Hao Zhu96a50b52017-06-14 18:09:35 -0400110 return(out)
111}
112
Hao Zhu2ce42b92017-06-15 17:15:33 -0400113standardize_header <- function(header, n_row) {
Hao Zhu96a50b52017-06-14 18:09:35 -0400114 header_names <- names(header)
115
116 if (is.null(header_names)) {
117 return(data.frame(header = header, row = 1:length(header),
118 rowspan = 1, row.names = NULL))
119 }
120
121 names(header)[header_names == ""] <- header[header_names == ""]
122 header[header_names == ""] <- 1
123 header_names <- names(header)
124 header <- as.numeric(header)
125 names(header) <- header_names
126 if (sum(header) < n_row) {
127 header <- c(header, " " = n_row - sum(header))
128 }
129 row_pos <- c(1, cumsum(header)[-length(header)] + 1)
130 return(data.frame(
131 header = names(header),
132 row = row_pos, rowspan = header, row.names = NULL
133 ))
134}
135
Hao Zhu520d91b2017-06-16 01:41:26 -0400136add_header_left_latex <- function(kable_input, header, header_name, align,
Hao Zhu32f43f72017-06-20 18:24:54 -0400137 width, bold, italic, full_midline = F) {
Hao Zhu2ce42b92017-06-15 17:15:33 -0400138 table_info <- magic_mirror(kable_input)
139 usepackage_latex("multirow")
140 if (!table_info$booktabs) {
141 warning("add_header_left only supports LaTeX table with booktabs. Please",
142 " use kable(..., booktabs = T) in your kable function.")
143 }
144 out <- as.character(kable_input)
145 contents <- table_info$contents
146 header_name <- escape_latex(header_name)
147 header <- standardize_header(header, length(contents) - 1)
148 header$header <- escape_latex(header$header)
149 header$header <- gsub("\\\\", "\\\\\\\\", header$header)
150 header$row <- header$row + 1
151 header$row_end <- header$row + header$rowspan - 1
152
153 # Align
Hao Zhu32f43f72017-06-20 18:24:54 -0400154 align_row <- latex_column_align_builder(align, width, bold, italic)
155
Hao Zhu2ce42b92017-06-15 17:15:33 -0400156 out <- sub(paste0(table_info$begin_tabular, "\\{"),
Hao Zhu32f43f72017-06-20 18:24:54 -0400157 paste0(table_info$begin_tabular, "{", align_row,
Hao Zhu2ce42b92017-06-15 17:15:33 -0400158 ifelse(table_info$booktabs, "", "|")),
159 out, perl = T)
Hao Zhu32f43f72017-06-20 18:24:54 -0400160 # table_info$align_vector <- c(align, table_info$align_vector)
Hao Zhu2ce42b92017-06-15 17:15:33 -0400161
162 # Header
Hao Zhu32f43f72017-06-20 18:24:54 -0400163 ## Extra header rows introduced by add_header_above
Hao Zhu2ce42b92017-06-15 17:15:33 -0400164 if (!is.null(table_info$new_header_row)) {
165 new_header_row <- table_info$new_header_row
166 for (i in 1:length(new_header_row)) {
167 out <- sub(regex_escape(new_header_row[i]),
168 paste0(" & ", new_header_row[i]), out)
169 cline_old <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
170 cline_old <- regex_escape(cline_old)
171 table_info$header_df[[i]] <- rbind(
172 data.frame(header = " ", colspan = 1),
173 table_info$header_df[[i]]
174 )
175 cline_new <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
176 out <- sub(cline_old, cline_new, out)
177 }
178 }
Hao Zhu32f43f72017-06-20 18:24:54 -0400179 ## Base Header row
Hao Zhu2ce42b92017-06-15 17:15:33 -0400180 out <- sub(contents[1], paste0(header_name, " & ", contents[1]), out)
181 table_info$contents[1] <- paste0(header_name, " & ", contents[1])
182
183 # move existing midrules if exists
184 out_lines <- read_lines(out)
185 tbody_start_row <- which(out_lines == "\\midrule")
186 tbody_end_row <- which(out_lines == "\\bottomrule")
187 before_tbody <- out_lines[seq(1, tbody_start_row)]
188 tbody <- out_lines[seq(tbody_start_row + 1, tbody_end_row - 1)]
189 after_tbody <- out_lines[seq(tbody_end_row, length(out_lines))]
190
191 # Remove addlinespace in this case
192 tbody <- tbody[tbody != "\\addlinespace"]
193
194 midrule_exist <- str_sub(tbody, 1, 9) == "\\cmidrule"
195 if (sum(midrule_exist) > 0) {
196 existing_midrules <- which(midrule_exist)
197 tbody[existing_midrules] <- unlist(lapply(
198 tbody[existing_midrules], cmidrule_plus_one
199 ))
200 out <- paste0(c(before_tbody, tbody, after_tbody), collapse = "\n")
201 }
202
203 for (j in 1:nrow(header)) {
204 new_row_pre <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -0400205 "\\\\multirow\\{", -header$rowspan[j], "\\}\\{",
206 ifelse(is.null(width), "\\*", width),
207 "\\}\\{",
208 switch(align,
Hao Zhubf5bfe22017-06-21 14:37:41 -0400209 "l" = "\\\\raggedright\\\\arraybackslash ",
210 "c" = "\\\\centering\\\\arraybackslash ",
211 "r" = "\\\\raggedleft\\\\arraybackslash "),
Hao Zhu32f43f72017-06-20 18:24:54 -0400212 header$header[j], "\\} & "
Hao Zhu2ce42b92017-06-15 17:15:33 -0400213 )
214 new_row_text <- paste0(new_row_pre, contents[header$row_end[j]])
215 out <- sub(contents[header$row_end[j]], new_row_text, out)
216 table_info$contents[header$row_end[j]] <- new_row_text
217 if (j != nrow(header)) {
218 out <- sub(
219 paste0(contents[header$row_end[j]], "\\\\\\\\\n"),
220 paste0(contents[header$row_end[j]],
Hao Zhu520d91b2017-06-16 01:41:26 -0400221 "\\\\\\\\\n\\\\cmidrule[0.5pt](l{2pt}r{2pt}){1-",
222 ifelse(full_midline, str_count(tbody[1], " & ") + 2, 1),
223 "}\n"),
Hao Zhu2ce42b92017-06-15 17:15:33 -0400224 out
225 )
226 }
227 }
228
229 for (k in setdiff(seq(2, length(contents)), header$row_end)) {
230 out <- sub(contents[k],
231 paste0(" & ", contents[k]),
232 out)
233 table_info$contents[k] <- paste0(" & ", contents[k])
234 }
235
236 out <- structure(out, format = "latex", class = "knitr_kable")
Hao Zhu32f43f72017-06-20 18:24:54 -0400237
238 attr(out, "kable_meta") <- table_info
Hao Zhu2ce42b92017-06-15 17:15:33 -0400239 return(out)
240}
241
242cmidrule_plus_one <- function(x) {
243 start_pos <- as.numeric(str_match(x, "\\)\\{(.*)-")[2]) + 1
244 stop_pos <- as.numeric(str_match(x, "-(.*)\\}")[2]) + 1
245 return(
246 paste0("\\cmidrule[0.5pt](l{2pt}r{2pt}){", start_pos, "-", stop_pos, "}")
247 )
Hao Zhu96a50b52017-06-14 18:09:35 -0400248}