blob: 25012afc4f8f7f58e8491adff016bc059dc295b8 [file] [log] [blame]
Hao Zhu700da5c2017-07-03 11:53:52 -04001#' Add a heading column to the left side of the table
2#'
3#' @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
17#' @param width A character string for the width of the new column. Values
18#' could be "10cm", "3in" or "30em", etc..
19#' @param align Column alignment. you can choose from "c", "l" or "r"
20#' @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.
22#' @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.
25#'
26#' @examples x <- knitr::kable(head(mtcars), "html")
27#' add_header_left(x, c("A" = 2, "B" = 2, "C" = 2))
28add_header_left <- function(kable_input, header = NULL, header_name = "",
29 align = "c", width = NULL, bold = F, italic = F,
30 ...) {
31 if (is.null(header)) return(kable_input)
32 kable_format <- attr(kable_input, "format")
33 if (!kable_format %in% c("html", "latex")) {
34 stop("Please specify output format in your kable function. Currently ",
35 "generic markdown table using pandoc is not supported.")
36 }
37 if (kable_format == "html") {
38 return(add_header_left_html(kable_input, header, header_name, align,
39 width, bold, italic))
40 }
41 if (kable_format == "latex") {
42 return(add_header_left_latex(kable_input, header, header_name, align,
43 width, bold, italic, ...))
44 }
45}
46
47# HTML
48add_header_left_html <- function(kable_input, header, header_name, align,
49 width, bold, italic) {
50 kable_attrs <- attributes(kable_input)
51 kable_xml <- read_xml(as.character(kable_input), options = "COMPACT")
52 kable_thead <- xml_tpart(kable_xml, "thead")
53 kable_tbody <- xml_tpart(kable_xml, "tbody")
54
55 align <- match.arg(align, c("c", "l", "r"))
56 align <- switch(align, "c" = "center", "l" = "left", "r" = "right")
57
58 column_style <- paste0(
59 ifelse(!is.null(width), paste0("width: ", width, "; "), ""),
60 ifelse(bold, "font-weight: bold; ", ""),
61 ifelse(italic, "font-style: italic; ", "")
62 )
63
64 new_header <- paste0(
65 '<th style="text-align:', align, '; vertical-align: bottom;', column_style,
66 '" rowspan="', length(xml_children(kable_thead)), '">', header_name, '</th>'
67 )
68 new_header <- read_xml(new_header, options = c("COMPACT"))
69 xml_add_child(xml_child(kable_thead, 1), new_header, .where = 0)
70
71 header <- standardize_header(header, length(xml_children(kable_tbody)))
72 for (i in 1:nrow(header)) {
73 new_row_item <- paste0(
74 '<td style="text-align:', align, '; vertical-align: middle;',
75 column_style, '" rowspan="',
76 header$rowspan[i], '">', header$header[i], '</td>')
77 new_row_item <- read_xml(new_row_item, options = "COMPACT")
78 target_row <- xml_child(kable_tbody, header$row[i])
79 xml_add_child(target_row, new_row_item, .where = 0)
80 }
81
82 out <- structure(as.character(kable_xml), format = "html",
83 class = "knitr_kable")
84
85 # Adjust for column_spec
86 if (is.null(kable_attrs$column_adjust)) {
87 table_nrow <- length(xml_children(kable_tbody))
88 # if (!is.null(kable_attrs$group_header_rows)) {
89 # table_nrow <- table_nrow - length(kable_attrs$group_header_rows)
90 # }
91 table_ncol <- length(xml_children(
92 xml_child(kable_thead, length(xml_children(kable_thead)))
93 ))
94 kable_attrs$column_adjust$matrix <- matrix(
95 rep(TRUE, table_nrow * table_ncol), ncol = table_nrow)
96 kable_attrs$column_adjust$count <- 1
97 new_row_index <- rep(FALSE, table_nrow)
98 } else {
99 new_row_index <- rep(FALSE, ncol(kable_attrs$column_adjust$matrix))
100 kable_attrs$column_adjust$count <- 1 + kable_attrs$column_adjust$count
101 }
102 new_row_index[header$row] <- TRUE
103 kable_attrs$column_adjust$matrix <- rbind(
104 new_row_index, kable_attrs$column_adjust$matrix
105 )
106 attributes(out) <- kable_attrs
107
108 return(out)
109}
110
111standardize_header <- function(header, n_row) {
112 header_names <- names(header)
113
114 if (is.null(header_names)) {
115 return(data.frame(header = header, row = 1:length(header),
116 rowspan = 1, row.names = NULL))
117 }
118
119 names(header)[header_names == ""] <- header[header_names == ""]
120 header[header_names == ""] <- 1
121 header_names <- names(header)
122 header <- as.numeric(header)
123 names(header) <- header_names
124 if (sum(header) < n_row) {
125 header <- c(header, " " = n_row - sum(header))
126 }
127 row_pos <- c(1, cumsum(header)[-length(header)] + 1)
128 return(data.frame(
129 header = names(header),
130 row = row_pos, rowspan = header, row.names = NULL
131 ))
132}
133
134add_header_left_latex <- function(kable_input, header, header_name, align,
135 width, bold, italic, full_midline = F) {
136 table_info <- magic_mirror(kable_input)
137 usepackage_latex("multirow")
138 if (!table_info$booktabs) {
139 warning("add_header_left only supports LaTeX table with booktabs. Please",
140 " use kable(..., booktabs = T) in your kable function.")
141 }
142 out <- as.character(kable_input)
143 contents <- table_info$contents
144 header_name <- escape_latex(header_name)
145 header <- standardize_header(header, length(contents) - 1)
146 header$header <- escape_latex(header$header)
147 header$header <- gsub("\\\\", "\\\\\\\\", header$header)
148 header$row <- header$row + 1
149 header$row_end <- header$row + header$rowspan - 1
150
151 # Align
152 align_row <- latex_column_align_builder(align, width, bold, italic)
153
154 out <- sub(paste0(table_info$begin_tabular, "\\{"),
155 paste0(table_info$begin_tabular, "{", align_row,
156 ifelse(table_info$booktabs, "", "|")),
157 out, perl = T)
158 # table_info$align_vector <- c(align, table_info$align_vector)
159
160 # Header
161 ## Extra header rows introduced by add_header_above
162 if (!is.null(table_info$new_header_row)) {
163 new_header_row <- table_info$new_header_row
164 for (i in 1:length(new_header_row)) {
165 out <- sub(regex_escape(new_header_row[i]),
166 paste0(" & ", new_header_row[i]), out)
167 cline_old <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
168 cline_old <- regex_escape(cline_old)
169 table_info$header_df[[i]] <- rbind(
170 data.frame(header = " ", colspan = 1),
171 table_info$header_df[[i]]
172 )
173 cline_new <- cline_gen(table_info$header_df[[i]], table_info$booktabs)
174 out <- sub(cline_old, cline_new, out)
175 }
176 }
177 ## Base Header row
178 out <- sub(contents[1], paste0(header_name, " & ", contents[1]), out)
179 table_info$contents[1] <- paste0(header_name, " & ", contents[1])
180
181 # move existing midrules if exists
182 out_lines <- read_lines(out)
183 tbody_start_row <- which(out_lines == "\\midrule")
184 tbody_end_row <- which(out_lines == "\\bottomrule")
185 before_tbody <- out_lines[seq(1, tbody_start_row)]
186 tbody <- out_lines[seq(tbody_start_row + 1, tbody_end_row - 1)]
187 after_tbody <- out_lines[seq(tbody_end_row, length(out_lines))]
188
189 # Remove addlinespace in this case
190 tbody <- tbody[tbody != "\\addlinespace"]
191
192 midrule_exist <- str_sub(tbody, 1, 9) == "\\cmidrule"
193 if (sum(midrule_exist) > 0) {
194 existing_midrules <- which(midrule_exist)
195 tbody[existing_midrules] <- unlist(lapply(
196 tbody[existing_midrules], cmidrule_plus_one
197 ))
198 out <- paste0(c(before_tbody, tbody, after_tbody), collapse = "\n")
199 }
200
201 for (j in 1:nrow(header)) {
202 new_row_pre <- paste0(
203 "\\\\multirow\\{", -header$rowspan[j], "\\}\\{",
204 ifelse(is.null(width), "\\*", width),
205 "\\}\\{",
206 switch(align,
207 "l" = "\\\\raggedright\\\\arraybackslash ",
208 "c" = "\\\\centering\\\\arraybackslash ",
209 "r" = "\\\\raggedleft\\\\arraybackslash "),
210 header$header[j], "\\} & "
211 )
212 new_row_text <- paste0(new_row_pre, contents[header$row_end[j]])
213 out <- sub(contents[header$row_end[j]], new_row_text, out)
214 table_info$contents[header$row_end[j]] <- new_row_text
215 if (j != nrow(header)) {
216 out <- sub(
217 paste0(contents[header$row_end[j]], "\\\\\\\\\n"),
218 paste0(contents[header$row_end[j]],
219 "\\\\\\\\\n\\\\cmidrule[0.5pt](l{2pt}r{2pt}){1-",
220 ifelse(full_midline, str_count(tbody[1], " & ") + 2, 1),
221 "}\n"),
222 out
223 )
224 }
225 }
226
227 for (k in setdiff(seq(2, length(contents)), header$row_end)) {
228 out <- sub(contents[k],
229 paste0(" & ", contents[k]),
230 out)
231 table_info$contents[k] <- paste0(" & ", contents[k])
232 }
233
234 out <- structure(out, format = "latex", class = "knitr_kable")
235
236 attr(out, "kable_meta") <- table_info
237 return(out)
238}
239
240cmidrule_plus_one <- function(x) {
241 start_pos <- as.numeric(str_match(x, "\\)\\{(.*)-")[2]) + 1
242 stop_pos <- as.numeric(str_match(x, "-(.*)\\}")[2]) + 1
243 return(
244 paste0("\\cmidrule[0.5pt](l{2pt}r{2pt}){", start_pos, "-", stop_pos, "}")
245 )
246}