blob: 1b4e1feebd4db42e1a5a52224cdcbbb2cf8a02d6 [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#'
26#' @export
Hao Zhu2ce42b92017-06-15 17:15:33 -040027add_header_left <- function(kable_input, header = NULL, header_name = "",
Hao Zhu32f43f72017-06-20 18:24:54 -040028 align = "c", width = NULL, bold = F, italic = F,
Hao Zhubf5bfe22017-06-21 14:37:41 -040029 ...) {
Hao Zhu96a50b52017-06-14 18:09:35 -040030 if (is.null(header)) return(kable_input)
31 kable_format <- attr(kable_input, "format")
32 if (!kable_format %in% c("html", "latex")) {
33 stop("Please specify output format in your kable function. Currently ",
34 "generic markdown table using pandoc is not supported.")
35 }
36 if (kable_format == "html") {
Hao Zhu32f43f72017-06-20 18:24:54 -040037 return(add_header_left_html(kable_input, header, header_name, align,
38 width, bold, italic))
Hao Zhu96a50b52017-06-14 18:09:35 -040039 }
40 if (kable_format == "latex") {
Hao Zhu32f43f72017-06-20 18:24:54 -040041 return(add_header_left_latex(kable_input, header, header_name, align,
Hao Zhubf5bfe22017-06-21 14:37:41 -040042 width, bold, italic, ...))
Hao Zhu96a50b52017-06-14 18:09:35 -040043 }
44}
45
46# HTML
Hao Zhu32f43f72017-06-20 18:24:54 -040047add_header_left_html <- function(kable_input, header, header_name, align,
48 width, bold, italic) {
Hao Zhu96a50b52017-06-14 18:09:35 -040049 kable_attrs <- attributes(kable_input)
50 kable_xml <- read_xml(as.character(kable_input), options = "COMPACT")
51 kable_thead <- xml_tpart(kable_xml, "thead")
52 kable_tbody <- xml_tpart(kable_xml, "tbody")
53
Hao Zhu32f43f72017-06-20 18:24:54 -040054 align <- match.arg(align, c("c", "l", "r"))
Hao Zhu2ce42b92017-06-15 17:15:33 -040055 align <- switch(align, "c" = "center", "l" = "left", "r" = "right")
56
Hao Zhu32f43f72017-06-20 18:24:54 -040057 column_style <- paste0(
58 ifelse(!is.null(width), paste0("width: ", width, "; "), ""),
59 ifelse(bold, "font-weight: bold; ", ""),
60 ifelse(italic, "font-style: italic; ", "")
61 )
62
Hao Zhu96a50b52017-06-14 18:09:35 -040063 new_header <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -040064 '<th style="text-align:', align, '; vertical-align: bottom;', column_style,
65 '" rowspan="', length(xml_children(kable_thead)), '">', header_name, '</th>'
Hao Zhu96a50b52017-06-14 18:09:35 -040066 )
67 new_header <- read_xml(new_header, options = c("COMPACT"))
68 xml_add_child(xml_child(kable_thead, 1), new_header, .where = 0)
69
Hao Zhu2ce42b92017-06-15 17:15:33 -040070 header <- standardize_header(header, length(xml_children(kable_tbody)))
Hao Zhu96a50b52017-06-14 18:09:35 -040071 for (i in 1:nrow(header)) {
72 new_row_item <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -040073 '<td style="text-align:', align, '; vertical-align: middle;',
74 column_style, '" rowspan="',
Hao Zhu96a50b52017-06-14 18:09:35 -040075 header$rowspan[i], '">', header$header[i], '</td>')
76 new_row_item <- read_xml(new_row_item, options = "COMPACT")
77 target_row <- xml_child(kable_tbody, header$row[i])
78 xml_add_child(target_row, new_row_item, .where = 0)
79 }
80
81 out <- structure(as.character(kable_xml), format = "html",
82 class = "knitr_kable")
Hao Zhu32f43f72017-06-20 18:24:54 -040083
84 # Adjust for column_spec
85 if (is.null(kable_attrs$column_adjust)) {
86 table_nrow <- length(xml_children(kable_tbody))
87 # if (!is.null(kable_attrs$group_header_rows)) {
88 # table_nrow <- table_nrow - length(kable_attrs$group_header_rows)
89 # }
90 table_ncol <- length(xml_children(
91 xml_child(kable_thead, length(xml_children(kable_thead)))
92 ))
93 kable_attrs$column_adjust$matrix <- matrix(
94 rep(TRUE, table_nrow * table_ncol), ncol = table_nrow)
95 kable_attrs$column_adjust$count <- 1
96 new_row_index <- rep(FALSE, table_nrow)
97 } else {
98 new_row_index <- rep(FALSE, ncol(kable_attrs$column_adjust$matrix))
99 kable_attrs$column_adjust$count <- 1 + kable_attrs$column_adjust$count
100 }
101 new_row_index[header$row] <- TRUE
102 kable_attrs$column_adjust$matrix <- rbind(
103 new_row_index, kable_attrs$column_adjust$matrix
104 )
Hao Zhu96a50b52017-06-14 18:09:35 -0400105 attributes(out) <- kable_attrs
Hao Zhu32f43f72017-06-20 18:24:54 -0400106
Hao Zhu96a50b52017-06-14 18:09:35 -0400107 return(out)
108}
109
Hao Zhu2ce42b92017-06-15 17:15:33 -0400110standardize_header <- function(header, n_row) {
Hao Zhu96a50b52017-06-14 18:09:35 -0400111 header_names <- names(header)
112
113 if (is.null(header_names)) {
114 return(data.frame(header = header, row = 1:length(header),
115 rowspan = 1, row.names = NULL))
116 }
117
118 names(header)[header_names == ""] <- header[header_names == ""]
119 header[header_names == ""] <- 1
120 header_names <- names(header)
121 header <- as.numeric(header)
122 names(header) <- header_names
123 if (sum(header) < n_row) {
124 header <- c(header, " " = n_row - sum(header))
125 }
126 row_pos <- c(1, cumsum(header)[-length(header)] + 1)
127 return(data.frame(
128 header = names(header),
129 row = row_pos, rowspan = header, row.names = NULL
130 ))
131}
132
Hao Zhu520d91b2017-06-16 01:41:26 -0400133add_header_left_latex <- function(kable_input, header, header_name, align,
Hao Zhu32f43f72017-06-20 18:24:54 -0400134 width, bold, italic, full_midline = F) {
Hao Zhu2ce42b92017-06-15 17:15:33 -0400135 table_info <- magic_mirror(kable_input)
136 usepackage_latex("multirow")
137 if (!table_info$booktabs) {
138 warning("add_header_left only supports LaTeX table with booktabs. Please",
139 " use kable(..., booktabs = T) in your kable function.")
140 }
141 out <- as.character(kable_input)
142 contents <- table_info$contents
143 header_name <- escape_latex(header_name)
144 header <- standardize_header(header, length(contents) - 1)
145 header$header <- escape_latex(header$header)
146 header$header <- gsub("\\\\", "\\\\\\\\", header$header)
147 header$row <- header$row + 1
148 header$row_end <- header$row + header$rowspan - 1
149
150 # Align
Hao Zhu32f43f72017-06-20 18:24:54 -0400151 align_row <- latex_column_align_builder(align, width, bold, italic)
152
Hao Zhu2ce42b92017-06-15 17:15:33 -0400153 out <- sub(paste0(table_info$begin_tabular, "\\{"),
Hao Zhu32f43f72017-06-20 18:24:54 -0400154 paste0(table_info$begin_tabular, "{", align_row,
Hao Zhu2ce42b92017-06-15 17:15:33 -0400155 ifelse(table_info$booktabs, "", "|")),
156 out, perl = T)
Hao Zhu32f43f72017-06-20 18:24:54 -0400157 # table_info$align_vector <- c(align, table_info$align_vector)
Hao Zhu2ce42b92017-06-15 17:15:33 -0400158
159 # Header
Hao Zhu32f43f72017-06-20 18:24:54 -0400160 ## Extra header rows introduced by add_header_above
Hao Zhu2ce42b92017-06-15 17:15:33 -0400161 if (!is.null(table_info$new_header_row)) {
162 new_header_row <- table_info$new_header_row
163 for (i in 1:length(new_header_row)) {
164 out <- sub(regex_escape(new_header_row[i]),
165 paste0(" & ", new_header_row[i]), out)
166 cline_old <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
167 cline_old <- regex_escape(cline_old)
168 table_info$header_df[[i]] <- rbind(
169 data.frame(header = " ", colspan = 1),
170 table_info$header_df[[i]]
171 )
172 cline_new <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
173 out <- sub(cline_old, cline_new, out)
174 }
175 }
Hao Zhu32f43f72017-06-20 18:24:54 -0400176 ## Base Header row
Hao Zhu2ce42b92017-06-15 17:15:33 -0400177 out <- sub(contents[1], paste0(header_name, " & ", contents[1]), out)
178 table_info$contents[1] <- paste0(header_name, " & ", contents[1])
179
180 # move existing midrules if exists
181 out_lines <- read_lines(out)
182 tbody_start_row <- which(out_lines == "\\midrule")
183 tbody_end_row <- which(out_lines == "\\bottomrule")
184 before_tbody <- out_lines[seq(1, tbody_start_row)]
185 tbody <- out_lines[seq(tbody_start_row + 1, tbody_end_row - 1)]
186 after_tbody <- out_lines[seq(tbody_end_row, length(out_lines))]
187
188 # Remove addlinespace in this case
189 tbody <- tbody[tbody != "\\addlinespace"]
190
191 midrule_exist <- str_sub(tbody, 1, 9) == "\\cmidrule"
192 if (sum(midrule_exist) > 0) {
193 existing_midrules <- which(midrule_exist)
194 tbody[existing_midrules] <- unlist(lapply(
195 tbody[existing_midrules], cmidrule_plus_one
196 ))
197 out <- paste0(c(before_tbody, tbody, after_tbody), collapse = "\n")
198 }
199
200 for (j in 1:nrow(header)) {
201 new_row_pre <- paste0(
Hao Zhu32f43f72017-06-20 18:24:54 -0400202 "\\\\multirow\\{", -header$rowspan[j], "\\}\\{",
203 ifelse(is.null(width), "\\*", width),
204 "\\}\\{",
205 switch(align,
Hao Zhubf5bfe22017-06-21 14:37:41 -0400206 "l" = "\\\\raggedright\\\\arraybackslash ",
207 "c" = "\\\\centering\\\\arraybackslash ",
208 "r" = "\\\\raggedleft\\\\arraybackslash "),
Hao Zhu32f43f72017-06-20 18:24:54 -0400209 header$header[j], "\\} & "
Hao Zhu2ce42b92017-06-15 17:15:33 -0400210 )
211 new_row_text <- paste0(new_row_pre, contents[header$row_end[j]])
212 out <- sub(contents[header$row_end[j]], new_row_text, out)
213 table_info$contents[header$row_end[j]] <- new_row_text
214 if (j != nrow(header)) {
215 out <- sub(
216 paste0(contents[header$row_end[j]], "\\\\\\\\\n"),
217 paste0(contents[header$row_end[j]],
Hao Zhu520d91b2017-06-16 01:41:26 -0400218 "\\\\\\\\\n\\\\cmidrule[0.5pt](l{2pt}r{2pt}){1-",
219 ifelse(full_midline, str_count(tbody[1], " & ") + 2, 1),
220 "}\n"),
Hao Zhu2ce42b92017-06-15 17:15:33 -0400221 out
222 )
223 }
224 }
225
226 for (k in setdiff(seq(2, length(contents)), header$row_end)) {
227 out <- sub(contents[k],
228 paste0(" & ", contents[k]),
229 out)
230 table_info$contents[k] <- paste0(" & ", contents[k])
231 }
232
233 out <- structure(out, format = "latex", class = "knitr_kable")
Hao Zhu32f43f72017-06-20 18:24:54 -0400234
235 attr(out, "kable_meta") <- table_info
Hao Zhu2ce42b92017-06-15 17:15:33 -0400236 return(out)
237}
238
239cmidrule_plus_one <- function(x) {
240 start_pos <- as.numeric(str_match(x, "\\)\\{(.*)-")[2]) + 1
241 stop_pos <- as.numeric(str_match(x, "-(.*)\\}")[2]) + 1
242 return(
243 paste0("\\cmidrule[0.5pt](l{2pt}r{2pt}){", start_pos, "-", stop_pos, "}")
244 )
Hao Zhu96a50b52017-06-14 18:09:35 -0400245}