diff --git a/DESCRIPTION b/DESCRIPTION index e109ca0..09b0670 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,6 +13,7 @@ Authors@R: c( person("Agota", "Bodoni", , "Agota.Bodoni@ucb.com", role = "ctb"), person("Eilis", "Meldrum-Dolan", , "Eilis.Meldrum-Dolan@ucb.com", role = "ctb"), person("Gary", "Cao", , "Gary.Cao@ucb.com", role = "ctb"), + person("Monika", "Beh", , "Monika.Beh@ucb.com", role = "ctb"), person("UCB S.A., Belgium", role = c("cph", "fnd")) ) Description: A simple and flexible tool designed to create enriched figures and tables by providing a way to add text @@ -24,7 +25,6 @@ URL: https://pharmaverse.github.io/gridify/ BugReports: https://github.com/pharmaverse/gridify/issues Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 Imports: grDevices, grid, @@ -41,7 +41,7 @@ Suggests: spelling, testthat (>= 3.0.0) Collate: - grid_utils.R + gridify-utils.R gridify-classes.R gridify-methods.R ansi_colour.R @@ -54,3 +54,4 @@ Collate: VignetteBuilder: knitr Config/testthat/edition: 3 Language: en-GB +Config/roxygen2/version: 8.0.0 diff --git a/NEWS.md b/NEWS.md index b90c988..a644531 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,18 @@ ## New features +* Added vertical anchoring for the gridify object inside its cell via the new + `vjust` slot of `gridifyObject()` and the `object_vjust` argument of + `simple_layout()`, `complex_layout()`, `pharma_layout_base()`, + `pharma_layout_A4()` and `pharma_layout_letter()`. + `0` aligns the object to the bottom, `0.5` (default) centers it, and `1` + anchors it to the top of the cell. Most useful + for fixed-size grobs such as `gt` and `flextable` tables. When `vjust != 0.5` + is used with a fixed-size grob the viewport is sized to `grid::grobHeight()` + and the `height` slot of `gridifyObject()` is ignored. For fixed-size tables, + edge values (`0` or `1`) place the table directly against the object-row edge; + add spacer rows in custom layouts or use inset values such as `0.05` or `0.95` + if nearby text appears too close. Reported and proposed by Monika Beh. * Added support for `fill_empty = NA` in the `paginate_table()` function. ## Bug fixes diff --git a/R/complex_layout.R b/R/complex_layout.R index 310948f..35cefab 100644 --- a/R/complex_layout.R +++ b/R/complex_layout.R @@ -5,20 +5,7 @@ #' notes and footnotes around the output. #' #' @name complex_layout -#' @param margin A unit object specifying the margins around the output. Default is 10% of the output area on all sides. -#' @param global_gpar A gpar object specifying the global graphical parameters. -#' Must be the result of a call to `grid::gpar()`. -#' @param background A string specifying the background fill colour. -#' Default `grid::get.gpar()$fill` for a white background. -#' @param scales A string, either `"free"` or `"fixed"`. -#' By default, `"fixed"` ensures that text elements (titles, footers, etc.) -#' retain a static height, preventing text overlap while maintaining a -#' structured layout. However, this may result in different height proportions -#' between the text elements and the output. -#' -#' The `"free"` option makes the row heights proportional, -#' allowing them to scale dynamically based on the overall output size. -#' This ensures that the text elements and the output maintain relative proportions. +#' @inheritParams simple_layout #' #' @details The layout consists of six rows for headers, titles, object (figure or table), notes, and footnotes. #' The object is placed in the fourth row.\cr @@ -81,7 +68,8 @@ complex_layout <- function( margin = grid::unit(c(t = 0.1, r = 0.1, b = 0.1, l = 0.1), units = "npc"), global_gpar = grid::gpar(), background = grid::get.gpar()$fill, - scales = c("fixed", "free") + scales = c("fixed", "free"), + object_vjust = 0.5 ) { scales <- match.arg(scales, c("fixed", "free")) @@ -103,7 +91,7 @@ complex_layout <- function( background = background, margin = margin, adjust_height = TRUE, - object = gridifyObject(row = 4, col = c(1, 3)), + object = gridifyObject(row = 4, col = c(1, 3), vjust = object_vjust), cells = gridifyCells( header_left = gridifyCell(row = 1, col = 1), header_middle = gridifyCell(row = 1, col = 2), diff --git a/R/grid_utils.R b/R/grid_utils.R deleted file mode 100644 index a5fb90d..0000000 --- a/R/grid_utils.R +++ /dev/null @@ -1,49 +0,0 @@ -#' Wrapper for `grid::unitType` which supports older R versions -#' @param x a grid::unit -#' @param use_grid means try to call grid::unitType if it exists. -#' The main purpose of this argument is to have full test coverage in tests. -#' Default TRUE. -#' @return a character vector with unit type for each element. -#' @keywords internal -grid_unit_type <- function(x, use_grid = TRUE) { - if (use_grid && is.function(base::asNamespace("grid")[["unitType"]])) { - utils::getFromNamespace("unitType", "grid")(x) - } else { - unit_val <- attr(x, "unit") - if (!is.null(unit_val)) { - rep(unit_val, length = length(x)) - } else { - stop("grid_unit_type x argument: Not a unit object") - } - } -} - -#' Get `grid::gpar` arguments -#' @param gpar a `grid::gpar` object. -#' @return a list. -#' @keywords internal -gpar_args <- function(gpar) { - args <- as.list(gpar) - fontface <- args[["fontface"]] - font <- if (isTRUE(is.na(args[["font"]]))) NULL else args[["font"]] - - # Remove the original font and fontface from args - args[["font"]] <- NULL - args[["fontface"]] <- NULL - - args[["fontface"]] <- if (!is.null(fontface)) fontface else font - - args -} - -#' Convert `grid::gpar` to a call -#' @param gpar a `grid::gpar` object. -#' @return a call. -#' @keywords internal -gpar_call <- function(gpar) { - if (length(gpar) == 0) { - return(as.call(c(quote(grid::gpar), list()))) - } - - as.call(c(quote(grid::gpar), gpar_args(gpar))) -} diff --git a/R/gridify-classes.R b/R/gridify-classes.R index 1a330c6..963d974 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -439,8 +439,21 @@ gridifyCells <- function(...) { #' #' @slot row A numeric value, span or sequence specifying the row position of the object. #' @slot col A numeric value, span or sequence specifying the column position of the object. -#' @slot height A numeric value specifying the height of the object. +#' @slot height A numeric value specifying the height of the object as a fraction of the row +#' (interpreted in `npc`). Ignored when `vjust != 0.5` and the grob is fixed-size: in that +#' case the viewport is sized to `grid::grobHeight()` so the object can be anchored within +#' a taller row. The 1-inch floor in applies in both cases. #' @slot width A numeric value specifying the width of the object. +#' @slot vjust A numeric value in `[0, 1]` specifying the vertical anchoring of the object +#' within its cell. `0` aligns to the bottom, `0.5` (default) centers it, and +#' `1` aligns to the top. +#' Anchoring only takes effect for fixed-size grobs (e.g. `gt::as_gtable()`, +#' `flextable::gen_grob()`, plain `grid::rectGrob()`). Flexible grobs whose natural height is +#' meant to fill the container (e.g. `ggplot2::ggplotGrob()`, recorded gTrees from +#' `grid::grid.grabExpr()`) always span the full row regardless of `vjust`. +#' For fixed-size table grobs, edge values (`0` or `1`) place the table directly +#' against the object-row edge; add spacer rows in a custom layout or use an +#' inset value such as `0.05` or `0.95` if nearby text appears too close. #' @exportClass gridifyObject setClass( "gridifyObject", @@ -448,19 +461,37 @@ setClass( row = "numeric", col = "numeric", height = "numeric", - width = "numeric" - ) + width = "numeric", + vjust = "numeric" + ), + prototype = list(vjust = 0.5) ) setValidity("gridifyObject", function(object) { + errs <- character() if (min(object@row) < 1 || !all(object@row %% 1 == 0)) { - stop("cell row has to be positive integer.") + errs <- c(errs, "cell row has to be positive integer.") } if (min(object@col) < 1 || !all(object@col %% 1 == 0)) { - stop("cell col has to be positive integer.") + errs <- c(errs, "cell col has to be positive integer.") + } + if ( + length(object@vjust) != 1L || + anyNA(object@vjust) || + !is.finite(object@vjust) || + object@vjust < 0 || + object@vjust > 1 + ) { + errs <- c( + errs, + "vjust has to be a single finite numeric value in [0, 1]." + ) + } + if (object@height > 1 ) { + errs <- c(errs, "height must be less than or equal to 1.") } - TRUE + if (length(errs)) errs else TRUE }) #' Create a gridifyObject @@ -469,8 +500,20 @@ setValidity("gridifyObject", function(object) { #' #' @param row A numeric value, span or sequence specifying the row position of the object. #' @param col A numeric value, span or sequence specifying the row position of the object. -#' @param height A numeric value specifying the height of the object. Default is 1. +#' @param height A numeric value specifying the height of the object as a fraction of the row +#' (interpreted in `npc`). Default is 1. Ignored when `vjust != 0.5` and the grob is +#' fixed-size: in that case the viewport is sized to `grid::grobHeight()` so the object +#' can be anchored within a taller row. #' @param width A numeric value specifying the width of the object. Default is 1. +#' @param vjust A numeric value in `[0, 1]` specifying the vertical anchoring of the object +#' within its cell. `0` aligns to the bottom, `0.5` (default) centers it, and +#' `1` aligns to the top. +#' Anchoring only takes effect for fixed-size grobs (e.g. `gt::as_gtable()`, +#' `flextable::gen_grob()`). Flexible grobs (e.g. `ggplot2::ggplotGrob()`) always fill the +#' full row regardless of `vjust`. +#' For fixed-size table grobs, edge values (`0` or `1`) place the table directly +#' against the object-row edge; add spacer rows in a custom layout or use an +#' inset value such as `0.05` or `0.95` if nearby text appears too close. #' #' @return An instance of the gridifyObject class. #' @@ -478,8 +521,15 @@ setValidity("gridifyObject", function(object) { #' @export #' @examples #' object <- gridifyObject(row = 1, col = 1, height = 1, width = 1) -gridifyObject <- function(row, col, height = 1, width = 1) { - new("gridifyObject", row = row, col = col, height = height, width = width) +gridifyObject <- function(row, col, height = 1, width = 1, vjust = 0.5) { + new( + "gridifyObject", + row = row, + col = col, + height = height, + width = width, + vjust = vjust + ) } #' gridifyClass class @@ -644,6 +694,11 @@ gridify <- function( object <- grid::grid.grabExpr( gridGraphics::grid.echo(function() eval(object_expr)) ) + # Mark the recorded gTree as flexible: its `grobHeight()` is meaningful + # only inside the recording viewport (collapses to 0 elsewhere), so the + # viewport must span the full row regardless of `vjust`. + # See is_flexible_grob() / object_viewport_height_expr(). + attr(object, "gridify.flexible") <- TRUE } else { stop("Please install gridGraphics to use it in gridify.") } diff --git a/R/gridify-methods.R b/R/gridify-methods.R index 69790ad..7aa23cd 100644 --- a/R/gridify-methods.R +++ b/R/gridify-methods.R @@ -254,16 +254,22 @@ setMethod( setMethod("print", "gridifyClass", function(x, ...) { grid::grid.newpage() + # See object_viewport_height_expr() for the rationale behind this choice. + height_expr <- object_viewport_height_expr( + grob = x@object, + vjust = x@layout@object@vjust, + height = x@layout@object@height + ) + pp_list <- list( substitute( grid::grobTree( grid::editGrob( OBJECT, vp = grid::viewport( - height = grid::unit.pmax( - grid::unit(height_value, "npc"), - grid::unit(1, "inch") - ), + y = grid::unit(vjust_value, "npc"), + just = c(0.5, vjust_value), + height = HEIGHT_EXPR, width = grid::unit.pmax( grid::unit(width_value, "npc"), grid::unit(1, "inch") @@ -276,10 +282,11 @@ setMethod("print", "gridifyClass", function(x, ...) { ) ), env = list( - height_value = x@layout@object@height, width_value = x@layout@object@width, nrow_value = x@layout@object@row, - ncol_value = x@layout@object@col + ncol_value = x@layout@object@col, + vjust_value = x@layout@object@vjust, + HEIGHT_EXPR = height_expr ) ) ) @@ -668,6 +675,7 @@ setMethod("show_spec", "gridifyLayout", function(object) { cat(sprintf(" Width: %s\n", object@object@width)) cat(sprintf(" Height: %s\n", object@object@height)) + cat(sprintf(" Vjust: %s\n", object@object@vjust)) cat("\nObject Row Heights:\n") rows_span <- object_row[1]:object_row[length(object_row)] diff --git a/R/gridify-utils.R b/R/gridify-utils.R new file mode 100644 index 0000000..ee17f91 --- /dev/null +++ b/R/gridify-utils.R @@ -0,0 +1,126 @@ +#' Wrapper for `grid::unitType` which supports older R versions +#' @param x a grid::unit +#' @param use_grid means try to call grid::unitType if it exists. +#' The main purpose of this argument is to have full test coverage in tests. +#' Default TRUE. +#' @return a character vector with unit type for each element. +#' @keywords internal +grid_unit_type <- function(x, use_grid = TRUE) { + if (use_grid && is.function(base::asNamespace("grid")[["unitType"]])) { + utils::getFromNamespace("unitType", "grid")(x) + } else { + unit_val <- attr(x, "unit") + if (!is.null(unit_val)) { + rep(unit_val, length = length(x)) + } else { + stop("grid_unit_type x argument: Not a unit object") + } + } +} + +#' Get `grid::gpar` arguments +#' @param gpar a `grid::gpar` object. +#' @return a list. +#' @keywords internal +gpar_args <- function(gpar) { + args <- as.list(gpar) + fontface <- args[["fontface"]] + font <- if (isTRUE(is.na(args[["font"]]))) NULL else args[["font"]] + + # Remove the original font and fontface from args + args[["font"]] <- NULL + args[["fontface"]] <- NULL + + args[["fontface"]] <- if (!is.null(fontface)) fontface else font + + args +} + +#' Convert `grid::gpar` to a call +#' @param gpar a `grid::gpar` object. +#' @return a call. +#' @keywords internal +gpar_call <- function(gpar) { + if (length(gpar) == 0) { + return(as.call(c(quote(grid::gpar), list()))) + } + + as.call(c(quote(grid::gpar), gpar_args(gpar))) +} + +#' Detect a "flexible" grob whose natural height is not meaningful +#' +#' A flexible grob is one designed to fill whatever container it is placed +#' in, so `grid::grobHeight()` cannot be used to size its viewport. +#' Detection layers, in order of precedence: +#' 1. an explicit `gridify.flexible` attribute on the grob (set by the +#' `gridify()` constructor for grobs produced via `grid::grid.grabExpr()` +#' on the `formula` input path); +#' 2. a `gtable` with at least one `null` unit in its `heights` +#' (e.g. `ggplot2::ggplotGrob()`). +#' +#' All other grobs (`gt::as_gtable()`, `flextable::gen_grob()`, plain +#' `grid::rectGrob()` / `grid::nullGrob()`, user gTrees, ...) are treated as +#' fixed-size. The previous heuristic of "any gTree carrying a `childrenvp`" +#' was dropped because `childrenvp` is set for many reasons unrelated to +#' container-filling (clip viewports, custom transforms, ...). +#' +#' @param grob a grob. +#' @return `TRUE` if `grob` is flexible, `FALSE` otherwise. +#' @keywords internal +is_flexible_grob <- function(grob) { + if (isTRUE(attr(grob, "gridify.flexible"))) { + return(TRUE) + } + if (inherits(grob, "gtable")) { + return(any(grid_unit_type(grob$heights) == "null")) + } + FALSE +} + + +#' Build the viewport-height expression for the object's grob +#' +#' Chooses between the grob's natural height (`grid::grobHeight()`) and a +#' layout-driven height in npc, then floors the result via `grid::unit.pmax()` +#' so the viewport never collapses to zero. +#' +#' `use_grob_height_for_object` evaluates to `TRUE` when the caller has opted +#' into vertical anchoring (`vjust != 0.5`) and the grob has a meaningful +#' natural height (i.e. is not flexible, see [is_flexible_grob()]). +#' The `vjust == 0.5` short-circuit preserves the historical "fill the row" +#' behaviour for users who did not opt in. +#' +#' The returned expression references an unbound symbol `OBJECT`; the +#' caller is responsible for evaluating it in an environment that binds +#' `OBJECT` to the grob. +#' +#' @param grob a grob; used to evaluate `use_grob_height_for_object`. +#' @param vjust numeric, the layout's object vjust. +#' @param height numeric, the layout's object height (in npc). Ignored on the +#' `grid::grobHeight()` branch (i.e. when `use_grob_height_for_object` +#' returns `TRUE`); used otherwise. +#' @param min_height a `grid::unit` floor applied via `grid::unit.pmax()`. +#' Default `grid::unit(1, "inch")`. +#' @return an unevaluated call producing a `grid::unit`. +#' @keywords internal +object_viewport_height_expr <- function(grob, + vjust, + height, + min_height = grid::unit(1, "inches")) { + min_height_call <- as.call(c( + quote(grid::unit), + list(as.numeric(min_height), grid_unit_type(min_height)) + )) + + use_grob_height_for_object <- vjust != 0.5 && !is_flexible_grob(grob) + natural_height <- if (use_grob_height_for_object) { + quote(grid::grobHeight(OBJECT)) + } else { + substitute(grid::unit(h, "npc"), list(h = height)) + } + substitute( + grid::unit.pmax(NH, MIN), + list(NH = natural_height, MIN = min_height_call) + ) +} diff --git a/R/pharma_layout.R b/R/pharma_layout.R index f15dc25..0e7f411 100644 --- a/R/pharma_layout.R +++ b/R/pharma_layout.R @@ -34,6 +34,7 @@ NULL #' @param background A string specifying the background fill colour. #' Default `grid::get.gpar()$fill` for a white background. #' @param adjust_height A logical value indicating whether to adjust the height of the layout. Default is `TRUE`. +#' @inheritParams simple_layout #' #' @return A `gridifyLayout` object that defines the general structure and parameters for a pharma layout. #' @@ -77,7 +78,8 @@ pharma_layout_base <- function( margin = grid::unit(c(t = 1, r = 1, b = 1, l = 1), units = "inches"), global_gpar = NULL, background = grid::get.gpar()$fill, - adjust_height = TRUE + adjust_height = TRUE, + object_vjust = 0.5 ) { default_gpar <- list(fontfamily = "serif", fontsize = 9, lineheight = 0.95) global_gpar <- if (!is.null(global_gpar)) { @@ -101,7 +103,7 @@ pharma_layout_base <- function( background = background, margin = margin, adjust_height = adjust_height, - object = gridifyObject(row = 10, col = c(1, 3)), + object = gridifyObject(row = 10, col = c(1, 3), vjust = object_vjust), cells = gridifyCells( header_left_1 = gridifyCell(row = 1, col = 1, x = 0, hjust = 0), header_left_2 = gridifyCell(row = 2, col = 1, x = 0, hjust = 0), @@ -146,6 +148,7 @@ pharma_layout_base <- function( #' which can be overwritten alongside other graphical parameters found by `grid::get.gpar()`. #' @param background A character string specifying the background fill colour. #' Default `grid::get.gpar()$fill` for a white background. +#' @inheritParams simple_layout #' @details #' The margins for the A4 layout are: #' * top = 1 inch @@ -192,12 +195,14 @@ pharma_layout_base <- function( #' @export pharma_layout_A4 <- function( global_gpar = NULL, - background = grid::get.gpar()$fill + background = grid::get.gpar()$fill, + object_vjust = 0.5 ) { pharma_layout_base( global_gpar = global_gpar, background = background, - margin = grid::unit(c(t = 1, r = 1.69, b = 1, l = 1), units = "inches") + margin = grid::unit(c(t = 1, r = 1.69, b = 1, l = 1), units = "inches"), + object_vjust = object_vjust ) } @@ -212,6 +217,7 @@ pharma_layout_A4 <- function( #' which can be overwritten alongside other graphical parameters found by `grid::get.gpar()`. #' @param background A character string specifying the background fill colour. #' Default `grid::get.gpar()$fill` for a white background. +#' @inheritParams simple_layout #' @details #' The margins for the letter layout are: #' * top = 1 inch @@ -258,11 +264,13 @@ pharma_layout_A4 <- function( #' @export pharma_layout_letter <- function( global_gpar = NULL, - background = grid::get.gpar()$fill + background = grid::get.gpar()$fill, + object_vjust = 0.5 ) { pharma_layout_base( global_gpar = global_gpar, background = background, - margin = grid::unit(c(t = 1, r = 1, b = 1.23, l = 1), units = "inches") + margin = grid::unit(c(t = 1, r = 1, b = 1.23, l = 1), units = "inches"), + object_vjust = object_vjust ) } diff --git a/R/simple_layout.R b/R/simple_layout.R index 48195cb..6b59794 100644 --- a/R/simple_layout.R +++ b/R/simple_layout.R @@ -17,6 +17,14 @@ #' The `"free"` option makes the row heights proportional, #' allowing them to scale dynamically based on the overall output size. #' This ensures that the text elements and the output maintain relative proportions. +#' @param object_vjust A numeric value in `[0, 1]` controlling the vertical anchoring of the +#' object within its row. `0` aligns to the bottom, `0.5` (default) centers it, +#' and `1` aligns to the top. Useful when the +#' object's row is taller than the object itself. Has no effect on flexible grobs +#' (e.g. `ggplot2::ggplotGrob()`), which always fill the full row. For fixed-size +#' table grobs such as `gt` and `flextable`, values at the edge (`0` or `1`) +#' place the table directly against the object-row edge. +#' Use an inset value such as `0.05` or `0.95` if nearby text appears too close. #' #' @details The layout consists of three rows, one each for the title, output, and footer.\cr #' The heights of the rows in simple_layout with `"free"` scales are 15%, 70% and 15% of the area respectively.\cr @@ -68,7 +76,8 @@ simple_layout <- function( margin = grid::unit(c(t = 0.1, r = 0.1, b = 0.1, l = 0.1), units = "npc"), global_gpar = grid::gpar(), background = grid::get.gpar()$fill, - scales = c("fixed", "free") + scales = c("fixed", "free"), + object_vjust = 0.5 ) { scales <- match.arg(scales, c("fixed", "free")) @@ -87,7 +96,7 @@ simple_layout <- function( global_gpar = global_gpar, background = background, adjust_height = TRUE, - object = gridifyObject(row = 2, col = 1), + object = gridifyObject(row = 2, col = 1, vjust = object_vjust), cells = gridifyCells( title = gridifyCell(row = 1, col = 1), footer = gridifyCell(row = 3, col = 1) diff --git a/inst/UML/UML_graph.md b/inst/UML/UML_graph.md index 6d9f141..eddc6b4 100644 --- a/inst/UML/UML_graph.md +++ b/inst/UML/UML_graph.md @@ -72,6 +72,7 @@ classDiagram +col: numeric +height: numeric +width: numeric + +vjust: numeric } class gridifyCells { cells: namedList[gridifyCell] diff --git a/inst/WORDLIST b/inst/WORDLIST index dcb22d7..6e2f2fb 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,11 +1,9 @@ Abdy Acknowledgments Bleier -centers CMD Codecov Dallimore -doi HersheySerif Laetitia Lemoine @@ -22,9 +20,15 @@ Qmd RStudio Rmd TransactionID +Vicencio +centered +centers +doi etc flextable fontfamily +gTree +gTrees ggplot gpar gridifyCell @@ -33,9 +37,11 @@ gridifyClass gridifyLayout gridifyObject gridifying +grob's gt gtable labeled +lifecycle npc pharmaverse px @@ -43,4 +49,4 @@ rd rtables sprintf unitType -Vicencio \ No newline at end of file +vjust diff --git a/man/complex_layout.Rd b/man/complex_layout.Rd index 8d109ae..17c0bbf 100644 --- a/man/complex_layout.Rd +++ b/man/complex_layout.Rd @@ -8,7 +8,8 @@ complex_layout( margin = grid::unit(c(t = 0.1, r = 0.1, b = 0.1, l = 0.1), units = "npc"), global_gpar = grid::gpar(), background = grid::get.gpar()$fill, - scales = c("fixed", "free") + scales = c("fixed", "free"), + object_vjust = 0.5 ) } \arguments{ @@ -29,6 +30,15 @@ between the text elements and the output. The \code{"free"} option makes the row heights proportional, allowing them to scale dynamically based on the overall output size. This ensures that the text elements and the output maintain relative proportions.} + +\item{object_vjust}{A numeric value in \verb{[0, 1]} controlling the vertical anchoring of the +object within its row. \code{0} aligns to the bottom, \code{0.5} (default) centers it, +and \code{1} aligns to the top. Useful when the +object's row is taller than the object itself. Has no effect on flexible grobs +(e.g. \code{ggplot2::ggplotGrob()}), which always fill the full row. For fixed-size +table grobs such as \code{gt} and \code{flextable}, values at the edge (\code{0} or \code{1}) +place the table directly against the object-row edge. +Use an inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ A gridifyLayout object. diff --git a/man/gpar_args.Rd b/man/gpar_args.Rd index 5299e4f..4dd8b1a 100644 --- a/man/gpar_args.Rd +++ b/man/gpar_args.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/grid_utils.R +% Please edit documentation in R/gridify-utils.R \name{gpar_args} \alias{gpar_args} \title{Get \code{grid::gpar} arguments} diff --git a/man/gpar_call.Rd b/man/gpar_call.Rd index a1ebaf0..e45d20b 100644 --- a/man/gpar_call.Rd +++ b/man/gpar_call.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/grid_utils.R +% Please edit documentation in R/gridify-utils.R \name{gpar_call} \alias{gpar_call} \title{Convert \code{grid::gpar} to a call} diff --git a/man/grid_unit_type.Rd b/man/grid_unit_type.Rd index e2b0924..0c7a070 100644 --- a/man/grid_unit_type.Rd +++ b/man/grid_unit_type.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/grid_utils.R +% Please edit documentation in R/gridify-utils.R \name{grid_unit_type} \alias{grid_unit_type} \title{Wrapper for \code{grid::unitType} which supports older R versions} diff --git a/man/gridifyObject-class.Rd b/man/gridifyObject-class.Rd index f3a035b..e662a39 100644 --- a/man/gridifyObject-class.Rd +++ b/man/gridifyObject-class.Rd @@ -14,8 +14,22 @@ Class for creating an object in a gridify layout. \item{\code{col}}{A numeric value, span or sequence specifying the column position of the object.} -\item{\code{height}}{A numeric value specifying the height of the object.} +\item{\code{height}}{A numeric value specifying the height of the object as a fraction of the row +(interpreted in \code{npc}). Ignored when \code{vjust != 0.5} and the grob is fixed-size: in that +case the viewport is sized to \code{grid::grobHeight()} so the object can be anchored within +a taller row. The 1-inch floor in applies in both cases.} \item{\code{width}}{A numeric value specifying the width of the object.} + +\item{\code{vjust}}{A numeric value in \verb{[0, 1]} specifying the vertical anchoring of the object +within its cell. \code{0} aligns to the bottom, \code{0.5} (default) centers it, and +\code{1} aligns to the top. +Anchoring only takes effect for fixed-size grobs (e.g. \code{gt::as_gtable()}, +\code{flextable::gen_grob()}, plain \code{grid::rectGrob()}). Flexible grobs whose natural height is +meant to fill the container (e.g. \code{ggplot2::ggplotGrob()}, recorded gTrees from +\code{grid::grid.grabExpr()}) always span the full row regardless of \code{vjust}. +For fixed-size table grobs, edge values (\code{0} or \code{1}) place the table directly +against the object-row edge; add spacer rows in a custom layout or use an +inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} }} diff --git a/man/gridifyObject.Rd b/man/gridifyObject.Rd index 72f0e28..97378ef 100644 --- a/man/gridifyObject.Rd +++ b/man/gridifyObject.Rd @@ -4,16 +4,29 @@ \alias{gridifyObject} \title{Create a gridifyObject} \usage{ -gridifyObject(row, col, height = 1, width = 1) +gridifyObject(row, col, height = 1, width = 1, vjust = 0.5) } \arguments{ \item{row}{A numeric value, span or sequence specifying the row position of the object.} \item{col}{A numeric value, span or sequence specifying the row position of the object.} -\item{height}{A numeric value specifying the height of the object. Default is 1.} +\item{height}{A numeric value specifying the height of the object as a fraction of the row +(interpreted in \code{npc}). Default is 1. Ignored when \code{vjust != 0.5} and the grob is +fixed-size: in that case the viewport is sized to \code{grid::grobHeight()} so the object +can be anchored within a taller row.} \item{width}{A numeric value specifying the width of the object. Default is 1.} + +\item{vjust}{A numeric value in \verb{[0, 1]} specifying the vertical anchoring of the object +within its cell. \code{0} aligns to the bottom, \code{0.5} (default) centers it, and +\code{1} aligns to the top. +Anchoring only takes effect for fixed-size grobs (e.g. \code{gt::as_gtable()}, +\code{flextable::gen_grob()}). Flexible grobs (e.g. \code{ggplot2::ggplotGrob()}) always fill the +full row regardless of \code{vjust}. +For fixed-size table grobs, edge values (\code{0} or \code{1}) place the table directly +against the object-row edge; add spacer rows in a custom layout or use an +inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ An instance of the gridifyObject class. diff --git a/man/is_flexible_grob.Rd b/man/is_flexible_grob.Rd new file mode 100644 index 0000000..15eccf8 --- /dev/null +++ b/man/is_flexible_grob.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gridify-utils.R +\name{is_flexible_grob} +\alias{is_flexible_grob} +\title{Detect a "flexible" grob whose natural height is not meaningful} +\usage{ +is_flexible_grob(grob) +} +\arguments{ +\item{grob}{a grob.} +} +\value{ +\code{TRUE} if \code{grob} is flexible, \code{FALSE} otherwise. +} +\description{ +A flexible grob is one designed to fill whatever container it is placed +in, so \code{grid::grobHeight()} cannot be used to size its viewport. +Detection layers, in order of precedence: +\enumerate{ +\item an explicit \code{gridify.flexible} attribute on the grob (set by the +\code{gridify()} constructor for grobs produced via \code{grid::grid.grabExpr()} +on the \code{formula} input path); +\item a \code{gtable} with at least one \code{null} unit in its \code{heights} +(e.g. \code{ggplot2::ggplotGrob()}). +} +} +\details{ +All other grobs (\code{gt::as_gtable()}, \code{flextable::gen_grob()}, plain +\code{grid::rectGrob()} / \code{grid::nullGrob()}, user gTrees, ...) are treated as +fixed-size. The previous heuristic of "any gTree carrying a \code{childrenvp}" +was dropped because \code{childrenvp} is set for many reasons unrelated to +container-filling (clip viewports, custom transforms, ...). +} +\keyword{internal} diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd new file mode 100644 index 0000000..5e83e4f --- /dev/null +++ b/man/object_viewport_height_expr.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gridify-utils.R +\name{object_viewport_height_expr} +\alias{object_viewport_height_expr} +\title{Build the viewport-height expression for the object's grob} +\usage{ +object_viewport_height_expr( + grob, + vjust, + height, + min_height = grid::unit(1, "inches") +) +} +\arguments{ +\item{grob}{a grob; used to evaluate \code{use_grob_height_for_object}.} + +\item{vjust}{numeric, the layout's object vjust.} + +\item{height}{numeric, the layout's object height (in npc). Ignored on the +\code{grid::grobHeight()} branch (i.e. when \code{use_grob_height_for_object} +returns \code{TRUE}); used otherwise.} + +\item{min_height}{a \code{grid::unit} floor applied via \code{grid::unit.pmax()}. +Default \code{grid::unit(1, "inch")}.} +} +\value{ +an unevaluated call producing a \code{grid::unit}. +} +\description{ +Chooses between the grob's natural height (\code{grid::grobHeight()}) and a +layout-driven height in npc, then floors the result via \code{grid::unit.pmax()} +so the viewport never collapses to zero. +} +\details{ +\code{use_grob_height_for_object} evaluates to \code{TRUE} when the caller has opted +into vertical anchoring (\code{vjust != 0.5}) and the grob has a meaningful +natural height (i.e. is not flexible, see \code{\link[=is_flexible_grob]{is_flexible_grob()}}). +The \code{vjust == 0.5} short-circuit preserves the historical "fill the row" +behaviour for users who did not opt in. + +The returned expression references an unbound symbol \code{OBJECT}; the +caller is responsible for evaluating it in an environment that binds +\code{OBJECT} to the grob. +} +\keyword{internal} diff --git a/man/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index 4a7cf50..7c9eabd 100644 --- a/man/pharma_layout_A4.Rd +++ b/man/pharma_layout_A4.Rd @@ -4,7 +4,11 @@ \alias{pharma_layout_A4} \title{Pharma Layout (A4) for a gridify object} \usage{ -pharma_layout_A4(global_gpar = NULL, background = grid::get.gpar()$fill) +pharma_layout_A4( + global_gpar = NULL, + background = grid::get.gpar()$fill, + object_vjust = 0.5 +) } \arguments{ \item{global_gpar}{A list specifying global graphical parameters to change in the layout. @@ -14,6 +18,15 @@ which can be overwritten alongside other graphical parameters found by \code{gri \item{background}{A character string specifying the background fill colour. Default \code{grid::get.gpar()$fill} for a white background.} + +\item{object_vjust}{A numeric value in \verb{[0, 1]} controlling the vertical anchoring of the +object within its row. \code{0} aligns to the bottom, \code{0.5} (default) centers it, +and \code{1} aligns to the top. Useful when the +object's row is taller than the object itself. Has no effect on flexible grobs +(e.g. \code{ggplot2::ggplotGrob()}), which always fill the full row. For fixed-size +table grobs such as \code{gt} and \code{flextable}, values at the edge (\code{0} or \code{1}) +place the table directly against the object-row edge. +Use an inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ A \code{gridifyLayout} object with the structure defined for A4 paper size. diff --git a/man/pharma_layout_base.Rd b/man/pharma_layout_base.Rd index 3eaea02..88bd38c 100644 --- a/man/pharma_layout_base.Rd +++ b/man/pharma_layout_base.Rd @@ -8,7 +8,8 @@ pharma_layout_base( margin = grid::unit(c(t = 1, r = 1, b = 1, l = 1), units = "inches"), global_gpar = NULL, background = grid::get.gpar()$fill, - adjust_height = TRUE + adjust_height = TRUE, + object_vjust = 0.5 ) } \arguments{ @@ -24,6 +25,15 @@ which can be overwritten alongside other graphical parameters found by \code{gri Default \code{grid::get.gpar()$fill} for a white background.} \item{adjust_height}{A logical value indicating whether to adjust the height of the layout. Default is \code{TRUE}.} + +\item{object_vjust}{A numeric value in \verb{[0, 1]} controlling the vertical anchoring of the +object within its row. \code{0} aligns to the bottom, \code{0.5} (default) centers it, +and \code{1} aligns to the top. Useful when the +object's row is taller than the object itself. Has no effect on flexible grobs +(e.g. \code{ggplot2::ggplotGrob()}), which always fill the full row. For fixed-size +table grobs such as \code{gt} and \code{flextable}, values at the edge (\code{0} or \code{1}) +place the table directly against the object-row edge. +Use an inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ A \code{gridifyLayout} object that defines the general structure and parameters for a pharma layout. diff --git a/man/pharma_layout_letter.Rd b/man/pharma_layout_letter.Rd index c512312..91aa595 100644 --- a/man/pharma_layout_letter.Rd +++ b/man/pharma_layout_letter.Rd @@ -4,7 +4,11 @@ \alias{pharma_layout_letter} \title{Pharma Layout (Letter) for a gridify object} \usage{ -pharma_layout_letter(global_gpar = NULL, background = grid::get.gpar()$fill) +pharma_layout_letter( + global_gpar = NULL, + background = grid::get.gpar()$fill, + object_vjust = 0.5 +) } \arguments{ \item{global_gpar}{A list specifying global graphical parameters to change in the layout. @@ -14,6 +18,15 @@ which can be overwritten alongside other graphical parameters found by \code{gri \item{background}{A character string specifying the background fill colour. Default \code{grid::get.gpar()$fill} for a white background.} + +\item{object_vjust}{A numeric value in \verb{[0, 1]} controlling the vertical anchoring of the +object within its row. \code{0} aligns to the bottom, \code{0.5} (default) centers it, +and \code{1} aligns to the top. Useful when the +object's row is taller than the object itself. Has no effect on flexible grobs +(e.g. \code{ggplot2::ggplotGrob()}), which always fill the full row. For fixed-size +table grobs such as \code{gt} and \code{flextable}, values at the edge (\code{0} or \code{1}) +place the table directly against the object-row edge. +Use an inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ A \code{gridifyLayout} object with the structure defined for letter paper size. diff --git a/man/simple_layout.Rd b/man/simple_layout.Rd index af8dabf..4546b62 100644 --- a/man/simple_layout.Rd +++ b/man/simple_layout.Rd @@ -8,7 +8,8 @@ simple_layout( margin = grid::unit(c(t = 0.1, r = 0.1, b = 0.1, l = 0.1), units = "npc"), global_gpar = grid::gpar(), background = grid::get.gpar()$fill, - scales = c("fixed", "free") + scales = c("fixed", "free"), + object_vjust = 0.5 ) } \arguments{ @@ -29,6 +30,15 @@ between the text elements and the output. The \code{"free"} option makes the row heights proportional, allowing them to scale dynamically based on the overall output size. This ensures that the text elements and the output maintain relative proportions.} + +\item{object_vjust}{A numeric value in \verb{[0, 1]} controlling the vertical anchoring of the +object within its row. \code{0} aligns to the bottom, \code{0.5} (default) centers it, +and \code{1} aligns to the top. Useful when the +object's row is taller than the object itself. Has no effect on flexible grobs +(e.g. \code{ggplot2::ggplotGrob()}), which always fill the full row. For fixed-size +table grobs such as \code{gt} and \code{flextable}, values at the edge (\code{0} or \code{1}) +place the table directly against the object-row edge. +Use an inset value such as \code{0.05} or \code{0.95} if nearby text appears too close.} } \value{ A gridifyLayout object. diff --git a/tests/testthat/test-gridifyObject.R b/tests/testthat/test-gridifyObject.R index c4efa7c..f8c3eca 100644 --- a/tests/testthat/test-gridifyObject.R +++ b/tests/testthat/test-gridifyObject.R @@ -9,10 +9,19 @@ test_that("gridifyObject can be created with a proper input", { ) }) -test_that("gridifyObject has four cells of proper types: row, col, height and width", { +test_that("gridifyObject has the expected slots and types", { class_spec <- getSlots("gridifyObject") - expect_identical(class_spec, c(row = "numeric", col = "numeric", height = "numeric", width = "numeric")) + expect_identical( + class_spec, + c( + row = "numeric", + col = "numeric", + height = "numeric", + width = "numeric", + vjust = "numeric" + ) + ) }) @@ -58,4 +67,46 @@ test_that("gridifyObject setValidity tests", { ), "cell col has to be positive integer" ) + + expect_error( + new("gridifyObject", + row = 1, + col = 1, + height = 2, + width = 1 + ), + "height must be less than or equal to 1." + ) +}) + +test_that("gridifyObject vjust validity and default", { + expect_identical(gridifyObject(row = 1, col = 1)@vjust, 0.5) + + expect_silent(gridifyObject(row = 1, col = 1, vjust = 0)) + expect_silent(gridifyObject(row = 1, col = 1, vjust = 1)) + + expect_error( + gridifyObject(row = 1, col = 1, vjust = -0.1), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = 1.5), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = c(0, 1)), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = NA_real_), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = NaN), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = Inf), + "vjust has to be a single finite numeric value in \\[0, 1\\]" + ) }) diff --git a/tests/testthat/test_layouts.R b/tests/testthat/test_layouts.R index e36f675..4b3e240 100644 --- a/tests/testthat/test_layouts.R +++ b/tests/testthat/test_layouts.R @@ -15,14 +15,21 @@ test_that("all layout functions return a gridifyLayout", { } }) -test_that("all layout funs have any or some of args: margin, global_gpar, background, scales, adjust_height", { +test_that("all layout funs have any or some of args: margin, global_gpar, background, scales, adjust_height, object_vjust", { for (layout in layouts) { args <- formalArgs(asNamespace("gridify")[[layout]]) expect_true( is.null(args) || all( args %in% - c("margin", "global_gpar", "background", "scales", "adjust_height") + c( + "margin", + "global_gpar", + "background", + "scales", + "adjust_height", + "object_vjust" + ) ) ) } diff --git a/tests/testthat/test_print.R b/tests/testthat/test_print.R index 311bfed..ea312a1 100644 --- a/tests/testthat/test_print.R +++ b/tests/testthat/test_print.R @@ -11,6 +11,21 @@ test_that("print on gridifyClass returns invisibly grob object", { expect_type(print(test_gridify), "language") }) +test_that("print returns a call evaluable with attached env", { + expect_silent( + test_gridify <- gridify( + object = ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + + ggplot2::geom_point(), + layout = simple_layout() + ) + ) + + gg <- print(test_gridify) + expect_type(gg, "language") + expect_true(is.environment(attr(gg, "env"))) + expect_no_error(eval(gg, envir = attr(gg, "env"))) +}) + test_that("print on gridifyClass returns invisibly grob object", { expect_silent( test_gridify <- gridify( diff --git a/tests/testthat/test_show.R b/tests/testthat/test_show.R index bab0f4f..a6013f9 100644 --- a/tests/testthat/test_show.R +++ b/tests/testthat/test_show.R @@ -64,6 +64,7 @@ test_that("show on gridifyClass returns information to the console", { " Col: 1", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -157,6 +158,7 @@ test_that("show on more complex gridifyClass returns information to the console" " Col: 1", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -237,6 +239,7 @@ test_that("show on gridifyLayout returns information to the console", { " Col: 1", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -301,6 +304,7 @@ test_that("show on complex gridifyLayout returns information to the console", { " Col: 1-3", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 4: 1 null", @@ -387,6 +391,7 @@ test_that("test span row for output height row = c(x:y)", { " Col: 1", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 1: 0.05 npc", @@ -461,6 +466,7 @@ test_that("test span row for output height row = c(x, y)", { " Col: 1", " Width: 1", " Height: 1", + " Vjust: 0.5", "", "Object Row Heights:", " Row 1: 0.05 npc", @@ -490,3 +496,19 @@ test_that("test span row for output height row = c(x, y)", { ) ) }) + +test_that("show_spec annotates Vjust line for both default and anchored values", { + default_lyt <- simple_layout() + out <- capture_output_lines(show_spec(default_lyt)) + expect_true(any(grepl( + "^ Vjust: 0.5$", + out + ))) + + anchored_lyt <- simple_layout(object_vjust = 1) + out <- capture_output_lines(show_spec(anchored_lyt)) + expect_true(any(grepl( + "^ Vjust: 1$", + out + ))) +}) diff --git a/tests/testthat/test_viewport_height.R b/tests/testthat/test_viewport_height.R new file mode 100644 index 0000000..d351cb2 --- /dev/null +++ b/tests/testthat/test_viewport_height.R @@ -0,0 +1,101 @@ +test_that("is_flexible_grob detects ggplotGrob (gtable with null heights)", { + skip_if_not_installed("ggplot2") + gg <- ggplot2::ggplotGrob( + ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) + ggplot2::geom_point() + ) + expect_true(is_flexible_grob(gg)) +}) + +test_that("is_flexible_grob honours the gridify.flexible attribute", { + g <- grid::rectGrob() + expect_false(is_flexible_grob(g)) + attr(g, "gridify.flexible") <- TRUE + expect_true(is_flexible_grob(g)) + attr(g, "gridify.flexible") <- FALSE + expect_false(is_flexible_grob(g)) +}) + +test_that("is_flexible_grob ignores plain childrenvp (regression for false positive)", { + # Custom gTree carrying a childrenvp for clip/transform purposes — not flexible. + g <- grid::gTree( + children = grid::gList(grid::rectGrob()), + childrenvp = grid::viewport() + ) + expect_false(is_flexible_grob(g)) +}) + +test_that("is_flexible_grob returns FALSE for plain fixed-size grobs", { + expect_false(is_flexible_grob(grid::rectGrob())) + expect_false(is_flexible_grob(grid::textGrob("x"))) + expect_false(is_flexible_grob(grid::nullGrob())) +}) + +test_that("is_flexible_grob returns FALSE for a fixed-height gtable", { + skip_if_not_installed("gtable") + gt <- gtable::gtable( + widths = grid::unit(1, "npc"), + heights = grid::unit(1, "cm") + ) + expect_false(is_flexible_grob(gt)) +}) + + +test_that("object_viewport_height_expr uses npc height when vjust == 0.5", { + e <- object_viewport_height_expr(grid::rectGrob(), vjust = 0.5, height = 0.8) + expect_true(is.call(e)) + expect_identical(e[[1]], quote(grid::unit.pmax)) + inner <- e[[2]] + expect_identical(inner[[1]], quote(grid::unit)) + expect_equal(inner[[2]], 0.8) + expect_identical(inner[[3]], "npc") +}) + +test_that("object_viewport_height_expr uses npc height for flexible grobs", { + flex <- structure(grid::rectGrob(), gridify.flexible = TRUE) + e <- object_viewport_height_expr(flex, vjust = 0, height = 0.5) + inner <- e[[2]] + expect_identical(inner[[1]], quote(grid::unit)) + expect_equal(inner[[2]], 0.5) +}) + +test_that("object_viewport_height_expr uses grobHeight for fixed grobs when vjust != 0.5", { + e <- object_viewport_height_expr(grid::rectGrob(), vjust = 0, height = 0.8) + expect_identical(e[[1]], quote(grid::unit.pmax)) + expect_identical(e[[2]], quote(grid::grobHeight(OBJECT))) +}) + +test_that("object_viewport_height_expr default floor is 1 inch", { + flex <- structure(grid::rectGrob(), gridify.flexible = TRUE) + for (case in list( + list(g = grid::rectGrob(), v = 0.5, h = 0.1), + list(g = grid::rectGrob(), v = 0.0, h = 0.1), + list(g = flex, v = 0.0, h = 0.1) + )) { + e <- object_viewport_height_expr(case$g, case$v, case$h) + floor_arg <- e[[3]] + expect_identical(floor_arg[[1]], quote(grid::unit)) + expect_equal(floor_arg[[2]], 1) + expect_identical(floor_arg[[3]], "inches") + } +}) + +test_that("object_viewport_height_expr min_height is configurable", { + e <- object_viewport_height_expr( + grid::rectGrob(), + vjust = 0, + height = 0.8, + min_height = grid::unit(2, "cm") + ) + ee <- new.env() + ee[["OBJECT"]] <- grid::rectGrob() + res <- eval(e, ee) + expect_s3_class(res, "unit") +}) + +test_that("object_viewport_height_expr returns an evaluable expression", { + ee <- new.env() + ee[["OBJECT"]] <- grid::rectGrob() + e <- object_viewport_height_expr(grid::rectGrob(), vjust = 0, height = 0.8) + res <- eval(e, ee) + expect_s3_class(res, "unit") +}) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index 8310087..53f4bea 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -704,6 +704,100 @@ options(gridify.adjust_height.line = 0.7) print(g) ``` +## Vertically Anchoring the Object + +By default the object (plot or table) is centered vertically inside its row. +When the row is taller than the object (e.g., object with a fixed-size table grob, +or when the object's `height` is set below `1`), there will be visible empty +space above and below it. + +The `object_vjust` argument of the layout helpers (and the `vjust` slot of +`gridifyObject()` for custom layouts) controls this anchoring: + +- `object_vjust = 0.5` (default) — centered. +- `object_vjust = 1` — anchored to the top of the row. +- `object_vjust = 0` — anchored to the bottom of the row. + +Note: When the object is a flextable and vjust = 0.5, the table will expand to +fill the space. When the object is a flextable and vjust does not equal 0.5, the +table height will remain fixed and position based on the vjust argument provided. +For fixed-size table grobs such as `gt` and `flextable`, edge values (`0` or `1`) +place the table directly against the object-row edge. If the first text line +above or below the table appears too close, add small spacer rows around the +object row in a custom layout, use an inset value such as `0.05` or `0.95`, or +add an explicit blank line to the nearby text. + +The example below uses a custom layout with the object occupying only `40%` +of the available row height so the difference is visible: + +```{r, fig.width = 7, fig.height = 5} +options(gridify.adjust_height.line = NULL) + +p <- ggplot(mtcars, aes(mpg, wt)) + + geom_point() + + theme_minimal() + +make_layout <- function(object_vjust = 0.5) { + gridifyLayout( + nrow = 3, ncol = 1, + heights = grid::unit(c(2, 10, 2), "cm"), + widths = grid::unit(1, "npc"), + margin = grid::unit(c(0.05, 0.05, 0.05, 0.05), "npc"), + adjust_height = FALSE, + object = gridifyObject(row = 2, col = 1, height = 0.4, vjust = object_vjust), + cells = gridifyCells( + title = gridifyCell(row = 1, col = 1), + footer = gridifyCell(row = 3, col = 1) + ) + ) +} + +# Default: object centered in the available space +gridify(p, layout = make_layout()) %>% + set_cell("title", "object_vjust = 0.5 (default behaviour)") %>% + set_cell("footer", "Footer") +``` + +```{r, fig.width = 7, fig.height = 5} +# Anchored to the top of the row +gridify(p, layout = make_layout(object_vjust = 1)) %>% + set_cell("title", "object_vjust = 1 (anchored to the top)") %>% + set_cell("footer", "Footer") +``` + +The same `object_vjust` argument is also accepted by `simple_layout()`, +`complex_layout()`, `pharma_layout_base()`, `pharma_layout_A4()` and +`pharma_layout_letter()`. For fixed-size table grobs (`flextable`, `gt`), +anchoring to the top is often preferred: + +```{r, fig.height = 5.5} +# flextable anchored to the top of the object row +ft_top <- flextable::flextable(head(mtcars[c("mpg", "wt", "cyl")], 4)) + +gridify( + object = ft_top, + layout = pharma_layout_letter(object_vjust = 1) +) %>% + set_cell("output_num", "