File: patchwork.R

package info (click to toggle)
r-cran-flextable 0.9.11-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,296 kB
  • sloc: javascript: 28; sh: 15; makefile: 2
file content (360 lines) | stat: -rw-r--r-- 12,411 bytes parent folder | download
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")
    )
  }
}