Skip to content

vjust arg for gridify Object#18

Merged
Polkas merged 22 commits into
mainfrom
topjust
Jun 3, 2026
Merged

vjust arg for gridify Object#18
Polkas merged 22 commits into
mainfrom
topjust

Conversation

@Polkas
Copy link
Copy Markdown
Collaborator

@Polkas Polkas commented May 10, 2026

Adds a vjust slot to gridifyObject() so users can vertically anchor the rendered object inside its layout row (e.g. vjust = 1 pins a gt/flextable to the top of a taller row instead of stretching it).

New vjust argument on gridifyObject() (default 0.5; fully backwards-compatible).
print() now sizes the object's viewport to grobHeight() for fixed-size grobs when vjust != 0.5, floored at 1 inch via grid::unit.pmax(). Flexible grobs (ggplotGrob-style gtables with null heights, recorded gTrees with childrenvp) keep the historical "fill the row" behaviour.
Detection & expression building factored into is_flexible_grob() and object_viewport_height_expr() in gridify-utils.R, with unit tests.
S4 validity rejects NA, NaN, ±Inf, non-scalar and out-of-range values.
Roxygen on vjust / object_vjust (in simple_layout(), complex_layout(), pharma_layout_*) makes the flexible-grob caveat explicit; show_spec() annotates the Vjust line accordingly.
No changes for users who don't set vjust.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.58%. Comparing base (01b7233) to head (293f2ce).

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #18   +/-   ##
=======================================
  Coverage   99.57%   99.58%           
=======================================
  Files           9        9           
  Lines         936      954   +18     
=======================================
+ Hits          932      950   +18     
  Misses          4        4           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread R/gridify-utils.R Outdated
#' @param vjust numeric, the layout's object vjust.
#' @return a single logical.
#' @keywords internal
use_grob_height_for_object <- function(grob, vjust) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be a function as it's only 1 line and is used in one place, we could just have
use_grob_height_for_object <- vjust != 0.5 && !is_flexible_grob(grob) on line 124 above the natural_height assignment

@bethanygeorge bethanygeorge marked this pull request as ready for review May 22, 2026 13:03
@Polkas
Copy link
Copy Markdown
Collaborator Author

Polkas commented May 28, 2026

Examples:

# Manual branch/vjust examples
# Run from the package root with:
# source("inst/manual-tests/vjust_branch_examples.R")
#
# Outputs are written to inst/manual-tests/outputs/ by default.
# Review the generated PDFs/PNGs visually, especially examples whose labels
# mention top, center, or bottom anchoring.

if (requireNamespace("devtools", quietly = TRUE)) {
  devtools::load_all(".", quiet = TRUE)
} else {
  library(gridify)
}

out_dir <- file.path("inst", "manual-tests", "outputs")
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)

clean_file <- function(path) {
  unlink(path, force = TRUE)
}

check_file <- function(path) {
  stopifnot(file.exists(path))
  stopifnot(file.size(path) > 0)
}

run_example <- function(label, expr) {
  message("\n", label)
  force(expr)
  message("OK: ", label)
}

skip_if_missing <- function(package, label) {
  if (!requireNamespace(package, quietly = TRUE)) {
    message("Skipped ", label, " because package '", package, "' is not installed.")
    return(TRUE)
  }
  FALSE
}

safe_export <- function(x, path, ...) {
  clean_file(path)
  export_to(x, path, ...)
  check_file(path)
  invisible(path)
}

rect_obj <- function(col = "#5B8DEF") {
  grid::rectGrob(
    width = grid::unit(1.2, "in"),
    height = grid::unit(0.6, "in"),
    gp = grid::gpar(fill = col, col = "#1F2937", lwd = 2)
  )
}

label_obj <- function(label, fill = "#E0F2FE") {
  grid::grobTree(
    grid::rectGrob(
      width = grid::unit(1.4, "in"),
      height = grid::unit(0.55, "in"),
      gp = grid::gpar(fill = fill, col = "#0F172A", lwd = 2)
    ),
    grid::textGrob(label, gp = grid::gpar(fontsize = 12, fontface = "bold"))
  )
}

cm_to_in <- function(x) {
  x / 2.54
}

fixed_gtable_obj <- function(label, height = grid::unit(2, "cm"), fill = "#DBEAFE") {
  g <- gtable::gtable(
    widths = grid::unit(10, "cm"),
    heights = height
  )
  gtable::gtable_add_grob(
    g,
    grobs = label_obj(label, fill),
    t = 1,
    l = 1
  )
}

null_height_gtable_obj <- function(label, fill = "#FCE7F3") {
  g <- gtable::gtable(
    widths = grid::unit(1, "npc"),
    heights = grid::unit(c(1, 2), c("cm", "null"))
  )
  g <- gtable::gtable_add_grob(
    g,
    grobs = grid::rectGrob(gp = grid::gpar(fill = "#F8FAFC", col = "#64748B")),
    t = 1,
    l = 1
  )
  gtable::gtable_add_grob(
    g,
    grobs = label_obj(label, fill),
    t = 2,
    l = 1
  )
}

make_simple <- function(object, object_vjust, title, footer = NULL, scales = "fixed") {
  obj <- gridify(
    object,
    layout = simple_layout(
      margin = grid::unit(c(0.8, 0.8, 0.8, 0.8), "in"),
      scales = scales,
      object_vjust = object_vjust
    )
  ) |>
    set_cell("title", title)

  if (!is.null(footer)) {
    obj <- obj |>
      set_cell("footer", footer)
  }
  obj
}

make_text_vjust_layout <- function() {
  gridifyLayout(
    nrow = 5L,
    ncol = 1L,
    heights = grid::unit(c(1, 1, 3, 1, 1), c("lines", "lines", "null", "lines", "lines")),
    widths = grid::unit(1, "npc"),
    margin = grid::unit(c(0.5, 0.5, 0.5, 0.5), "in"),
    global_gpar = grid::gpar(fontsize = 11),
    background = grid::get.gpar()$fill,
    adjust_height = TRUE,
    object = gridifyObject(row = 3, col = 1, height = 0.5, vjust = 0.5),
    cells = gridifyCells(
      title = gridifyCell(row = 1, col = 1),
      top_text = gridifyCell(row = 2, col = 1, y = 1, vjust = 1),
      bottom_text = gridifyCell(row = 4, col = 1, y = 0, vjust = 0),
      footer = gridifyCell(row = 5, col = 1)
    )
  )
}

make_fixed_10cm_layout <- function(object_vjust = 1, object_height = 1) {
  gridifyLayout(
    nrow = 3L,
    ncol = 1L,
    heights = grid::unit(c(1.2, 10, 1.2), c("cm", "cm", "cm")),
    widths = grid::unit(1, "npc"),
    margin = grid::unit(c(0.8, 0.8, 0.8, 0.8), "in"),
    global_gpar = grid::gpar(fontsize = 11),
    background = grid::get.gpar()$fill,
    adjust_height = TRUE,
    object = gridifyObject(row = 2, col = 1, height = object_height, vjust = object_vjust),
    cells = gridifyCells(
      title = gridifyCell(row = 1, col = 1),
      footer = gridifyCell(row = 3, col = 1)
    )
  )
}

run_example("01 simple layout fixed grob centered with object_vjust = 0.5", {
  path <- file.path(out_dir, "01_simple_center.pdf")
  x <- make_simple(rect_obj(), 0.5, "object_vjust = 0.5: centered/fills row")
  safe_export(x, path)
})

run_example("02 simple layout fixed grob anchored to top with object_vjust = 1", {
  path <- file.path(out_dir, "02_simple_top.pdf")
  x <- make_simple(rect_obj("#BBF7D0"), 1, "object_vjust = 1: anchored to top")
  safe_export(x, path)
})

run_example("03 simple layout fixed grob anchored to bottom with object_vjust = 0", {
  path <- file.path(out_dir, "03_simple_bottom.pdf")
  x <- make_simple(rect_obj("#FDE68A"), 0, "object_vjust = 0: anchored to bottom")
  safe_export(x, path)
})

run_example("04 simple layout top/center/bottom comparison exported as separate PNGs", {
  paths <- file.path(out_dir, sprintf("04_simple_compare_%s.png", c("bottom", "center", "top")))
  values <- c(0, 0.5, 1)
  labels <- c("bottom", "center", "top")
  for (i in seq_along(values)) {
    x <- make_simple(label_obj(labels[[i]]), values[[i]], paste("object_vjust =", values[[i]], labels[[i]]))
    safe_export(x, paths[[i]], width = 900, height = 700, res = 120)
  }
})

run_example("05 simple layout with free scales and object_vjust = 1", {
  path <- file.path(out_dir, "05_simple_free_top.pdf")
  x <- make_simple(rect_obj("#C4B5FD"), 1, "free scales + object_vjust = 1", scales = "free")
  safe_export(x, path)
})

run_example("06 complex layout fixed grob anchored to top", {
  path <- file.path(out_dir, "06_complex_top.pdf")
  x <- gridify(
    rect_obj("#FBCFE8"),
    layout = complex_layout(object_vjust = 1)
  ) |>
    set_cell("header_left", "Complex layout") |>
    set_cell("title", "Fixed grob anchored to top") |>
    set_cell("footer_right", "object_vjust = 1")
  safe_export(x, path)
})

run_example("07 complex layout fixed grob anchored to bottom", {
  path <- file.path(out_dir, "07_complex_bottom.pdf")
  x <- gridify(
    rect_obj("#BFDBFE"),
    layout = complex_layout(object_vjust = 0)
  ) |>
    set_cell("header_left", "Complex layout") |>
    set_cell("title", "Fixed grob anchored to bottom") |>
    set_cell("footer_right", "object_vjust = 0")
  safe_export(x, path)
})

run_example("08 pharma base layout with default object_vjust = 0.5", {
  path <- file.path(out_dir, "08_pharma_center.pdf")
  x <- gridify(rect_obj("#DDD6FE"), layout = pharma_layout_base()) |>
    set_cell("title_1", "pharma_layout_base default object_vjust = 0.5") |>
    set_cell("footer_right", "center/default")
  safe_export(x, path)
})

run_example("09 pharma base layout anchored to top", {
  path <- file.path(out_dir, "09_pharma_top.pdf")
  x <- gridify(rect_obj("#A7F3D0"), layout = pharma_layout_base(object_vjust = 1)) |>
    set_cell("title_1", "pharma_layout_base object_vjust = 1") |>
    set_cell("footer_right", "top")
  safe_export(x, path)
})

run_example("10 pharma base layout anchored to bottom", {
  path <- file.path(out_dir, "10_pharma_bottom.pdf")
  x <- gridify(rect_obj("#FED7AA"), layout = pharma_layout_base(object_vjust = 0)) |>
    set_cell("title_1", "pharma_layout_base object_vjust = 0") |>
    set_cell("footer_right", "bottom")
  safe_export(x, path)
})

run_example("11 custom gridifyObject height with vjust = 1", {
  path <- file.path(out_dir, "11_custom_object_top.pdf")
  custom_layout <- gridifyLayout(
    nrow = 3L,
    ncol = 1L,
    heights = grid::unit(c(1, 5, 1), c("lines", "null", "lines")),
    widths = grid::unit(1, "npc"),
    margin = grid::unit(c(0.5, 0.5, 0.5, 0.5), "in"),
    global_gpar = grid::gpar(),
    background = grid::get.gpar()$fill,
    adjust_height = TRUE,
    object = gridifyObject(row = 2, col = 1, height = 0.25, vjust = 1),
    cells = gridifyCells(
      title = gridifyCell(row = 1, col = 1),
      footer = gridifyCell(row = 3, col = 1)
    )
  )
  x <- gridify(rect_obj("#FDBA74"), layout = custom_layout) |>
    set_cell("title", "Custom layout: gridifyObject(height = 0.25, vjust = 1)") |>
    set_cell("footer", "Fixed grob should anchor to the top of the object row")
  safe_export(x, path)
})

run_example("12 text-cell vjust values in gridifyCell/set_cell", {
  path <- file.path(out_dir, "12_text_cell_vjust.pdf")
  x <- gridify(rect_obj("#BAE6FD"), layout = make_text_vjust_layout()) |>
    set_cell("title", "Text cell vjust checks") |>
    set_cell("top_text", "top_text: y = 1, vjust = 1") |>
    set_cell("bottom_text", "bottom_text: y = 0, vjust = 0") |>
    set_cell("footer", "Object remains centered; text cells test vjust separately")
  safe_export(x, path)
})

run_example("13 invalid gridifyObject vjust below range errors", {
  err <- tryCatch(
    {
      gridifyObject(row = 1, col = 1, vjust = -0.1)
      NULL
    },
    error = identity
  )
  stopifnot(inherits(err, "error"))
  stopifnot(grepl("vjust", conditionMessage(err), fixed = TRUE))
})

run_example("14 invalid gridifyObject vjust above range errors", {
  err <- tryCatch(
    {
      gridifyObject(row = 1, col = 1, vjust = 1.1)
      NULL
    },
    error = identity
  )
  stopifnot(inherits(err, "error"))
  stopifnot(grepl("vjust", conditionMessage(err), fixed = TRUE))
})

run_example("15 ggplot flexible grob ignores object_vjust and fills row", {
  if (!skip_if_missing("ggplot2", "15 ggplot flexible grob")) {
    path <- file.path(out_dir, "15_ggplot_flexible_top.pdf")
    p <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) +
      ggplot2::geom_point() +
      ggplot2::theme_minimal()
    x <- gridify(p, layout = simple_layout(object_vjust = 1)) |>
      set_cell("title", "ggplot object_vjust = 1: flexible grob should still fill row")
    safe_export(x, path)
  }
})

run_example("16 formula input flexible grob with object_vjust = 0", {
  if (!skip_if_missing("gridGraphics", "16 formula flexible grob")) {
    path <- file.path(out_dir, "16_formula_flexible_bottom.pdf")
    x <- gridify(~ plot(mtcars$mpg, mtcars$wt), layout = simple_layout(object_vjust = 0)) |>
      set_cell("title", "formula object_vjust = 0: recorded grob should fill row")
    safe_export(x, path)
  }
})

run_example("17 gt table fixed grob anchors to top", {
  if (!skip_if_missing("gt", "17 gt fixed grob")) {
    path <- file.path(out_dir, "17_gt_table_top.pdf")
    tbl <- gt::gt(head(mtcars, 6))
    x <- gridify(tbl, layout = pharma_layout_letter(object_vjust = 1)) |>
      set_cell("title_1", "gt table object_vjust = 1") |>
      set_cell("footer_right", "top anchored fixed-size table")
    safe_export(x, path)
  }
})

run_example("18 flextable fixed grob anchors to bottom", {
  if (!skip_if_missing("flextable", "18 flextable fixed grob")) {
    path <- file.path(out_dir, "18_flextable_bottom.pdf")
    tbl <- flextable::flextable(head(mtcars, 6))
    x <- gridify(tbl, layout = pharma_layout_letter(object_vjust = 0)) |>
      set_cell("title_1", "flextable object_vjust = 0") |>
      set_cell("footer_right", "bottom anchored fixed-size table")
    safe_export(x, path)
  }
})

run_example("19 gt table with pixel table width and row padding", {
  if (!skip_if_missing("gt", "19 gt px sizing")) {
    path <- file.path(out_dir, "19_gt_px_table.pdf")
    tbl <- gt::gt(head(mtcars, 8)) |>
      gt::tab_header(title = "gt with px sizing") |>
      gt::tab_options(
        table.width = gt::px(520),
        data_row.padding = gt::px(4),
        heading.padding = gt::px(6)
      )
    x <- gridify(tbl, layout = simple_layout(object_vjust = 1)) |>
      set_cell("title", "gt: table.width/data_row.padding specified in px")
    safe_export(x, path)
  }
})

run_example("20 gt table with percentage width", {
  if (!skip_if_missing("gt", "20 gt pct sizing")) {
    path <- file.path(out_dir, "20_gt_pct_table.pdf")
    tbl <- gt::gt(head(iris, 10)) |>
      gt::tab_header(title = "gt with percentage width") |>
      gt::tab_options(
        table.width = gt::pct(70),
        data_row.padding = gt::px(2)
      )
    x <- gridify(tbl, layout = simple_layout(object_vjust = 0)) |>
      set_cell("title", "gt: table.width = pct(70), object_vjust = 0")
    safe_export(x, path)
  }
})

run_example("21 gt table with multi-line header and compact padding", {
  if (!skip_if_missing("gt", "21 gt compact multiline")) {
    path <- file.path(out_dir, "21_gt_compact_multiline.pdf")
    tbl <- gt::gt(head(mtcars, 5)) |>
      gt::tab_header(
        title = "gt compact table",
        subtitle = "multi-line heading area"
      ) |>
      gt::tab_options(
        table.width = gt::px(460),
        data_row.padding = gt::px(1),
        column_labels.padding = gt::px(2)
      )
    x <- gridify(tbl, layout = complex_layout(scales = "fixed", object_vjust = 1)) |>
      set_cell("title", "gt compact/multiline case") |>
      set_cell("subtitle", "sthyg") |>
      set_cell("footer_right", "top anchored")
    safe_export(x, path)
  }
})

run_example("21b gt table with multi-line header and compact padding", {
  if (!skip_if_missing("gt", "21 gt compact multiline")) {
    path <- file.path(out_dir, "21_gt_compact_multiline.pdf")
    tbl <- gt::gt(head(mtcars, 5)) |>
      gt::tab_header(
        title = "gt compact table",
        subtitle = "multi-line heading area"
      ) |>
      gt::tab_options(
        table.width = gt::px(460),
        data_row.padding = gt::px(1),
        column_labels.padding = gt::px(2)
      )
    x <- gridify(tbl, layout = complex_layout(object_vjust = 1)) |>
      set_cell("title_1", "gt compact/multiline case") |>
      set_cell("subtitle", "sthyg") |>
      set_cell("footer_right", "top anchored")
    safe_export(x, path)
  }
})

run_example("22 flextable with fixed widths/heights converted from cm", {
  if (!skip_if_missing("flextable", "22 flextable cm sizing")) {
    path <- file.path(out_dir, "22_flextable_cm_dimensions.pdf")
    tbl <- flextable::flextable(head(iris, 8)) |>
      flextable::width(width = cm_to_in(2.4)) |>
      flextable::height_all(height = 4) |>
      flextable::set_caption("flextable fixed dimensions converted from cm")
    x <- gridify(tbl, layout = simple_layout(object_vjust = 1)) |>
      set_cell("title", "flextable: widths/heights specified from cm values")
    safe_export(x, path)
  }
})

run_example("23 flextable autofit table centered", {
  if (!skip_if_missing("flextable", "23 flextable autofit")) {
    path <- file.path(out_dir, "23_flextable_autofit_center.pdf")
    tbl <- flextable::flextable(head(mtcars, 6)) |>
      flextable::autofit()
    x <- gridify(tbl, layout = simple_layout(object_vjust = 0.5)) |>
      set_cell("title", "flextable: autofit, default centered/fill behavior")
    safe_export(x, path)
  }
})

run_example("24 flextable with long wrapped text anchored to top", {
  if (!skip_if_missing("flextable", "24 flextable wrapped text")) {
    path <- file.path(out_dir, "24_flextable_wrapped_text_top.pdf")
    long_data <- data.frame(
      group = c("A", "B", "C"),
      description = c(
        "Short label",
        "Longer label that should wrap inside the flextable cell",
        "Another long label for manual fixed-size table inspection"
      )
    )
    tbl <- flextable::flextable(long_data) |>
      flextable::width(j = "group", width = cm_to_in(1.2)) |>
      flextable::width(j = "description", width = cm_to_in(6)) |>
      flextable::height_all(height = cm_to_in(0.7))
    x <- gridify(tbl, layout = pharma_layout_letter(object_vjust = 1)) |>
      set_cell("title_1", "flextable: wrapped text + cm-derived dimensions") |>
      set_cell("footer_right", "top anchored")
    safe_export(x, path)
  }
})

run_example("25 fixed-height gtable in cm anchors to top", {
  if (!skip_if_missing("gtable", "25 fixed-height gtable")) {
    path <- file.path(out_dir, "25_gtable_fixed_cm_top.pdf")
    x <- gridify(
      fixed_gtable_obj("fixed gtable: 2 cm", height = grid::unit(2, "cm")),
      layout = simple_layout(object_vjust = 1)
    ) |>
      set_cell("title", "gtable with fixed cm height should anchor to top")
    safe_export(x, path)
  }
})

run_example("26 fixed-height gtable in pixels anchors to bottom", {
  if (!skip_if_missing("gtable", "26 fixed-height pixel gtable")) {
    path <- file.path(out_dir, "26_gtable_fixed_px_bottom.pdf")
    x <- gridify(
      fixed_gtable_obj("fixed gtable: 96 px", height = grid::unit(96, "points")),
      layout = simple_layout(object_vjust = 0)
    ) |>
      set_cell("title", "gtable using point/pixel-like fixed height should anchor to bottom")
    safe_export(x, path)
  }
})

run_example("27 null-height gtable fills row regardless of object_vjust", {
  if (!skip_if_missing("gtable", "27 null-height gtable")) {
    path <- file.path(out_dir, "27_gtable_null_height_flexible.pdf")
    x <- gridify(
      null_height_gtable_obj("contains a null height row"),
      layout = simple_layout(object_vjust = 1)
    ) |>
      set_cell("title", "gtable with null height should be treated as flexible")
    safe_export(x, path)
  }
})

run_example("28 ggplot object with object_vjust = 1 remains flexible", {
  if (!skip_if_missing("ggplot2", "28 ggplot object")) {
    path <- file.path(out_dir, "28_ggplot_object_top.pdf")
    p <- ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Petal.Length, colour = Species)) +
      ggplot2::geom_point() +
      ggplot2::theme_minimal()
    x <- gridify(p, layout = complex_layout(object_vjust = 1)) |>
      set_cell("title", "ggplot object: gtable has null heights and fills row")
    safe_export(x, path)
  }
})

run_example("29 ggplotGrob object with null heights remains flexible", {
  if (!skip_if_missing("ggplot2", "29 ggplotGrob object")) {
    path <- file.path(out_dir, "29_ggplot_grob_null_heights.pdf")
    p <- ggplot2::ggplot(mtcars, ggplot2::aes(factor(cyl), mpg)) +
      ggplot2::geom_boxplot() +
      ggplot2::theme_minimal()
    x <- gridify(ggplot2::ggplotGrob(p), layout = simple_layout(object_vjust = 0)) |>
      set_cell("title", "ggplotGrob: explicit gtable with null heights")
    safe_export(x, path)
  }
})

run_example("30 base plot scatter via formula remains flexible", {
  if (!skip_if_missing("gridGraphics", "30 base scatter formula")) {
    path <- file.path(out_dir, "30_base_scatter_formula.pdf")
    x <- gridify(
      ~ plot(mtcars$hp, mtcars$mpg, pch = 19, col = "steelblue"),
      layout = simple_layout(object_vjust = 1)
    ) |>
      set_cell("title", "base plot formula: recorded grob should fill row")
    safe_export(x, path)
  }
})

run_example("31 base plot histogram via formula remains flexible", {
  if (!skip_if_missing("gridGraphics", "31 base histogram formula")) {
    path <- file.path(out_dir, "31_base_hist_formula.pdf")
    x <- gridify(
      ~ hist(mtcars$mpg, breaks = 8, col = "gray85", border = "white"),
      layout = simple_layout(object_vjust = 0)
    ) |>
      set_cell("title", "base hist formula: recorded grob should fill row")
    safe_export(x, path)
  }
})

run_example("32 custom 10 cm row with fixed rect grob anchored to top", {
  path <- file.path(out_dir, "32_custom_10cm_rect_top.pdf")
  x <- gridify(
    rect_obj("#A7F3D0"),
    layout = make_fixed_10cm_layout(object_vjust = 1, object_height = 0.35)
  ) |>
    set_cell("title", "Custom layout: 10 cm object row, rect grob, object_vjust = 1") |>
    set_cell("footer", "Fixed grob should sit at the top of the 10 cm row")
  safe_export(x, path)
})

run_example("33 custom 10 cm row with fixed rect grob anchored to bottom", {
  path <- file.path(out_dir, "33_custom_10cm_rect_bottom.pdf")
  x <- gridify(
    rect_obj("#FDE68A"),
    layout = make_fixed_10cm_layout(object_vjust = 0, object_height = 0.35)
  ) |>
    set_cell("title", "Custom layout: 10 cm object row, rect grob, object_vjust = 0") |>
    set_cell("footer", "Fixed grob should sit at the bottom of the 10 cm row")
  safe_export(x, path)
})

run_example("34 custom 10 cm row with gt table anchored to top", {
  if (!skip_if_missing("gt", "34 custom 10 cm gt")) {
    path <- file.path(out_dir, "34_custom_10cm_gt_top.pdf")
    tbl <- gt::gt(head(mtcars, 6)) |>
      gt::tab_header(title = "gt in 10 cm object row") |>
      gt::tab_options(table.width = gt::px(520), data_row.padding = gt::px(3))
    x <- gridify(
      tbl,
      layout = make_fixed_10cm_layout(object_vjust = 1)
    ) |>
      set_cell("title", "Custom layout: 10 cm object row, gt table, object_vjust = 1") |>
      set_cell("footer", "Check how a fixed-size gt table sits inside a fixed-height row")
    safe_export(x, path)
  }
})

run_example("35 custom 10 cm row with flextable anchored to bottom", {
  if (!skip_if_missing("flextable", "35 custom 10 cm flextable")) {
    path <- file.path(out_dir, "35_custom_10cm_flextable_bottom.pdf")
    tbl <- flextable::flextable(head(iris, 6)) |>
      flextable::width(width = cm_to_in(2.2)) |>
      flextable::height_all(height = cm_to_in(0.6))
    x <- gridify(
      tbl,
      layout = make_fixed_10cm_layout(object_vjust = 0)
    ) |>
      set_cell("title", "Custom layout: 10 cm object row, flextable, object_vjust = 0") |>
      set_cell("footer", "Check bottom anchoring for a fixed-size flextable")
    safe_export(x, path)
  }
})

run_example("36 custom 10 cm row with ggplot object remains flexible", {
  if (!skip_if_missing("ggplot2", "36 custom 10 cm ggplot")) {
    path <- file.path(out_dir, "36_custom_10cm_ggplot_top.pdf")
    p <- ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt, colour = factor(cyl))) +
      ggplot2::geom_point() +
      ggplot2::theme_minimal()
    x <- gridify(
      p,
      layout = make_fixed_10cm_layout(object_vjust = 1, object_height = 1)
    ) |>
      set_cell("title", "Custom layout: 10 cm object row, ggplot, object_vjust = 1") |>
      set_cell("footer", "Flexible ggplot should fill the fixed 10 cm row")
    safe_export(x, path)
  }
})

run_example("37 custom 10 cm row with ggplotGrob remains flexible", {
  if (!skip_if_missing("ggplot2", "37 custom 10 cm ggplotGrob")) {
    path <- file.path(out_dir, "37_custom_10cm_ggplotgrob_bottom.pdf")
    p <- ggplot2::ggplot(iris, ggplot2::aes(Species, Sepal.Length, fill = Species)) +
      ggplot2::geom_boxplot() +
      ggplot2::theme_minimal()
    x <- gridify(
      ggplot2::ggplotGrob(p),
      layout = make_fixed_10cm_layout(object_vjust = 0, object_height = 0.3)
    ) |>
      set_cell("title", "Custom layout: 10 cm object row, ggplotGrob, object_vjust = 0") |>
      set_cell("footer", "Explicit ggplot gtable should still fill the fixed 10 cm row")
    safe_export(x, path)
  }
})

run_example("38 custom 10 cm row with base plot formula remains flexible", {
  if (!skip_if_missing("gridGraphics", "38 custom 10 cm base plot")) {
    path <- file.path(out_dir, "38_custom_10cm_base_formula_top.pdf")
    x <- gridify(
      ~ plot(mtcars$disp, mtcars$mpg, pch = 19, col = "firebrick"),
      layout = make_fixed_10cm_layout(object_vjust = 1, object_height = 0.3)
    ) |>
      set_cell("title", "Custom layout: 10 cm object row, base formula plot, object_vjust = 1") |>
      set_cell("footer", "Recorded base plot should fill the fixed 10 cm row")
    safe_export(x, path)
  }
})

run_example("39 multi-page PDF with bottom, center, and top vjust", {
  path <- file.path(out_dir, "39_multipage_vjust_comparison.pdf")
  pages <- list(
    make_simple(label_obj("bottom", "#FDE68A"), 0, "Page 1: object_vjust = 0"),
    make_simple(label_obj("center", "#BFDBFE"), 0.5, "Page 2: object_vjust = 0.5"),
    make_simple(label_obj("top", "#BBF7D0"), 1, "Page 3: object_vjust = 1")
  )
  safe_export(pages, path)
})

run_example("40 multiple separate files with bottom, center, and top vjust", {
  paths <- file.path(out_dir, sprintf("40_separate_vjust_%s.png", c("bottom", "center", "top")))
  pages <- list(
    make_simple(label_obj("bottom", "#FDE68A"), 0, "Separate file: object_vjust = 0"),
    make_simple(label_obj("center", "#BFDBFE"), 0.5, "Separate file: object_vjust = 0.5"),
    make_simple(label_obj("top", "#BBF7D0"), 1, "Separate file: object_vjust = 1")
  )
  for (path in paths) clean_file(path)
  export_to(pages, paths, width = 900, height = 700, res = 120)
  lapply(paths, check_file)
})

message("\nManual branch/vjust examples completed. Outputs are in: ", normalizePath(out_dir))


@Polkas
Copy link
Copy Markdown
Collaborator Author

Polkas commented May 29, 2026

Example 21 looks bad, the question is if it is expected. It is easy to adjust it by the end user.

But it is a problem only for complex_layout as it uses 0 lines in header.

Comment thread R/gridify-methods.R Outdated
" Vjust: %s%s\n",
object@object@vjust,
if (object@object@vjust == 0.5) {
" (default; the object fills the full row regardless of grob type)"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not think so it is a good description

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already documented it well so I think we can remove this additional text.

Comment thread R/gridify-methods.R Outdated
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)"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not think so it is a good description

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already documented it well so I think we can remove this additional text.

Comment thread R/simple_layout.R Outdated
Comment thread R/gridify-utils.R Outdated
#' 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' `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

Comment thread R/gridify-utils.R Outdated
@Polkas Polkas merged commit ff4f441 into main Jun 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants