1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
|
#' @title Wrap a flextable for use with patchwork
#'
#' @description
#' This function wraps a flextable as a patchwork-compliant patch, similar
#' to what [patchwork::wrap_table()] does for gt tables. It allows flextable
#' objects to be combined with ggplot2 plots in a patchwork layout, with
#' optional alignment of table headers and body with plot panel areas.
#'
#' Note this is experimental and may change in the future.
#' @param x A flextable object.
#' @param panel What portion of the table should be aligned with the panel
#' region? `"body"` means that header and footer will be placed outside the
#' panel region. `"full"` means that the whole table will be placed inside the
#' panel region. `"rows"` keeps all rows inside the panel but is otherwise
#' equivalent to `"body"`. `"cols"` places all columns within the panel region
#' but keeps column headers on top.
#' @param space How should the dimension of the table influence the final
#' composition? `"fixed"` means that the table width and height will set the
#' dimensions of the area it occupies. `"free"` means the table dimensions will
#' not influence the sizing. `"free_x"` and `"free_y"` allow freeing either
#' direction.
#' @param n_row_headers Number of leading columns to treat as row headers.
#' These columns will be placed outside the panel region and will not
#' participate in alignment with the plot axes.
#' @param flex_body If `TRUE`, the table body row heights become flexible:
#' the adjacent ggplot determines the panel height and the body rows
#' stretch equally to fill it. Header and footer keep their fixed size.
#' This is useful to align table rows with discrete bars or categories
#' in a neighbouring plot. Implies `free_y` for `space`.
#' @param flex_cols If `TRUE`, the data column widths (all columns after
#' `n_row_headers`) become flexible: the adjacent ggplot determines the
#' panel width and the data columns stretch to fill it. Row header
#' columns keep their fixed width. This is useful to align table columns
#' with discrete x-axis categories in a neighbouring plot.
#' Implies `free_x` for `space`.
#' @param expand Expansion value matching the ggplot discrete axis expansion
#' (`ggplot2::expansion(add = expand)`). Default is `0.6`, which is the
#' ggplot2 default for discrete axes. Only used when `flex_cols = TRUE`.
#' @param just Horizontal alignment of the table within its patchwork panel.
#' One of `"left"` (default), `"right"`, or `"center"`. Useful when the
#' table is narrower than the available panel width.
#' Ignored when `flex_cols = TRUE` (columns fill the panel).
#' @return A patchwork-compliant object that can be combined with ggplot2 plots
#' using `+`, `|`, or `/` operators.
#' @export
#' @examplesIf requireNamespace("patchwork", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("ragg", quietly = TRUE)
#' library(gdtools)
#' font_set_liberation()
#' library(ggplot2)
#' library(patchwork)
#'
#' set_flextable_defaults(
#' font.family = "Liberation Sans",
#' font.size = 10,
#' big.mark = "",
#' border.color = "grey60"
#' )
#'
#' # Adapted from <https://r-graph-gallery.com/web-dumbell-chart.html>
#'
#' dataset <- data.frame(
#' team = c(
#' "FC Bayern Munchen", "SV Werder Bremen", "Borussia Dortmund",
#' "VfB Stuttgart", "Borussia M'gladbach", "Hamburger SV",
#' "Eintracht Frankfurt", "FC Schalke 04", "1. FC Koln",
#' "Bayer 04 Leverkusen"
#' ),
#' matches = c(2000, 1992, 1924, 1924, 1898, 1866, 1856, 1832, 1754, 1524),
#' won = c(1206, 818, 881, 782, 763, 746, 683, 700, 674, 669),
#' lost = c( 363, 676, 563, 673, 636, 625, 693, 669, 628, 447)
#' )
#' dataset$win_pct <- dataset$won / dataset$matches * 100
#' dataset$loss_pct <- dataset$lost / dataset$matches * 100
#' dataset$team <- factor(dataset$team, levels = rev(dataset$team))
#'
#' # -- dumbbell chart --
#' pal <- c(lost = "#EFAC00", won = "#28A87D")
#' df_long <- reshape(dataset, direction = "long",
#' varying = list(c("loss_pct", "win_pct")),
#' v.names = "pct", timevar = "type",
#' times = c("lost", "won"), idvar = "team"
#' )
#'
#' p <- ggplot(df_long, aes(x = pct / 100, y = team)) +
#' stat_summary(
#' geom = "linerange", fun.min = "min", fun.max = "max",
#' linewidth = .7, color = "grey60"
#' ) +
#' geom_point(aes(fill = type), size = 4, shape = 21,
#' stroke = .8, color = "white"
#' ) +
#' scale_x_continuous(
#' labels = scales::percent,
#' expand = expansion(add = c(.02, .02))
#' ) +
#' scale_y_discrete(name = NULL, guide = "none") +
#' scale_fill_manual(
#' values = pal,
#' labels = c(lost = "Lost", won = "Won")
#' ) +
#' labs(x = NULL, fill = NULL) +
#' theme_minimal(base_family = "Liberation Sans", base_size = 10) +
#' theme(
#' legend.position = "top",
#' legend.justification = "left",
#' panel.grid.minor = element_blank(),
#' panel.grid.major.y = element_blank()
#' )
#'
#' # -- flextable --
#' ft_dat <- dataset[, c("matches", "win_pct", "loss_pct", "team")]
#' ft_dat$team <- as.character(ft_dat$team)
#'
#' ft <- flextable(ft_dat)
#' ft <- border_remove(ft)
#' ft <- bold(ft, part = "header")
#' ft <- colformat_double(ft, j = c("win_pct", "loss_pct"),
#' digits = 1, suffix = "%"
#' )
#' ft <- set_header_labels(ft,
#' team = "Team", matches = "GP",
#' win_pct = "", loss_pct = ""
#' )
#' ft <- color(ft, color = "#28A87D", j = 2)
#' ft <- color(ft, color = "#EFAC00", j = 3)
#' ft <- bold(ft, bold = TRUE, j = 2:3)
#' ft <- italic(ft, italic = TRUE, j = 4)
#' ft <- align(ft, align = "right", part = "all")
#' ft <- autofit(ft)
#'
#' \dontshow{
#' cap <- ragg::agg_capture(width = 7, height = 6, units = "in", res = 150)
#' grDevices::dev.control("enable")
#' }
#' print(
#' wrap_flextable(ft, flex_body = TRUE, just = "right") +
#' p + plot_layout(widths = c(1.1, 2))
#' )
#' \dontshow{
#' raster <- cap()
#' dev.off()
#' plot(as.raster(raster))
#' init_flextable_defaults()
#' }
#' @family flextable print function
wrap_flextable <- function(
x,
panel = c("body", "full", "rows", "cols"),
space = c("free", "free_x", "free_y", "fixed"),
n_row_headers = 0L,
flex_body = FALSE,
flex_cols = FALSE,
expand = 0.6,
just = c("left", "right", "center")
) {
if (!requireNamespace("patchwork", quietly = TRUE)) {
stop("Package 'patchwork' is required for wrap_flextable().", call. = FALSE)
}
if (!requireNamespace("gtable", quietly = TRUE)) {
stop("Package 'gtable' is required for wrap_flextable().", call. = FALSE)
}
panel <- match.arg(panel)
space <- match.arg(space)
just <- match.arg(just)
flex_body <- isTRUE(flex_body)
flex_cols <- isTRUE(flex_cols)
if (flex_body) {
attr(x, ".patchwork_flex_body") <- TRUE
}
if (flex_cols) {
attr(x, ".patchwork_flex_cols") <- TRUE
attr(x, ".patchwork_n_row_headers") <- as.integer(n_row_headers)
attr(x, ".patchwork_flex_cols_expand") <- expand
}
attr(x, ".patchwork_just") <- just
wrapped <- patchwork::wrap_elements(panel = x, ignore_tag = FALSE)
attr(wrapped, "patch_settings")$panel <- panel
attr(wrapped, "patch_settings")$n_row_headers <- as.integer(n_row_headers)
attr(wrapped, "patch_settings")$space <- c(
if (flex_cols) TRUE else space %in% c("free", "free_x"),
if (flex_body) TRUE else space %in% c("free", "free_y")
)
class(wrapped) <- c("wrapped_table", class(wrapped))
wrapped
}
# S3 method: ggplot_add.flextable -------------------------------------------
# Enables: ggplot_obj + flextable_obj
ggplot_add.flextable <- function(object, plot, object_name) {
plot + wrap_flextable(object)
}
# S3 method: as_patch.flextable ---------------------------------------------
# Called by patchwork when it needs to convert a flextable to a grob.
# Returns a gtable with named components so that patchwork's
# patchGrob.wrapped_table() can split header/body/footer.
#' @importFrom grid unit viewport grobWidth grobHeight
as_patch.flextable <- function(x, ...) {
if (!requireNamespace("gtable", quietly = TRUE)) {
stop("Package 'gtable' is required.", call. = FALSE)
}
flex_body <- isTRUE(attr(x, ".patchwork_flex_body"))
flex_cols <- isTRUE(attr(x, ".patchwork_flex_cols"))
n_row_hdrs <- as.integer(attr(x, ".patchwork_n_row_headers") %||% 0L)
n_header <- nrow_part(x, "header")
n_body <- nrow_part(x, "body")
n_footer <- nrow_part(x, "footer")
n_rows <- n_header + n_body + n_footer
if (flex_body || flex_cols) {
grob <- gen_grob(x, fit = "auto", scaling = "fixed")
} else {
grob <- gen_grob(x, fit = "fixed", scaling = "fixed")
}
widths <- grob$ftpar$widths
heights <- grob$ftpar$heights
n_cols <- length(widths)
# --- row heights ---
if (flex_body && n_body > 0) {
grob$ftpar$flex_body <- TRUE
grob$ftpar$n_header_rows <- n_header
grob$ftpar$n_body_rows <- n_body
grob$ftpar$n_footer_rows <- n_footer
row_heights <- unit(heights, "in")
body_seq <- seq.int(n_header + 1L, n_header + n_body)
row_heights[body_seq] <- unit(rep(1, n_body), "null")
} else {
row_heights <- unit(heights, "in")
}
# --- column widths ---
n_data_cols <- n_cols - n_row_hdrs
if (flex_cols && n_data_cols > 0) {
grob$ftpar$flex_cols <- TRUE
grob$ftpar$n_row_header_cols <- n_row_hdrs
grob$ftpar$n_data_cols <- n_data_cols
col_widths <- unit(widths, "in")
data_seq <- seq.int(n_row_hdrs + 1L, n_cols)
col_widths[data_seq] <- unit(rep(1, n_data_cols), "null")
} else {
col_widths <- unit(widths, "in")
}
# Create the gtable skeleton
gt <- gtable::gtable(
widths = col_widths,
heights = row_heights
)
# Add the flextable grob spanning the entire table
gt <- gtable::gtable_add_grob(
gt,
grobs = list(grob),
t = 1L,
l = 1L,
b = n_rows,
r = n_cols,
clip = "off",
name = "table"
)
# Add a zero-size grob for "table_body" so patchwork can find boundaries
body_top <- n_header + 1L
body_bottom <- n_header + n_body
if (n_body > 0) {
gt <- gtable::gtable_add_grob(
gt,
grobs = list(grid::nullGrob()),
t = body_top,
l = 1L,
b = body_bottom,
r = n_cols,
clip = "off",
name = "table_body"
)
}
# --- viewport height ---
if (flex_body && n_header > 0) {
header_height <- sum(unit(heights[seq_len(n_header)], "in"))
vp_height <- unit(1, "npc") + header_height
} else if (flex_body) {
vp_height <- unit(1, "npc")
} else {
vp_height <- sum(row_heights)
}
# --- viewport width and horizontal position ---
if (flex_cols && n_data_cols > 0) {
expand_val <- as.numeric(
attr(x, ".patchwork_flex_cols_expand") %||% 0.6
)
# clamp: below 0.5, viewport would overflow the panel
expand_eff <- max(expand_val, 0.5)
range_units <- n_data_cols - 1 + 2 * expand_eff
# data columns span n_dc/range of the panel; margins are empty
cat_fraction <- n_data_cols / range_units
vp_x <- unit((expand_eff - 0.5) / range_units, "npc")
if (n_row_hdrs > 0) {
row_hdr_width <- sum(unit(widths[seq_len(n_row_hdrs)], "in"))
vp_width <- unit(cat_fraction, "npc") + row_hdr_width
} else {
vp_width <- unit(cat_fraction, "npc")
}
vp_just_x <- 0
} else {
halign <- attr(x, ".patchwork_just") %||% "left"
if (halign == "right") {
vp_x <- unit(1, "npc")
vp_just_x <- 1
} else if (halign == "center") {
vp_x <- unit(0.5, "npc")
vp_just_x <- 0.5
} else {
vp_x <- unit(0, "npc")
vp_just_x <- 0
}
vp_width <- sum(col_widths)
}
gt$vp <- viewport(
x = vp_x,
y = 1,
width = vp_width,
height = vp_height,
default.units = "npc",
just = c(vp_just_x, 1)
)
gt
}
# .onLoad registration ------------------------------------------------------
# Register S3 methods for generics from other packages (ggplot2 and patchwork)
# so that flextable objects are handled natively.
.onLoad_patchwork <- function() {
if (requireNamespace("ggplot2", quietly = TRUE)) {
registerS3method(
"ggplot_add",
"flextable",
ggplot_add.flextable,
envir = asNamespace("ggplot2")
)
}
if (requireNamespace("patchwork", quietly = TRUE)) {
registerS3method(
"as_patch",
"flextable",
as_patch.flextable,
envir = asNamespace("patchwork")
)
}
}
|