From 2115dc6879a29ec7fdd4582a6cecd2c0a3de7bce Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Sun, 10 May 2026 22:26:50 +0200 Subject: [PATCH 01/19] vjust arg for gridify Object --- DESCRIPTION | 5 ++- NEWS.md | 12 ++++++ R/complex_layout.R | 8 +++- R/gridify-classes.R | 24 +++++++++-- R/gridify-methods.R | 50 ++++++++++++++++++++--- R/pharma_layout.R | 20 +++++++--- R/simple_layout.R | 8 +++- inst/UML/UML_graph.md | 1 + man/complex_layout.Rd | 7 +++- man/gridifyObject-class.Rd | 3 ++ man/gridifyObject.Rd | 5 ++- man/pharma_layout_A4.Rd | 6 ++- man/pharma_layout_base.Rd | 7 +++- man/pharma_layout_letter.Rd | 6 ++- man/simple_layout.Rd | 7 +++- tests/testthat/test-gridifyObject.R | 33 ++++++++++++++- tests/testthat/test_layouts.R | 11 ++++- tests/testthat/test_show.R | 6 +++ vignettes/simple_examples.Rmd | 62 +++++++++++++++++++++++++++++ 19 files changed, 249 insertions(+), 32 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index bbd973d..5bc0099 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: gridify Title: Enrich Figures and Tables with Custom Headers and Footers and More -Version: 0.7.7 +Version: 0.7.7.9000 Authors@R: c( person("Maciej", "Nasinski", , "Maciej.Nasinski@ucb.com", role = c("aut", "cre")), person("Alexandra", "Wall", , "Alexandra.Wall@ucb.com", role = "aut"), @@ -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, @@ -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 e88a154..a00efc1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,15 @@ +# gridify 0.7.7.9000 + +* 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) keeps the previous + centered behaviour, and `1` anchors it to the top of the cell. Most useful + for fixed-size grobs such as `gt` and `flextable` tables. Reported and + proposed by Monika Beh. + + # gridify 0.7.7 * Updated `README.md` file. diff --git a/R/complex_layout.R b/R/complex_layout.R index 310948f..40ba064 100644 --- a/R/complex_layout.R +++ b/R/complex_layout.R @@ -19,6 +19,9 @@ #' 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, `1` aligns +#' to the top. Useful when the object's row is taller than the object itself. #' #' @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 +84,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 +107,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/gridify-classes.R b/R/gridify-classes.R index 1a330c6..22fa15a 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -441,6 +441,8 @@ gridifyCells <- function(...) { #' @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 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, `1` aligns to the top. #' @exportClass gridifyObject setClass( "gridifyObject", @@ -448,8 +450,10 @@ setClass( row = "numeric", col = "numeric", height = "numeric", - width = "numeric" - ) + width = "numeric", + vjust = "numeric" + ), + prototype = list(vjust = 0.5) ) setValidity("gridifyObject", function(object) { @@ -459,6 +463,9 @@ setValidity("gridifyObject", function(object) { if (min(object@col) < 1 || !all(object@col %% 1 == 0)) { stop("cell col has to be positive integer.") } + if (length(object@vjust) != 1 || object@vjust < 0 || object@vjust > 1) { + stop("vjust has to be a single numeric value in [0, 1].") + } TRUE }) @@ -471,6 +478,8 @@ setValidity("gridifyObject", function(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 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, `1` aligns to the top. #' #' @return An instance of the gridifyObject class. #' @@ -478,8 +487,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 diff --git a/R/gridify-methods.R b/R/gridify-methods.R index 69790ad..1060725 100644 --- a/R/gridify-methods.R +++ b/R/gridify-methods.R @@ -254,16 +254,52 @@ setMethod( setMethod("print", "gridifyClass", function(x, ...) { grid::grid.newpage() + # Choose the viewport height into which the object's grob is drawn. + # + # A grob is "flexible" when its contents are designed to fill whatever + # container they are placed in, rather than having a meaningful natural + # height. For such grobs we keep the historical behaviour and let the + # viewport span the full row. Two flavours are detected: + # * gtables that contain `null` units in their `heights` — + # e.g. `ggplot2::ggplotGrob()`. + # * recorded gTrees produced by `grid::grid.grabExpr()` (used internally + # for the `formula` input type via `gridGraphics::grid.echo()`); these + # carry a `childrenvp` describing the recording viewport, and + # `grobHeight()` collapses to 0 outside of it. + # + # All other grobs have a meaningful `grobHeight()` and are treated as + # fixed-size — including `gt::as_gtable()`, `flextable::gen_grob()`, plain + # `grid::rectGrob()` / `grid::nullGrob()` etc. For these the viewport is + # sized to the grob's natural height so that `vjust` can anchor it within + # a taller row. + # + # The `vjust != 0.5` guard preserves byte-for-byte backwards compatibility + # for users who do not opt into the new vertical anchoring behaviour. + is_flexible_grob <- + (inherits(x@object, "gtable") && + any(grid::unitType(x@object$heights) == "null")) || + (inherits(x@object, "gTree") && + !inherits(x@object, "gtable") && + !is.null(x@object$childrenvp)) + + height_expr <- if (x@layout@object@vjust != 0.5 && !is_flexible_grob) { + quote(grid::grobHeight(OBJECT)) + } else { + bquote( + grid::unit.pmax(grid::unit(.(h), "npc"), grid::unit(1, "inch")), + list(h = 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 +312,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 +705,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/pharma_layout.R b/R/pharma_layout.R index f15dc25..ccce130 100644 --- a/R/pharma_layout.R +++ b/R/pharma_layout.R @@ -34,6 +34,9 @@ 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`. +#' @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, `1` aligns +#' to the top. Useful when the object's row is taller than the object itself. #' #' @return A `gridifyLayout` object that defines the general structure and parameters for a pharma layout. #' @@ -77,7 +80,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 +105,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), @@ -192,12 +196,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 ) } @@ -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..24716fe 100644 --- a/R/simple_layout.R +++ b/R/simple_layout.R @@ -17,6 +17,9 @@ #' 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, `1` aligns +#' to the top. Useful when the object's row is taller than the object itself. #' #' @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 +71,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 +91,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/man/complex_layout.Rd b/man/complex_layout.Rd index 8d109ae..0cb1463 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,10 @@ 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, \code{1} aligns +to the top. Useful when the object's row is taller than the object itself.} } \value{ A gridifyLayout object. diff --git a/man/gridifyObject-class.Rd b/man/gridifyObject-class.Rd index f3a035b..d888fdd 100644 --- a/man/gridifyObject-class.Rd +++ b/man/gridifyObject-class.Rd @@ -17,5 +17,8 @@ Class for creating an object in a gridify layout. \item{\code{height}}{A numeric value specifying the height of the object.} \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, \code{1} aligns to the top.} }} diff --git a/man/gridifyObject.Rd b/man/gridifyObject.Rd index 72f0e28..154189f 100644 --- a/man/gridifyObject.Rd +++ b/man/gridifyObject.Rd @@ -4,7 +4,7 @@ \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.} @@ -14,6 +14,9 @@ gridifyObject(row, col, height = 1, width = 1) \item{height}{A numeric value specifying the height of the object. Default is 1.} \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, \code{1} aligns to the top.} } \value{ An instance of the gridifyObject class. diff --git a/man/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index 4a7cf50..e959dfa 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. diff --git a/man/pharma_layout_base.Rd b/man/pharma_layout_base.Rd index 3eaea02..7512221 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,10 @@ 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, \code{1} aligns +to the top. Useful when the object's row is taller than the object itself.} } \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..655be8d 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. diff --git a/man/simple_layout.Rd b/man/simple_layout.Rd index af8dabf..298b02d 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,10 @@ 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, \code{1} aligns +to the top. Useful when the object's row is taller than the object itself.} } \value{ A gridifyLayout object. diff --git a/tests/testthat/test-gridifyObject.R b/tests/testthat/test-gridifyObject.R index c4efa7c..bb7b9aa 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" + ) + ) }) @@ -59,3 +68,23 @@ test_that("gridifyObject setValidity tests", { "cell col has to be positive integer" ) }) + +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 numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = 1.5), + "vjust has to be a single numeric value in \\[0, 1\\]" + ) + expect_error( + gridifyObject(row = 1, col = 1, vjust = c(0, 1)), + "vjust has to be a single 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_show.R b/tests/testthat/test_show.R index bab0f4f..c02dda8 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", diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index 8310087..fb58554 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -704,6 +704,68 @@ options(gridify.adjust_height.line = 0.7) print(g) ``` +## Anchoring the Object to the Top of its Cell + +By default the object (plot or table) is centered vertically inside its row. +When the row is taller than the object naturally needs — for example with a +fixed-size table grob, or when the object's `height` is set below `1` — this +leaves 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, current behavior, fully backward-compatible. +- `object_vjust = 1` — anchored to the **top** of the row. +- `object_vjust = 0` — anchored to the bottom of the row. + +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(vj) { + 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 = vj), + 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(0.5)) %>% + set_cell("title", "vjust = 0.5 (default, centered)") %>% + set_cell("footer", "Footer") +``` + +```{r, fig.width = 7, fig.height = 5} +# Anchored to the top of the row +gridify(p, layout = make_layout(1)) %>% + set_cell("title", "vjust = 1 (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()`, which is particularly useful for tables (e.g. `gt`, +`flextable`) whose grobs have an intrinsic size smaller than the row: + +```{r, eval = FALSE} +gridify(my_table, layout = pharma_layout_A4(object_vjust = 1)) %>% + set_cell("title_1", "Table anchored to the top of its cell") +``` + ## Export to PDF, PNG, TIFF, and JPEG {#exporting-section} We can export `gridify` objects to PDF, PNG, TIFF, and JPEG files using the `export_to()` function. From 515e4d25f221693b0302b8b6c547223b4499f45e Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Sun, 10 May 2026 23:06:43 +0200 Subject: [PATCH 02/19] update docs --- R/pharma_layout.R | 6 ++++++ man/pharma_layout_A4.Rd | 4 ++++ man/pharma_layout_letter.Rd | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/R/pharma_layout.R b/R/pharma_layout.R index ccce130..23dba81 100644 --- a/R/pharma_layout.R +++ b/R/pharma_layout.R @@ -150,6 +150,9 @@ 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. +#' @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, `1` aligns +#' to the top. Useful when the object's row is taller than the object itself. #' @details #' The margins for the A4 layout are: #' * top = 1 inch @@ -218,6 +221,9 @@ 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. +#' @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, `1` aligns +#' to the top. Useful when the object's row is taller than the object itself. #' @details #' The margins for the letter layout are: #' * top = 1 inch diff --git a/man/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index e959dfa..e751b8e 100644 --- a/man/pharma_layout_A4.Rd +++ b/man/pharma_layout_A4.Rd @@ -18,6 +18,10 @@ 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, \code{1} aligns +to the top. Useful when the object's row is taller than the object itself.} } \value{ A \code{gridifyLayout} object with the structure defined for A4 paper size. diff --git a/man/pharma_layout_letter.Rd b/man/pharma_layout_letter.Rd index 655be8d..5c7ba3f 100644 --- a/man/pharma_layout_letter.Rd +++ b/man/pharma_layout_letter.Rd @@ -18,6 +18,10 @@ 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, \code{1} aligns +to the top. Useful when the object's row is taller than the object itself.} } \value{ A \code{gridifyLayout} object with the structure defined for letter paper size. From 35ada0112585f0baa02fbf0ebd658be0a51b137c Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Mon, 11 May 2026 10:36:59 +0200 Subject: [PATCH 03/19] organize the code --- R/grid_utils.R | 61 ++++++++++++++++++ R/gridify-methods.R | 42 ++----------- man/is_flexible_grob.Rd | 32 ++++++++++ man/object_viewport_height_expr.Rd | 35 +++++++++++ tests/testthat/test_viewport_height.R | 90 +++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 man/is_flexible_grob.Rd create mode 100644 man/object_viewport_height_expr.Rd create mode 100644 tests/testthat/test_viewport_height.R diff --git a/R/grid_utils.R b/R/grid_utils.R index a5fb90d..fb5b224 100644 --- a/R/grid_utils.R +++ b/R/grid_utils.R @@ -47,3 +47,64 @@ gpar_call <- function(gpar) { 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. Two +#' shapes are recognised: +#' * gtables with at least one `null` unit in their `heights` — +#' e.g. `ggplot2::ggplotGrob()`. +#' * recorded gTrees produced by `grid::grid.grabExpr()` (used internally +#' for the `formula` input type via `gridGraphics::grid.echo()`); these +#' carry a `childrenvp` describing the recording viewport, outside of +#' which `grobHeight()` collapses to 0. +#' +#' All other grobs (`gt::as_gtable()`, `flextable::gen_grob()`, plain +#' `grid::rectGrob()` / `grid::nullGrob()`, ...) are treated as fixed-size. +#' +#' @param grob a grob. +#' @return `TRUE` if `grob` is flexible, `FALSE` otherwise. +#' @keywords internal +is_flexible_grob <- function(grob) { + if (inherits(grob, "gtable")) { + return(any(grid::unitType(grob$heights) == "null")) + } + if (inherits(grob, "gTree")) { + return(!is.null(grob$childrenvp)) + } + 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 at 1 inch via +#' `grid::unit.pmax()` so the viewport never collapses. +#' +#' The natural height is used only when the caller has opted into vertical +#' anchoring (`vjust != 0.5`) and the grob is not flexible +#' (see `is_flexible_grob()`). The `vjust == 0.5` case is preserved for +#' byte-for-byte backwards compatibility with the historical +#' "fill the row" behaviour. +#' +#' 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 only for flexibility detection. +#' @param vjust numeric, the layout's object vjust. +#' @param height numeric, the layout's object height (in npc). +#' @return an unevaluated call producing a `grid::unit`. +#' @keywords internal +object_viewport_height_expr <- function(grob, vjust, height) { + natural_height <- if (vjust != 0.5 && !is_flexible_grob(grob)) { + quote(grid::grobHeight(OBJECT)) + } else { + substitute(grid::unit(h, "npc"), list(h = height)) + } + substitute( + grid::unit.pmax(NH, grid::unit(1, "inch")), + list(NH = natural_height) + ) +} diff --git a/R/gridify-methods.R b/R/gridify-methods.R index 1060725..7aa23cd 100644 --- a/R/gridify-methods.R +++ b/R/gridify-methods.R @@ -254,42 +254,12 @@ setMethod( setMethod("print", "gridifyClass", function(x, ...) { grid::grid.newpage() - # Choose the viewport height into which the object's grob is drawn. - # - # A grob is "flexible" when its contents are designed to fill whatever - # container they are placed in, rather than having a meaningful natural - # height. For such grobs we keep the historical behaviour and let the - # viewport span the full row. Two flavours are detected: - # * gtables that contain `null` units in their `heights` — - # e.g. `ggplot2::ggplotGrob()`. - # * recorded gTrees produced by `grid::grid.grabExpr()` (used internally - # for the `formula` input type via `gridGraphics::grid.echo()`); these - # carry a `childrenvp` describing the recording viewport, and - # `grobHeight()` collapses to 0 outside of it. - # - # All other grobs have a meaningful `grobHeight()` and are treated as - # fixed-size — including `gt::as_gtable()`, `flextable::gen_grob()`, plain - # `grid::rectGrob()` / `grid::nullGrob()` etc. For these the viewport is - # sized to the grob's natural height so that `vjust` can anchor it within - # a taller row. - # - # The `vjust != 0.5` guard preserves byte-for-byte backwards compatibility - # for users who do not opt into the new vertical anchoring behaviour. - is_flexible_grob <- - (inherits(x@object, "gtable") && - any(grid::unitType(x@object$heights) == "null")) || - (inherits(x@object, "gTree") && - !inherits(x@object, "gtable") && - !is.null(x@object$childrenvp)) - - height_expr <- if (x@layout@object@vjust != 0.5 && !is_flexible_grob) { - quote(grid::grobHeight(OBJECT)) - } else { - bquote( - grid::unit.pmax(grid::unit(.(h), "npc"), grid::unit(1, "inch")), - list(h = x@layout@object@height) - ) - } + # 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( diff --git a/man/is_flexible_grob.Rd b/man/is_flexible_grob.Rd new file mode 100644 index 0000000..7f38045 --- /dev/null +++ b/man/is_flexible_grob.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/grid_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. Two +shapes are recognised: +\itemize{ +\item gtables with at least one \code{null} unit in their \code{heights} — +e.g. \code{ggplot2::ggplotGrob()}. +\item recorded gTrees produced by \code{grid::grid.grabExpr()} (used internally +for the \code{formula} input type via \code{gridGraphics::grid.echo()}); these +carry a \code{childrenvp} describing the recording viewport, outside of +which \code{grobHeight()} collapses to 0. +} +} +\details{ +All other grobs (\code{gt::as_gtable()}, \code{flextable::gen_grob()}, plain +\code{grid::rectGrob()} / \code{grid::nullGrob()}, ...) are treated as fixed-size. +} +\keyword{internal} diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd new file mode 100644 index 0000000..7bfc83c --- /dev/null +++ b/man/object_viewport_height_expr.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/grid_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) +} +\arguments{ +\item{grob}{a grob; used only for flexibility detection.} + +\item{vjust}{numeric, the layout's object vjust.} + +\item{height}{numeric, the layout's object height (in npc).} +} +\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 at 1 inch via +\code{grid::unit.pmax()} so the viewport never collapses. +} +\details{ +The natural height is used only when the caller has opted into vertical +anchoring (\code{vjust != 0.5}) and the grob is not flexible +(see \code{is_flexible_grob()}). The \code{vjust == 0.5} case is preserved for +byte-for-byte backwards compatibility with the historical +"fill the row" behaviour. + +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/tests/testthat/test_viewport_height.R b/tests/testthat/test_viewport_height.R new file mode 100644 index 0000000..fa9c799 --- /dev/null +++ b/tests/testthat/test_viewport_height.R @@ -0,0 +1,90 @@ +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 detects gTrees carrying a childrenvp", { + # Shape produced internally by gridGraphics::grid.echo() for the + # `formula` input type; constructed directly to avoid the dependency. + rec <- grid::gTree( + children = grid::gList(grid::rectGrob()), + childrenvp = grid::viewport() + ) + expect_true(is_flexible_grob(rec)) +}) + +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 gTree without childrenvp", { + g <- grid::gTree(children = grid::gList(grid::rectGrob())) + expect_false(is_flexible_grob(g)) +}) + +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", { + rec <- grid::gTree( + children = grid::gList(grid::rectGrob()), + childrenvp = grid::viewport() + ) + e <- object_viewport_height_expr(rec, 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 always floors at 1 inch", { + flex <- grid::gTree( + children = grid::gList(grid::rectGrob()), + childrenvp = grid::viewport() + ) + 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]], "inch") + } +}) + +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") +}) From 83dcb5509f069b8e22e1a709c530e2e2f4ef767a Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Mon, 11 May 2026 11:03:35 +0200 Subject: [PATCH 04/19] improve and validity --- DESCRIPTION | 2 +- R/complex_layout.R | 1 + R/gridify-classes.R | 27 ++++++++++++++++++++++----- R/gridify-methods.R | 10 +++++++++- R/{grid_utils.R => gridify-utils.R} | 0 R/pharma_layout.R | 3 +++ R/simple_layout.R | 1 + man/complex_layout.Rd | 3 ++- man/gridifyObject-class.Rd | 6 +++++- man/gridifyObject.Rd | 5 ++++- man/pharma_layout_A4.Rd | 3 ++- man/pharma_layout_base.Rd | 3 ++- man/pharma_layout_letter.Rd | 3 ++- man/simple_layout.Rd | 3 ++- tests/testthat/test-gridifyObject.R | 18 +++++++++++++++--- tests/testthat/test_show.R | 12 ++++++------ 16 files changed, 77 insertions(+), 23 deletions(-) rename R/{grid_utils.R => gridify-utils.R} (100%) diff --git a/DESCRIPTION b/DESCRIPTION index 5bc0099..09b0670 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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 diff --git a/R/complex_layout.R b/R/complex_layout.R index 40ba064..4209502 100644 --- a/R/complex_layout.R +++ b/R/complex_layout.R @@ -22,6 +22,7 @@ #' @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, `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. #' #' @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 diff --git a/R/gridify-classes.R b/R/gridify-classes.R index 22fa15a..387715d 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -443,6 +443,10 @@ gridifyCells <- function(...) { #' @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, `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`. #' @exportClass gridifyObject setClass( "gridifyObject", @@ -457,17 +461,27 @@ setClass( ) 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) != 1 || object@vjust < 0 || object@vjust > 1) { - stop("vjust has to be a single numeric value in [0, 1].") + 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]." + ) } - TRUE + if (length(errs)) errs else TRUE }) #' Create a gridifyObject @@ -480,6 +494,9 @@ setValidity("gridifyObject", function(object) { #' @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, `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`. #' #' @return An instance of the gridifyObject class. #' diff --git a/R/gridify-methods.R b/R/gridify-methods.R index 7aa23cd..0d2065a 100644 --- a/R/gridify-methods.R +++ b/R/gridify-methods.R @@ -675,7 +675,15 @@ 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(sprintf( + " Vjust: %s%s\n", + object@object@vjust, + if (object@object@vjust == 0.5) { + " (default; the object fills the full row regardless of grob type)" + } else { + " (anchors fixed-size grobs; flexible grobs e.g. ggplot still fill the row)" + } + )) cat("\nObject Row Heights:\n") rows_span <- object_row[1]:object_row[length(object_row)] diff --git a/R/grid_utils.R b/R/gridify-utils.R similarity index 100% rename from R/grid_utils.R rename to R/gridify-utils.R diff --git a/R/pharma_layout.R b/R/pharma_layout.R index 23dba81..e1d1506 100644 --- a/R/pharma_layout.R +++ b/R/pharma_layout.R @@ -37,6 +37,7 @@ NULL #' @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, `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. #' #' @return A `gridifyLayout` object that defines the general structure and parameters for a pharma layout. #' @@ -153,6 +154,7 @@ pharma_layout_base <- function( #' @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, `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. #' @details #' The margins for the A4 layout are: #' * top = 1 inch @@ -224,6 +226,7 @@ pharma_layout_A4 <- function( #' @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, `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. #' @details #' The margins for the letter layout are: #' * top = 1 inch diff --git a/R/simple_layout.R b/R/simple_layout.R index 24716fe..32d0ff2 100644 --- a/R/simple_layout.R +++ b/R/simple_layout.R @@ -20,6 +20,7 @@ #' @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, `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. #' #' @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 diff --git a/man/complex_layout.Rd b/man/complex_layout.Rd index 0cb1463..3280d2d 100644 --- a/man/complex_layout.Rd +++ b/man/complex_layout.Rd @@ -33,7 +33,8 @@ 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, \code{1} aligns -to the top. Useful when the object's row is taller than the object itself.} +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.} } \value{ A gridifyLayout object. diff --git a/man/gridifyObject-class.Rd b/man/gridifyObject-class.Rd index d888fdd..588ca3e 100644 --- a/man/gridifyObject-class.Rd +++ b/man/gridifyObject-class.Rd @@ -19,6 +19,10 @@ Class for creating an object in a gridify layout. \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, \code{1} aligns to the top.} +within its cell. \code{0} aligns to the bottom, \code{0.5} (default) centers it, \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}.} }} diff --git a/man/gridifyObject.Rd b/man/gridifyObject.Rd index 154189f..591370a 100644 --- a/man/gridifyObject.Rd +++ b/man/gridifyObject.Rd @@ -16,7 +16,10 @@ gridifyObject(row, col, height = 1, width = 1, vjust = 0.5) \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, \code{1} aligns to the top.} +within its cell. \code{0} aligns to the bottom, \code{0.5} (default) centers it, \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}.} } \value{ An instance of the gridifyObject class. diff --git a/man/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index e751b8e..1f698e9 100644 --- a/man/pharma_layout_A4.Rd +++ b/man/pharma_layout_A4.Rd @@ -21,7 +21,8 @@ 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, \code{1} aligns -to the top. Useful when the object's row is taller than the object itself.} +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.} } \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 7512221..024d588 100644 --- a/man/pharma_layout_base.Rd +++ b/man/pharma_layout_base.Rd @@ -28,7 +28,8 @@ 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, \code{1} aligns -to the top. Useful when the object's row is taller than the object itself.} +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.} } \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 5c7ba3f..0d7f7cc 100644 --- a/man/pharma_layout_letter.Rd +++ b/man/pharma_layout_letter.Rd @@ -21,7 +21,8 @@ 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, \code{1} aligns -to the top. Useful when the object's row is taller than the object itself.} +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.} } \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 298b02d..0506a30 100644 --- a/man/simple_layout.Rd +++ b/man/simple_layout.Rd @@ -33,7 +33,8 @@ 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, \code{1} aligns -to the top. Useful when the object's row is taller than the object itself.} +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.} } \value{ A gridifyLayout object. diff --git a/tests/testthat/test-gridifyObject.R b/tests/testthat/test-gridifyObject.R index bb7b9aa..caf6f77 100644 --- a/tests/testthat/test-gridifyObject.R +++ b/tests/testthat/test-gridifyObject.R @@ -77,14 +77,26 @@ test_that("gridifyObject vjust validity and default", { expect_error( gridifyObject(row = 1, col = 1, vjust = -0.1), - "vjust has to be a single numeric value in \\[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 numeric value in \\[0, 1\\]" + "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 numeric value in \\[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_show.R b/tests/testthat/test_show.R index c02dda8..86635bb 100644 --- a/tests/testthat/test_show.R +++ b/tests/testthat/test_show.R @@ -64,7 +64,7 @@ test_that("show on gridifyClass returns information to the console", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 2: 1 null", @@ -158,7 +158,7 @@ test_that("show on more complex gridifyClass returns information to the console" " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 2: 1 null", @@ -239,7 +239,7 @@ test_that("show on gridifyLayout returns information to the console", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 2: 1 null", @@ -304,7 +304,7 @@ test_that("show on complex gridifyLayout returns information to the console", { " Col: 1-3", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 4: 1 null", @@ -391,7 +391,7 @@ test_that("test span row for output height row = c(x:y)", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 1: 0.05 npc", @@ -466,7 +466,7 @@ test_that("test span row for output height row = c(x, y)", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5", + " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", "", "Object Row Heights:", " Row 1: 0.05 npc", From c9416ef37d7f67ad4a67c2a63670df733ad6e4ee Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 12 May 2026 12:48:28 +0200 Subject: [PATCH 05/19] better docs --- NEWS.md | 5 +++-- R/gridify-classes.R | 10 ++++++++-- R/gridify-utils.R | 4 +++- man/gpar_args.Rd | 2 +- man/gpar_call.Rd | 2 +- man/grid_unit_type.Rd | 2 +- man/gridifyObject-class.Rd | 5 ++++- man/gridifyObject.Rd | 5 ++++- man/is_flexible_grob.Rd | 2 +- man/object_viewport_height_expr.Rd | 6 ++++-- 10 files changed, 30 insertions(+), 13 deletions(-) diff --git a/NEWS.md b/NEWS.md index a00efc1..4c63889 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,8 +6,9 @@ `pharma_layout_A4()` and `pharma_layout_letter()`. `0` aligns the object to the bottom, `0.5` (default) keeps the previous centered behaviour, and `1` anchors it to the top of the cell. Most useful - for fixed-size grobs such as `gt` and `flextable` tables. Reported and - proposed by Monika Beh. + 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. Reported and proposed by Monika Beh. # gridify 0.7.7 diff --git a/R/gridify-classes.R b/R/gridify-classes.R index 387715d..16783a1 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -439,7 +439,10 @@ 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, `1` aligns to the top. @@ -490,7 +493,10 @@ 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, `1` aligns to the top. diff --git a/R/gridify-utils.R b/R/gridify-utils.R index fb5b224..ab93719 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -94,7 +94,9 @@ is_flexible_grob <- function(grob) { #' #' @param grob a grob; used only for flexibility detection. #' @param vjust numeric, the layout's object vjust. -#' @param height numeric, the layout's object height (in npc). +#' @param height numeric, the layout's object height (in npc). Ignored on the +#' `grid::grobHeight()` branch (i.e. when `vjust != 0.5` and the grob is +#' fixed-size); used otherwise. #' @return an unevaluated call producing a `grid::unit`. #' @keywords internal object_viewport_height_expr <- function(grob, vjust, height) { 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 588ca3e..2d55ea9 100644 --- a/man/gridifyObject-class.Rd +++ b/man/gridifyObject-class.Rd @@ -14,7 +14,10 @@ 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.} diff --git a/man/gridifyObject.Rd b/man/gridifyObject.Rd index 591370a..1939afe 100644 --- a/man/gridifyObject.Rd +++ b/man/gridifyObject.Rd @@ -11,7 +11,10 @@ gridifyObject(row, col, height = 1, width = 1, vjust = 0.5) \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.} diff --git a/man/is_flexible_grob.Rd b/man/is_flexible_grob.Rd index 7f38045..ec1f8e5 100644 --- a/man/is_flexible_grob.Rd +++ b/man/is_flexible_grob.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{is_flexible_grob} \alias{is_flexible_grob} \title{Detect a "flexible" grob whose natural height is not meaningful} diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd index 7bfc83c..69dc0a6 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.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{object_viewport_height_expr} \alias{object_viewport_height_expr} \title{Build the viewport-height expression for the object's grob} @@ -11,7 +11,9 @@ object_viewport_height_expr(grob, vjust, height) \item{vjust}{numeric, the layout's object vjust.} -\item{height}{numeric, the layout's object height (in npc).} +\item{height}{numeric, the layout's object height (in npc). Ignored on the +\code{grid::grobHeight()} branch (i.e. when \code{vjust != 0.5} and the grob is +fixed-size); used otherwise.} } \value{ an unevaluated call producing a \code{grid::unit}. From f8a873d19921974bee60f52cab2004a8c093ec35 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 12 May 2026 17:06:28 +0200 Subject: [PATCH 06/19] improve small refactor --- R/gridify-classes.R | 5 ++ R/gridify-utils.R | 71 +++++++++++++++++---------- man/is_flexible_grob.Rd | 22 +++++---- man/object_viewport_height_expr.Rd | 26 +++++----- man/use_grob_height_for_object.Rd | 24 +++++++++ tests/testthat/test_show.R | 16 ++++++ tests/testthat/test_viewport_height.R | 71 ++++++++++++++++++--------- 7 files changed, 163 insertions(+), 72 deletions(-) create mode 100644 man/use_grob_height_for_object.Rd diff --git a/R/gridify-classes.R b/R/gridify-classes.R index 16783a1..e3a1b79 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -683,6 +683,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-utils.R b/R/gridify-utils.R index ab93719..6127ff0 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -51,62 +51,79 @@ gpar_call <- function(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. Two -#' shapes are recognised: -#' * gtables with at least one `null` unit in their `heights` — -#' e.g. `ggplot2::ggplotGrob()`. -#' * recorded gTrees produced by `grid::grid.grabExpr()` (used internally -#' for the `formula` input type via `gridGraphics::grid.echo()`); these -#' carry a `childrenvp` describing the recording viewport, outside of -#' which `grobHeight()` collapses to 0. +#' 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()`, ...) are treated as fixed-size. +#' `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::unitType(grob$heights) == "null")) } - if (inherits(grob, "gTree")) { - return(!is.null(grob$childrenvp)) - } FALSE } +#' Should the object's viewport be sized to the grob's natural height? +#' +#' Pure predicate driving [object_viewport_height_expr()]. Returns `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. +#' +#' @param grob a grob. +#' @param vjust numeric, the layout's object vjust. +#' @return a single logical. +#' @keywords internal +use_grob_height_for_object <- function(grob, vjust) { + vjust != 0.5 && !is_flexible_grob(grob) +} + #' 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 at 1 inch via -#' `grid::unit.pmax()` so the viewport never collapses. -#' -#' The natural height is used only when the caller has opted into vertical -#' anchoring (`vjust != 0.5`) and the grob is not flexible -#' (see `is_flexible_grob()`). The `vjust == 0.5` case is preserved for -#' byte-for-byte backwards compatibility with the historical -#' "fill the row" behaviour. +#' layout-driven height in npc, then floors the result via `grid::unit.pmax()` +#' so the viewport never collapses to zero. #' #' 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 only for flexibility detection. +#' @param grob a grob; used only by [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 `vjust != 0.5` and the grob is -#' fixed-size); used otherwise. +#' `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) { - natural_height <- if (vjust != 0.5 && !is_flexible_grob(grob)) { +object_viewport_height_expr <- function(grob, + vjust, + height, + min_height = grid::unit(1, "inch")) { + natural_height <- if (use_grob_height_for_object(grob, vjust)) { quote(grid::grobHeight(OBJECT)) } else { substitute(grid::unit(h, "npc"), list(h = height)) } substitute( - grid::unit.pmax(NH, grid::unit(1, "inch")), - list(NH = natural_height) + grid::unit.pmax(NH, MIN), + list(NH = natural_height, MIN = min_height) ) } diff --git a/man/is_flexible_grob.Rd b/man/is_flexible_grob.Rd index ec1f8e5..15eccf8 100644 --- a/man/is_flexible_grob.Rd +++ b/man/is_flexible_grob.Rd @@ -14,19 +14,21 @@ is_flexible_grob(grob) } \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. Two -shapes are recognised: -\itemize{ -\item gtables with at least one \code{null} unit in their \code{heights} — -e.g. \code{ggplot2::ggplotGrob()}. -\item recorded gTrees produced by \code{grid::grid.grabExpr()} (used internally -for the \code{formula} input type via \code{gridGraphics::grid.echo()}); these -carry a \code{childrenvp} describing the recording viewport, outside of -which \code{grobHeight()} collapses to 0. +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()}, ...) are treated as fixed-size. +\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 index 69dc0a6..91e8da0 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.Rd @@ -4,32 +4,34 @@ \alias{object_viewport_height_expr} \title{Build the viewport-height expression for the object's grob} \usage{ -object_viewport_height_expr(grob, vjust, height) +object_viewport_height_expr( + grob, + vjust, + height, + min_height = grid::unit(1, "inch") +) } \arguments{ -\item{grob}{a grob; used only for flexibility detection.} +\item{grob}{a grob; used only by \code{\link[=use_grob_height_for_object]{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{vjust != 0.5} and the grob is -fixed-size); used otherwise.} +\code{grid::grobHeight()} branch (i.e. when \code{\link[=use_grob_height_for_object]{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 at 1 inch via -\code{grid::unit.pmax()} so the viewport never collapses. +layout-driven height in npc, then floors the result via \code{grid::unit.pmax()} +so the viewport never collapses to zero. } \details{ -The natural height is used only when the caller has opted into vertical -anchoring (\code{vjust != 0.5}) and the grob is not flexible -(see \code{is_flexible_grob()}). The \code{vjust == 0.5} case is preserved for -byte-for-byte backwards compatibility with the historical -"fill the row" behaviour. - 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. diff --git a/man/use_grob_height_for_object.Rd b/man/use_grob_height_for_object.Rd new file mode 100644 index 0000000..fef838f --- /dev/null +++ b/man/use_grob_height_for_object.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gridify-utils.R +\name{use_grob_height_for_object} +\alias{use_grob_height_for_object} +\title{Should the object's viewport be sized to the grob's natural height?} +\usage{ +use_grob_height_for_object(grob, vjust) +} +\arguments{ +\item{grob}{a grob.} + +\item{vjust}{numeric, the layout's object vjust.} +} +\value{ +a single logical. +} +\description{ +Pure predicate driving \code{\link[=object_viewport_height_expr]{object_viewport_height_expr()}}. Returns \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. +} +\keyword{internal} diff --git a/tests/testthat/test_show.R b/tests/testthat/test_show.R index 86635bb..9e99268 100644 --- a/tests/testthat/test_show.R +++ b/tests/testthat/test_show.R @@ -496,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 \\(default; the object fills the full row regardless of grob type\\)$", + out + ))) + + anchored_lyt <- simple_layout(object_vjust = 1) + out <- capture_output_lines(show_spec(anchored_lyt)) + expect_true(any(grepl( + "^ Vjust: 1 \\(anchors fixed-size grobs; flexible grobs e.g. ggplot still fill the row\\)$", + out + ))) +}) diff --git a/tests/testthat/test_viewport_height.R b/tests/testthat/test_viewport_height.R index fa9c799..7361389 100644 --- a/tests/testthat/test_viewport_height.R +++ b/tests/testthat/test_viewport_height.R @@ -6,14 +6,22 @@ test_that("is_flexible_grob detects ggplotGrob (gtable with null heights)", { expect_true(is_flexible_grob(gg)) }) -test_that("is_flexible_grob detects gTrees carrying a childrenvp", { - # Shape produced internally by gridGraphics::grid.echo() for the - # `formula` input type; constructed directly to avoid the dependency. - rec <- grid::gTree( +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_true(is_flexible_grob(rec)) + expect_false(is_flexible_grob(g)) }) test_that("is_flexible_grob returns FALSE for plain fixed-size grobs", { @@ -22,11 +30,6 @@ test_that("is_flexible_grob returns FALSE for plain fixed-size grobs", { expect_false(is_flexible_grob(grid::nullGrob())) }) -test_that("is_flexible_grob returns FALSE for a gTree without childrenvp", { - g <- grid::gTree(children = grid::gList(grid::rectGrob())) - expect_false(is_flexible_grob(g)) -}) - test_that("is_flexible_grob returns FALSE for a fixed-height gtable", { skip_if_not_installed("gtable") gt <- gtable::gtable( @@ -36,6 +39,21 @@ test_that("is_flexible_grob returns FALSE for a fixed-height gtable", { expect_false(is_flexible_grob(gt)) }) +test_that("use_grob_height_for_object encodes the policy", { + fixed <- grid::rectGrob() + flex <- structure(grid::rectGrob(), gridify.flexible = TRUE) + + # vjust == 0.5 always uses npc height (legacy behaviour). + expect_false(use_grob_height_for_object(fixed, 0.5)) + expect_false(use_grob_height_for_object(flex, 0.5)) + + # vjust != 0.5 uses grobHeight only for fixed-size grobs. + expect_true(use_grob_height_for_object(fixed, 0)) + expect_true(use_grob_height_for_object(fixed, 1)) + expect_false(use_grob_height_for_object(flex, 0)) + expect_false(use_grob_height_for_object(flex, 1)) +}) + 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)) @@ -47,11 +65,8 @@ test_that("object_viewport_height_expr uses npc height when vjust == 0.5", { }) test_that("object_viewport_height_expr uses npc height for flexible grobs", { - rec <- grid::gTree( - children = grid::gList(grid::rectGrob()), - childrenvp = grid::viewport() - ) - e <- object_viewport_height_expr(rec, vjust = 0, height = 0.5) + 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) @@ -63,11 +78,8 @@ test_that("object_viewport_height_expr uses grobHeight for fixed grobs when vjus expect_identical(e[[2]], quote(grid::grobHeight(OBJECT))) }) -test_that("object_viewport_height_expr always floors at 1 inch", { - flex <- grid::gTree( - children = grid::gList(grid::rectGrob()), - childrenvp = grid::viewport() - ) +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), @@ -75,12 +87,25 @@ test_that("object_viewport_height_expr always floors at 1 inch", { )) { 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]], "inch") + expect_s3_class(floor_arg, "unit") + expect_equal(as.numeric(floor_arg), 1) + expect_identical(grid::unitType(floor_arg), "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() From edb3951cc40f25dba5857e12b1f13f77b1015950 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 12 May 2026 17:21:44 +0200 Subject: [PATCH 07/19] better examples --- vignettes/simple_examples.Rmd | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index fb58554..e1fe988 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -758,12 +758,35 @@ gridify(p, layout = make_layout(1)) %>% The same `object_vjust` argument is also accepted by `simple_layout()`, `complex_layout()`, `pharma_layout_base()`, `pharma_layout_A4()` and -`pharma_layout_letter()`, which is particularly useful for tables (e.g. `gt`, -`flextable`) whose grobs have an intrinsic size smaller than the row: +`pharma_layout_letter()`. For fixed-size table grobs (`flextable`, `gt`), +anchoring to the top is often preferred: -```{r, eval = FALSE} -gridify(my_table, layout = pharma_layout_A4(object_vjust = 1)) %>% - set_cell("title_1", "Table anchored to the top of its cell") +```{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", " xx.xx.xx") %>% + set_cell("title_1", "Anchored flextable (object_vjust = 1)") %>% + set_cell("note", "") %>% + set_cell("footer_left", "Program: , YYYY-MM-DD at HH:MM") +``` + +```{r, fig.height = 6} +# gt table anchored to the top of the object row +gt_top <- gt::gt(head(mtcars[c("mpg", "wt", "cyl")], 4)) + +gridify( + object = gt_top, + layout = pharma_layout_letter(object_vjust = 1) +) %>% + set_cell("output_num", "
xx.xx.xx") %>% + set_cell("title_1", "Anchored gt table (object_vjust = 1)") %>% + set_cell("note", "") %>% + set_cell("footer_left", "Program: , YYYY-MM-DD at HH:MM") ``` ## Export to PDF, PNG, TIFF, and JPEG {#exporting-section} From b0248023054d77680a404ddc21859bd8da203902 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 12 May 2026 21:18:59 +0200 Subject: [PATCH 08/19] add test --- tests/testthat/test_print.R | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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( From b32ca4bc4479737bcac033b8eeb96fe15abbbd1d Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 12 May 2026 21:34:41 +0200 Subject: [PATCH 09/19] fix meta programming --- R/gridify-utils.R | 9 +++++++-- man/object_viewport_height_expr.Rd | 2 +- tests/testthat/test_viewport_height.R | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/R/gridify-utils.R b/R/gridify-utils.R index 6127ff0..cf3e15a 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -116,7 +116,12 @@ use_grob_height_for_object <- function(grob, vjust) { object_viewport_height_expr <- function(grob, vjust, height, - min_height = grid::unit(1, "inch")) { + 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)) + )) + natural_height <- if (use_grob_height_for_object(grob, vjust)) { quote(grid::grobHeight(OBJECT)) } else { @@ -124,6 +129,6 @@ object_viewport_height_expr <- function(grob, } substitute( grid::unit.pmax(NH, MIN), - list(NH = natural_height, MIN = min_height) + list(NH = natural_height, MIN = min_height_call) ) } diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd index 91e8da0..8479e4d 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.Rd @@ -8,7 +8,7 @@ object_viewport_height_expr( grob, vjust, height, - min_height = grid::unit(1, "inch") + min_height = grid::unit(1, "inches") ) } \arguments{ diff --git a/tests/testthat/test_viewport_height.R b/tests/testthat/test_viewport_height.R index 7361389..bb32f89 100644 --- a/tests/testthat/test_viewport_height.R +++ b/tests/testthat/test_viewport_height.R @@ -87,9 +87,9 @@ test_that("object_viewport_height_expr default floor is 1 inch", { )) { e <- object_viewport_height_expr(case$g, case$v, case$h) floor_arg <- e[[3]] - expect_s3_class(floor_arg, "unit") - expect_equal(as.numeric(floor_arg), 1) - expect_identical(grid::unitType(floor_arg), "inches") + expect_identical(floor_arg[[1]], quote(grid::unit)) + expect_equal(floor_arg[[2]], 1) + expect_identical(floor_arg[[3]], "inches") } }) From 3df43b876a14c3b1faa275fcf36cb5bbd9002e31 Mon Sep 17 00:00:00 2001 From: Agota Bodoni <148207853+abodoni@users.noreply.github.com> Date: Thu, 21 May 2026 12:31:32 +0100 Subject: [PATCH 10/19] Updated vignette --- vignettes/simple_examples.Rmd | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index e1fe988..e21b0dc 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -704,18 +704,18 @@ options(gridify.adjust_height.line = 0.7) print(g) ``` -## Anchoring the Object to the Top of its Cell +## 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 naturally needs — for example with a -fixed-size table grob, or when the object's `height` is set below `1` — this -leaves visible empty space above and below it. +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, current behavior, fully backward-compatible. -- `object_vjust = 1` — anchored to the **top** of the row. +- `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. The example below uses a custom layout with the object occupying only `40%` @@ -728,14 +728,14 @@ p <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + theme_minimal() -make_layout <- function(vj) { +make_layout <- function(object_vjust) { 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 = vj), + 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) @@ -744,15 +744,15 @@ make_layout <- function(vj) { } # Default: object centered in the available space -gridify(p, layout = make_layout(0.5)) %>% - set_cell("title", "vjust = 0.5 (default, centered)") %>% +gridify(p, layout = make_layout()) %>% + set_cell("title", "object_vjust = 0.5 (default, centered)") %>% set_cell("footer", "Footer") ``` ```{r, fig.width = 7, fig.height = 5} # Anchored to the top of the row -gridify(p, layout = make_layout(1)) %>% - set_cell("title", "vjust = 1 (top)") %>% +gridify(p, layout = make_layout(object_vjust = 1)) %>% + set_cell("title", "object_vjust = 1 (anchored to the top)") %>% set_cell("footer", "Footer") ``` From d63430950ffd17ef0e44342dece186bc010baefa Mon Sep 17 00:00:00 2001 From: Agota Bodoni <148207853+abodoni@users.noreply.github.com> Date: Thu, 21 May 2026 14:20:59 +0100 Subject: [PATCH 11/19] Add default to vignette --- vignettes/simple_examples.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index e21b0dc..62f8974 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -728,7 +728,7 @@ p <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + theme_minimal() -make_layout <- function(object_vjust) { +make_layout <- function(object_vjust = 0.5) { gridifyLayout( nrow = 3, ncol = 1, heights = grid::unit(c(2, 10, 2), "cm"), From ee3c33fb3b4e04117d22b09bf2397ad4dccfdf99 Mon Sep 17 00:00:00 2001 From: "George Bethany (External)" Date: Thu, 21 May 2026 18:23:55 +0100 Subject: [PATCH 12/19] removed use_grob_height_for_object function --- R/gridify-utils.R | 28 ++++++++++----------------- tests/testthat/test_viewport_height.R | 14 -------------- vignettes/simple_examples.Rmd | 4 ++++ 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/R/gridify-utils.R b/R/gridify-utils.R index cf3e15a..882e243 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -78,36 +78,27 @@ is_flexible_grob <- function(grob) { FALSE } -#' Should the object's viewport be sized to the grob's natural height? -#' -#' Pure predicate driving [object_viewport_height_expr()]. Returns `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. -#' -#' @param grob a grob. -#' @param vjust numeric, the layout's object vjust. -#' @return a single logical. -#' @keywords internal -use_grob_height_for_object <- function(grob, vjust) { - vjust != 0.5 && !is_flexible_grob(grob) -} #' 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 only by [use_grob_height_for_object()]. +#' @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()] +#' `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")`. @@ -122,7 +113,8 @@ object_viewport_height_expr <- function(grob, list(as.numeric(min_height), grid_unit_type(min_height)) )) - natural_height <- if (use_grob_height_for_object(grob, vjust)) { + 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)) diff --git a/tests/testthat/test_viewport_height.R b/tests/testthat/test_viewport_height.R index bb32f89..d351cb2 100644 --- a/tests/testthat/test_viewport_height.R +++ b/tests/testthat/test_viewport_height.R @@ -39,20 +39,6 @@ test_that("is_flexible_grob returns FALSE for a fixed-height gtable", { expect_false(is_flexible_grob(gt)) }) -test_that("use_grob_height_for_object encodes the policy", { - fixed <- grid::rectGrob() - flex <- structure(grid::rectGrob(), gridify.flexible = TRUE) - - # vjust == 0.5 always uses npc height (legacy behaviour). - expect_false(use_grob_height_for_object(fixed, 0.5)) - expect_false(use_grob_height_for_object(flex, 0.5)) - - # vjust != 0.5 uses grobHeight only for fixed-size grobs. - expect_true(use_grob_height_for_object(fixed, 0)) - expect_true(use_grob_height_for_object(fixed, 1)) - expect_false(use_grob_height_for_object(flex, 0)) - expect_false(use_grob_height_for_object(flex, 1)) -}) 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) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index 62f8974..a81d09b 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -718,6 +718,10 @@ The `object_vjust` argument of the layout helpers (and the `vjust` slot of - `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. + The example below uses a custom layout with the object occupying only `40%` of the available row height so the difference is visible: From 0cb589050069a0b8a0c662192fa7fd169dd49a40 Mon Sep 17 00:00:00 2001 From: "George Bethany (External)" Date: Thu, 21 May 2026 18:56:39 +0100 Subject: [PATCH 13/19] add validatation that gridifyObject height must not be > 1 --- R/gridify-classes.R | 3 +++ tests/testthat/test-gridifyObject.R | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/R/gridify-classes.R b/R/gridify-classes.R index e3a1b79..b587a8a 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -483,6 +483,9 @@ setValidity("gridifyObject", function(object) { "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.") + } if (length(errs)) errs else TRUE }) diff --git a/tests/testthat/test-gridifyObject.R b/tests/testthat/test-gridifyObject.R index caf6f77..f8c3eca 100644 --- a/tests/testthat/test-gridifyObject.R +++ b/tests/testthat/test-gridifyObject.R @@ -67,6 +67,16 @@ 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", { From c62b05fdf49e85e2077cde20a4421d6970aa0c97 Mon Sep 17 00:00:00 2001 From: "George Bethany (External)" Date: Thu, 21 May 2026 18:56:39 +0100 Subject: [PATCH 14/19] run devtools document --- R/gridify-classes.R | 3 +++ man/object_viewport_height_expr.Rd | 10 ++++++++-- man/use_grob_height_for_object.Rd | 24 ------------------------ tests/testthat/test-gridifyObject.R | 10 ++++++++++ 4 files changed, 21 insertions(+), 26 deletions(-) delete mode 100644 man/use_grob_height_for_object.Rd diff --git a/R/gridify-classes.R b/R/gridify-classes.R index e3a1b79..b587a8a 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -483,6 +483,9 @@ setValidity("gridifyObject", function(object) { "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.") + } if (length(errs)) errs else TRUE }) diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd index 8479e4d..c5c9697 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.Rd @@ -12,12 +12,12 @@ object_viewport_height_expr( ) } \arguments{ -\item{grob}{a grob; used only by \code{\link[=use_grob_height_for_object]{use_grob_height_for_object()}}.} +\item{grob}{a grob; used to evaluate \link{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{\link[=use_grob_height_for_object]{use_grob_height_for_object()}} +\code{grid::grobHeight()} branch (i.e. when \link{use_grob_height_for_object} returns \code{TRUE}); used otherwise.} \item{min_height}{a \code{grid::unit} floor applied via \code{grid::unit.pmax()}. @@ -32,6 +32,12 @@ layout-driven height in npc, then floors the result via \code{grid::unit.pmax()} so the viewport never collapses to zero. } \details{ +\link{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. diff --git a/man/use_grob_height_for_object.Rd b/man/use_grob_height_for_object.Rd deleted file mode 100644 index fef838f..0000000 --- a/man/use_grob_height_for_object.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/gridify-utils.R -\name{use_grob_height_for_object} -\alias{use_grob_height_for_object} -\title{Should the object's viewport be sized to the grob's natural height?} -\usage{ -use_grob_height_for_object(grob, vjust) -} -\arguments{ -\item{grob}{a grob.} - -\item{vjust}{numeric, the layout's object vjust.} -} -\value{ -a single logical. -} -\description{ -Pure predicate driving \code{\link[=object_viewport_height_expr]{object_viewport_height_expr()}}. Returns \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. -} -\keyword{internal} diff --git a/tests/testthat/test-gridifyObject.R b/tests/testthat/test-gridifyObject.R index caf6f77..f8c3eca 100644 --- a/tests/testthat/test-gridifyObject.R +++ b/tests/testthat/test-gridifyObject.R @@ -67,6 +67,16 @@ 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", { From 56a2400f344b7d275eb06331ba2df637b937774a Mon Sep 17 00:00:00 2001 From: "George Bethany (External)" Date: Fri, 22 May 2026 13:58:23 +0100 Subject: [PATCH 15/19] change use_grob_height_for_object from link to code --- R/gridify-utils.R | 6 +++--- man/object_viewport_height_expr.Rd | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/gridify-utils.R b/R/gridify-utils.R index 882e243..cc60e56 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -85,7 +85,7 @@ is_flexible_grob <- function(grob) { #' 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 +#' `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" @@ -95,10 +95,10 @@ is_flexible_grob <- function(grob) { #' 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 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] +#' `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")`. diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd index c5c9697..f332c7c 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.Rd @@ -12,12 +12,12 @@ object_viewport_height_expr( ) } \arguments{ -\item{grob}{a grob; used to evaluate \link{use_grob_height_for_object}.} +\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 \link{use_grob_height_for_object} +\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()}. @@ -32,7 +32,7 @@ layout-driven height in npc, then floors the result via \code{grid::unit.pmax()} so the viewport never collapses to zero. } \details{ -\link{use_grob_height_for_object} evaluates to \code{TRUE} when the caller has opted +\verb{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" From 8edd95955895d5e9f06b5ba0ede0ab8ec5b7d3a2 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 2 Jun 2026 10:40:14 +0200 Subject: [PATCH 16/19] additional docs vjust and simpler printout for layout --- NEWS.md | 9 ++++++--- R/complex_layout.R | 19 +------------------ R/gridify-classes.R | 12 ++++++++++-- R/gridify-methods.R | 10 +--------- R/pharma_layout.R | 15 +++------------ R/simple_layout.R | 11 ++++++++--- man/complex_layout.Rd | 11 ++++++++--- man/gridifyObject-class.Rd | 8 ++++++-- man/gridifyObject.Rd | 8 ++++++-- man/pharma_layout_A4.Rd | 11 ++++++++--- man/pharma_layout_base.Rd | 11 ++++++++--- man/pharma_layout_letter.Rd | 11 ++++++++--- man/simple_layout.Rd | 11 ++++++++--- tests/testthat/test_show.R | 16 ++++++++-------- vignettes/simple_examples.Rmd | 9 +++++++-- 15 files changed, 96 insertions(+), 76 deletions(-) diff --git a/NEWS.md b/NEWS.md index 69f7a8b..a644531 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,11 +6,14 @@ `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) keeps the previous - centered behaviour, and `1` anchors it to the top of the cell. Most useful + `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. Reported and proposed by Monika Beh. + 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 4209502..35cefab 100644 --- a/R/complex_layout.R +++ b/R/complex_layout.R @@ -5,24 +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. -#' @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, `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. +#' @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 diff --git a/R/gridify-classes.R b/R/gridify-classes.R index b587a8a..963d974 100644 --- a/R/gridify-classes.R +++ b/R/gridify-classes.R @@ -445,11 +445,15 @@ gridifyCells <- function(...) { #' 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, `1` aligns to the top. +#' 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", @@ -502,10 +506,14 @@ setValidity("gridifyObject", function(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, `1` aligns to the top. +#' 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. #' diff --git a/R/gridify-methods.R b/R/gridify-methods.R index 0d2065a..7aa23cd 100644 --- a/R/gridify-methods.R +++ b/R/gridify-methods.R @@ -675,15 +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%s\n", - object@object@vjust, - if (object@object@vjust == 0.5) { - " (default; the object fills the full row regardless of grob type)" - } else { - " (anchors fixed-size grobs; flexible grobs e.g. ggplot still fill the row)" - } - )) + 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/pharma_layout.R b/R/pharma_layout.R index e1d1506..0e7f411 100644 --- a/R/pharma_layout.R +++ b/R/pharma_layout.R @@ -34,10 +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`. -#' @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, `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. +#' @inheritParams simple_layout #' #' @return A `gridifyLayout` object that defines the general structure and parameters for a pharma layout. #' @@ -151,10 +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. -#' @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, `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. +#' @inheritParams simple_layout #' @details #' The margins for the A4 layout are: #' * top = 1 inch @@ -223,10 +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. -#' @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, `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. +#' @inheritParams simple_layout #' @details #' The margins for the letter layout are: #' * top = 1 inch diff --git a/R/simple_layout.R b/R/simple_layout.R index 32d0ff2..414b17c 100644 --- a/R/simple_layout.R +++ b/R/simple_layout.R @@ -18,9 +18,14 @@ #' 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, `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. +#' 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; 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. #' #' @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 diff --git a/man/complex_layout.Rd b/man/complex_layout.Rd index 3280d2d..1ec5e1e 100644 --- a/man/complex_layout.Rd +++ b/man/complex_layout.Rd @@ -32,9 +32,14 @@ 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, \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.} +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; 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{ A gridifyLayout object. diff --git a/man/gridifyObject-class.Rd b/man/gridifyObject-class.Rd index 2d55ea9..e662a39 100644 --- a/man/gridifyObject-class.Rd +++ b/man/gridifyObject-class.Rd @@ -22,10 +22,14 @@ 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, \code{1} aligns to the top. +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}.} +\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 1939afe..97378ef 100644 --- a/man/gridifyObject.Rd +++ b/man/gridifyObject.Rd @@ -19,10 +19,14 @@ 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, \code{1} aligns to the top. +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}.} +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/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index 1f698e9..5987225 100644 --- a/man/pharma_layout_A4.Rd +++ b/man/pharma_layout_A4.Rd @@ -20,9 +20,14 @@ which can be overwritten alongside other graphical parameters found by \code{gri 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, \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.} +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; 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{ 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 024d588..47e519e 100644 --- a/man/pharma_layout_base.Rd +++ b/man/pharma_layout_base.Rd @@ -27,9 +27,14 @@ 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, \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.} +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; 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{ 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 0d7f7cc..6386cbc 100644 --- a/man/pharma_layout_letter.Rd +++ b/man/pharma_layout_letter.Rd @@ -20,9 +20,14 @@ which can be overwritten alongside other graphical parameters found by \code{gri 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, \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.} +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; 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{ 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 0506a30..9ef5284 100644 --- a/man/simple_layout.Rd +++ b/man/simple_layout.Rd @@ -32,9 +32,14 @@ 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, \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.} +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; 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{ A gridifyLayout object. diff --git a/tests/testthat/test_show.R b/tests/testthat/test_show.R index 9e99268..a6013f9 100644 --- a/tests/testthat/test_show.R +++ b/tests/testthat/test_show.R @@ -64,7 +64,7 @@ test_that("show on gridifyClass returns information to the console", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -158,7 +158,7 @@ test_that("show on more complex gridifyClass returns information to the console" " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -239,7 +239,7 @@ test_that("show on gridifyLayout returns information to the console", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 2: 1 null", @@ -304,7 +304,7 @@ test_that("show on complex gridifyLayout returns information to the console", { " Col: 1-3", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 4: 1 null", @@ -391,7 +391,7 @@ test_that("test span row for output height row = c(x:y)", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 1: 0.05 npc", @@ -466,7 +466,7 @@ test_that("test span row for output height row = c(x, y)", { " Col: 1", " Width: 1", " Height: 1", - " Vjust: 0.5 (default; the object fills the full row regardless of grob type)", + " Vjust: 0.5", "", "Object Row Heights:", " Row 1: 0.05 npc", @@ -501,14 +501,14 @@ 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 \\(default; the object fills the full row regardless of grob type\\)$", + "^ 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 \\(anchors fixed-size grobs; flexible grobs e.g. ggplot still fill the row\\)$", + "^ Vjust: 1$", out ))) }) diff --git a/vignettes/simple_examples.Rmd b/vignettes/simple_examples.Rmd index a81d09b..53f4bea 100644 --- a/vignettes/simple_examples.Rmd +++ b/vignettes/simple_examples.Rmd @@ -718,9 +718,14 @@ The `object_vjust` argument of the layout helpers (and the `vjust` slot of - `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 +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: @@ -749,7 +754,7 @@ make_layout <- function(object_vjust = 0.5) { # Default: object centered in the available space gridify(p, layout = make_layout()) %>% - set_cell("title", "object_vjust = 0.5 (default, centered)") %>% + set_cell("title", "object_vjust = 0.5 (default behaviour)") %>% set_cell("footer", "Footer") ``` From 34254913cd2efbb9bfb415745c540dfd3f60f669 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 2 Jun 2026 11:25:48 +0200 Subject: [PATCH 17/19] Alex suggestion --- R/simple_layout.R | 5 ++--- man/complex_layout.Rd | 5 ++--- man/pharma_layout_A4.Rd | 5 ++--- man/pharma_layout_base.Rd | 5 ++--- man/pharma_layout_letter.Rd | 5 ++--- man/simple_layout.Rd | 5 ++--- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/R/simple_layout.R b/R/simple_layout.R index 414b17c..6b59794 100644 --- a/R/simple_layout.R +++ b/R/simple_layout.R @@ -23,9 +23,8 @@ #' 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; 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. +#' 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 diff --git a/man/complex_layout.Rd b/man/complex_layout.Rd index 1ec5e1e..17c0bbf 100644 --- a/man/complex_layout.Rd +++ b/man/complex_layout.Rd @@ -37,9 +37,8 @@ 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; 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.} +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/pharma_layout_A4.Rd b/man/pharma_layout_A4.Rd index 5987225..7c9eabd 100644 --- a/man/pharma_layout_A4.Rd +++ b/man/pharma_layout_A4.Rd @@ -25,9 +25,8 @@ 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; 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.} +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 47e519e..88bd38c 100644 --- a/man/pharma_layout_base.Rd +++ b/man/pharma_layout_base.Rd @@ -32,9 +32,8 @@ 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; 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.} +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 6386cbc..91aa595 100644 --- a/man/pharma_layout_letter.Rd +++ b/man/pharma_layout_letter.Rd @@ -25,9 +25,8 @@ 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; 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.} +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 9ef5284..4546b62 100644 --- a/man/simple_layout.Rd +++ b/man/simple_layout.Rd @@ -37,9 +37,8 @@ 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; 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.} +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. From 151ea8d1e2ff5eb87f60635df5a7c461af236c45 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 2 Jun 2026 15:58:46 +0200 Subject: [PATCH 18/19] grid_unit_type --- R/gridify-utils.R | 2 +- inst/WORDLIST | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/R/gridify-utils.R b/R/gridify-utils.R index cc60e56..bf58726 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -73,7 +73,7 @@ is_flexible_grob <- function(grob) { return(TRUE) } if (inherits(grob, "gtable")) { - return(any(grid::unitType(grob$heights) == "null")) + return(any(grid_unit_type(grob$heights) == "null")) } FALSE } 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 From 293f2cefb59f673c6fd9a6e3b32e6aab1f23e594 Mon Sep 17 00:00:00 2001 From: Maciej Nasinski Date: Tue, 2 Jun 2026 15:59:41 +0200 Subject: [PATCH 19/19] tick --- R/gridify-utils.R | 2 +- man/object_viewport_height_expr.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/gridify-utils.R b/R/gridify-utils.R index bf58726..ee17f91 100644 --- a/R/gridify-utils.R +++ b/R/gridify-utils.R @@ -85,7 +85,7 @@ is_flexible_grob <- function(grob) { #' 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 +#' `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" diff --git a/man/object_viewport_height_expr.Rd b/man/object_viewport_height_expr.Rd index f332c7c..5e83e4f 100644 --- a/man/object_viewport_height_expr.Rd +++ b/man/object_viewport_height_expr.Rd @@ -32,7 +32,7 @@ layout-driven height in npc, then floors the result via \code{grid::unit.pmax()} so the viewport never collapses to zero. } \details{ -\verb{use_grob_height_for_object evaluates} to \code{TRUE} when the caller has opted +\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"