Skip to content

Commit

Permalink
Even better support for tables
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasp85 committed Sep 6, 2024
1 parent a2a1b81 commit 3dbd7cd
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 18 deletions.
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ S3method(patchGrob,guide_area)
S3method(patchGrob,patch)
S3method(patchGrob,table_patch)
S3method(patchGrob,wrapped_patch)
S3method(patchGrob,wrapped_table)
S3method(plot,inset_patch)
S3method(plot,patch)
S3method(plot,patch_area)
Expand Down Expand Up @@ -86,6 +87,7 @@ export(set_dim)
export(wrap_elements)
export(wrap_ggplot_grob)
export(wrap_plots)
export(wrap_table)
import(cli)
import(rlang)
importFrom(farver,get_channel)
Expand Down Expand Up @@ -142,6 +144,7 @@ importFrom(grid,seekViewport)
importFrom(grid,unit)
importFrom(grid,unit.c)
importFrom(grid,unit.pmax)
importFrom(grid,unitType)
importFrom(grid,upViewport)
importFrom(grid,valid.just)
importFrom(grid,viewport)
Expand Down
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Plot backgrounds are now always placed beneath all other elements in the
patchwork (#370)
* Axis titles can now reliably be collected even with faceted plots (#367)
* Native support for gt objects
* Native support for gt objects, either adding them directly or controlling
their layout with `wrap_table()`
* Empty patches no longer breaks up axis title collection (#375)
* `wrap_ggplot_grob()` now respects auto-tagging (#363)
* Fix a bug where guide collecting would prevent proper axes collecting (#359)
Expand Down
7 changes: 5 additions & 2 deletions R/add_plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ggplot_add.ggplot <- function(object, plot, object_name) {
#' @importFrom ggplot2 ggplot_add
#' @export
ggplot_add.grob <- function(object, plot, object_name) {
table <- as_patch(object)
plot + wrap_elements(full = object)
}
#' @importFrom ggplot2 ggplot_add
Expand All @@ -20,12 +21,14 @@ ggplot_add.raster <- ggplot_add.grob
ggplot_add.nativeRaster <- ggplot_add.grob
#' @importFrom ggplot2 ggplot_add
#' @export
ggplot_add.gt_tbl <- ggplot_add.grob
ggplot_add.gt_tbl <- function(object, plot, object_name) {
plot + wrap_table(object)
}

#' @importFrom grid is.grob
#' @importFrom grDevices is.raster
should_autowrap <- function(x) {
is.grob(x) || inherits(x, 'formula') || is.raster(x) || inherits(x, 'nativeRaster') || inherits(x, 'gt_tbl')
is.grob(x) || inherits(x, 'formula') || is.raster(x) || inherits(x, 'nativeRaster')
}

# Convert a plot with a (possible) list of patches into a self-contained
Expand Down
19 changes: 19 additions & 0 deletions R/plot_patchwork.R
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ find_strip_pos <- function(gt) {
}
'inside'
}
#' @importFrom grid unitType
set_panel_dimensions <- function(gt, panels, widths, heights, fixed_asp, design) {
width_ind <- seq(PANEL_COL, by = TABLE_COLS, length.out = length(widths))
height_ind <- seq(PANEL_ROW, by = TABLE_ROWS, length.out = length(heights))
Expand All @@ -1022,6 +1023,24 @@ set_panel_dimensions <- function(gt, panels, widths, heights, fixed_asp, design)
heights <- unit(heights, 'null')
}
height_strings <- as.character(heights)

panel_widths <- do.call(unit.c, lapply(panels, function(x) x$widths[PANEL_COL]))
absolute_col <- unitType(panel_widths) == "points"
if (any(absolute_col)) {
pos <- ifelse(absolute_col & design$l == design$r & width_strings[design$l] == "-1null", design$l, NA)
fixed_widths <- lapply(split(panel_widths, pos), "sum")
widths[as.numeric(names(fixed_widths))] <- do.call(unit.c, fixed_widths)
width_strings <- as.character(widths)
}
panel_heights <- do.call(unit.c, lapply(panels, function(x) x$heights[PANEL_ROW]))
absolute_row <- unitType(panel_heights) == "points"
if (any(absolute_row)) {
pos <- ifelse(absolute_row & design$t == design$b & height_strings[design$t] == "-1null", design$t, NA)
fixed_heights <- lapply(split(panel_heights, pos), "sum")
heights[as.numeric(names(fixed_heights))] <- do.call(unit.c, fixed_heights)
height_strings <- as.character(heights)
}

if (any(width_strings == '-1null') && any(height_strings == '-1null')) {
respect <- matrix(0, nrow = length(gt$heights), ncol = length(gt$widths))
fixed_areas <- lapply(which(fixed_asp), function(i) {
Expand Down
15 changes: 0 additions & 15 deletions R/wrap_elements.R
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,6 @@ as_patch.raster <- function(x, ...) {
}
#' @export
as_patch.nativeRaster <- as_patch.raster
#' @export
#' @importFrom grid viewport grobWidth grobHeight grobTree
as_patch.gt_tbl <- function(x, ...) {
check_installed("gt", version = "0.11.0")
grob <- gt::as_gtable(x)
grob$vp <- viewport(
x = 0,
y = 1,
width = grobWidth(grob),
height = grobHeight(grob),
default.units = "npc",
just = c(0, 1)
)
grob
}

#' @importFrom ggplot2 ggplotGrob
get_grob <- function(x, name) {
Expand Down
139 changes: 139 additions & 0 deletions R/wrap_table.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#' Wrap a table in a patchwork compliant patch
#'
#' This function works much like [wrap_elements()] in that it turns the input
#' into patchwork compliant objects that can be added to a composition. However,
#' `wrap_table()` uses the knowledge that the input is a table to provide some
#' very nifty layout options that makes it generally better to use than
#' [wrap_elements()] for this type of object.
#'
#' @param table A gt table or an object coercible to a data frame
#' @param panel what portion of the table should be aligned with the panel
#' region? `"body"` means that any column and row headers will be placed outside
#' the panel region, i.e. the topleft corner of the panel region will be aligned
#' with the topleft data cell. `"full"` means that the whole table will be
#' placed inside the panel region. `"rows"` means that all rows (including column
#' headers) will be placed inside the panel region but row headers will be
#' placed to the left. `"cols"` is the opposite, placing all columns within the
#' panel region but keeping the column header on top of it.
#' @param space How should the dimension of the table influence the final
#' composition? `"fixed"` means that the table width will set the width of the
#' column it occupies and the table height will set the height of the row it
#' occupies. `"free"` is the opposite meaning that the table dimension will not
#' have any influence on the sizing. `"free_x"` and `"free_y"` allows you to
#' free either direction while keeping the remaining fixed. Do note that if you
#' set a specific width or height in [plot_layout()] it will have higher
#' priority than the table dimensions
#' @inheritParams wrap_elements
#'
#' @return A wrapped_table object
#'
#' @export
#'
#' @note This functionality requires v0.11.0 or higher of the gt package
#'
#' @examplesIf requireNamespace("gt", quietly = TRUE) && packageVersion("gt") >= "0.11.0"
#' library(ggplot2)
#' library(gt)
#'
#' p1 <- ggplot(airquality) +
#' geom_line(aes(x = Day, y = Temp, colour = month.name[Month])) +
#' labs(colour = "Month")
#'
#' table <- data.frame(
#' Month = month.name[5:9],
#' "Mean temp." = tapply(airquality$Temp, airquality$Month, mean),
#' "Min temp." = tapply(airquality$Temp, airquality$Month, min),
#' "Max temp." = tapply(airquality$Temp, airquality$Month, max)
#' )
#' gt_tab <- gt(table, rowname_col = "Month")
#'
#' # Default addition usees wrap_table
#' p1 + gt_tab
#'
#' # Default places column and row headers outside panel area. Use wrap_table
#' # to control this
#' p1 + wrap_table(gt_tab, panel = "full")
#'
#' # Tables generally have fixed dimensions and these can be used to control
#' # the size of the area they occupy
#' p2 <- ggplot(airquality) +
#' geom_boxplot(aes(y = month.name[Month], x = Temp)) +
#' scale_y_discrete(name = NULL, limits = month.name[5:9], guide = "none")
#'
#' wrap_table(gt_tab, space = "fixed") + p2
#'
wrap_table <- function(table, panel = c("body", "full", "rows", "cols"), space = c("free", "free_x", "free_y", "fixed"), ignore_tag = FALSE) {
check_installed("gt", version = "0.11.0")
if (!inherits(table, "gt_tbl")) {
table <- try_fetch(
gt::gt(as.data.frame(table)),
error = function(cnd, ...) cli::cli_abort("Unable to convert input table to {.cls gt_tbl}", parent = cnd)
)
}
n_row_headers <- (!all(is.na(table[["_stub_df"]]$row_id))) + (!all(is.na(table[["_stub_df"]]$group_id)))
if (n_row_headers == 2 && !table[["_options"]]$value[[which(table[["_options"]]$parameter == "row_group_as_column")]]) {
n_row_headers <- 1
}
table <- wrap_elements(table, ignore_tag = ignore_tag)
attr(table, "patch_settings")$panel <- arg_match(panel)
attr(table, "patch_settings")$n_row_headers <- n_row_headers
attr(table, "patch_settings")$space <- c(space %in% c("free", "free_x"), space %in% c("free", "free_y"))
class(table) <- c("wrapped_table", class(table))
table
}

#' @export
patchGrob.wrapped_table <- function(x, guides = 'auto') {
panel <- attr(x, "patch_settings")$panel
row_head <- attr(x, "patch_settings")$n_row_headers
space <- attr(x, "patch_settings")$space

x <- NextMethod()

table_loc <- which(x$layout$name == "panel")
table_width <- x$grobs[[table_loc]]$widths
table_height <- x$grobs[[table_loc]]$heights

if (panel %in% c("body", "rows")) {
col_head <- x$grobs[[table_loc]]$layout$t[x$grobs[[table_loc]]$layout$name == "table_body"] - 1
if (col_head > 0) {
height <- sum(x$grobs[[table_loc]]$heights[1:col_head])
x$grobs[[table_loc]]$vp$y <- x$grobs[[table_loc]]$vp$y + height
x$heights[PANEL_ROW - 2] <- height

table_height <- table_height[-(1:col_head)]
}
}
if (panel %in% c("body", "cols") && row_head > 0) {
width <- sum(x$grobs[[table_loc]]$widths[1:row_head])
x$grobs[[table_loc]]$vp$x <- x$grobs[[table_loc]]$vp$x - width
x$widths[PANEL_COL - 2] <- width

table_width <- table_width[-(1:row_head)]
}
if (!space[1]) {
x$widths[PANEL_COL] <- if (inherits(table_width, "simpleUnit")) sum(table_width) else Reduce(`+`, table_width)
}
if (!space[2]) {
x$heights[PANEL_ROW] <- if (inherits(table_height, "simpleUnit")) sum(table_height) else Reduce(`+`, table_height)
}
x
}

#' @export
#' @importFrom grid viewport grobWidth grobHeight grobTree
as_patch.gt_tbl <- function(x, ...) {
check_installed("gt", version = "0.11.0")
grob <- gt::as_gtable(x)
loc <- grob$layout[grob$layout$name == "table",]
grob <- grob[loc$t:loc$b, loc$l:loc$r]
grob$vp <- viewport(
x = 0,
y = 1,
width = grobWidth(grob),
height = grobHeight(grob),
default.units = "npc",
just = c(0, 1)
)
grob
}
84 changes: 84 additions & 0 deletions man/wrap_table.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3dbd7cd

Please sign in to comment.