Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ Gmisc/inst/extdata/Full_test_suite_files/*
^README\.Rmd$
^README\.html$
^Rplots\.pdf$
^blog-posts$
^blog-posts/.*$
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ inst/extdata/Full_test_suite_files/*
/doc/
/Meta/
# Ignore generated R plots
Rplots.pdf
Rplots.pdf
blog-posts/
6 changes: 3 additions & 3 deletions CRAN-SUBMISSION
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Version: 3.3.0
Date: 2026-05-30 11:26:21 UTC
SHA: eb6e18687a408a6af6aaf9a28205b286a95d1e5e
Version: 3.4.0
Date: 2026-06-20 10:50:07 UTC
SHA: f815bf5cf3341c3e8b158e627f4d3c5437aeceee
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ NEWS for the Gmisc package
- Improved interactive example ergonomics by pausing between graph pages in multi-plot examples.
- Added `box_fn_args` parameter to `boxGrob()` for passing extra arguments directly to the box drawing function. The default is now `list(r = unit(5, "pt"))`, giving every box a fixed 5 pt corner radius (approximately equivalent to the widely-used 5 px CSS `border-radius`) regardless of box size — solving the issue where larger boxes had visibly rounder corners than smaller ones. Override per box or globally via `options(boxGrobFnArgs = list(...))`.
- Added `equalizeHeights()` for flowchart box lists, mirroring the existing `equalizeWidths()`. It sets selected boxes to a shared height (defaulting to the tallest among the selection) and supports the same `subelement` path syntax as `equalizeWidths()`.
- Added `exclude` to `spreadVertical()`/`spreadHorizontal()` and the S3 `spread()` wrapper, allowing side branches to be left out of the main spread while preserving them in the flowchart list for later `align()`, `move()`, and `connect()` calls.
- Improved `spread()` span handling: explicit `from`/`to` spans now work naturally with `margin` as an inset inside that span (for example `spread(axis = "x", from = 0, to = 0.7, margin = 0.05)` spreads within the left 70% of the viewport with padding), and subelement spreads can use deferred `position()` endpoints such as `from = position("log", "top", "y")`.
- Added `references` to `alignVertical()`/`alignHorizontal()` and the S3 `align()` wrapper. Supplying two references aligns the target to their midpoint on the selected axis, useful for centering side boxes between the main boxes before and after them.
- Clarified and tested `moveBox()`/`move()` justification semantics: for absolute moves, `just = "right"` makes `x` the right border, `just = "left"` makes `x` the left border, and the same edge-based interpretation applies vertically for top/bottom.
- Added one-to-many grouped side fan-out support to the S3 `connect()` API for `connect("source", "group", type = "side")`. The shared fan-out bus now honors `side_route = "outside"` and `side_offset` before branching to the destination boxes, which keeps side-branch arrows away from the source box edge.
- Added many-to-one grouped side fan-in to the S3 `connect()` API for `connect(list("a", "b"), "target", type = "side")`. The shared return bus now honors `side`/`end_side` and `side_route`/`side_offset`, routing to the requested outer side instead of letting each line independently pick the closest side of the target.
- Fixed one-to-one `connect(..., type = "side")` connectors so they honor `side_route`/`side_offset`. The vertical segment now steps out from the source box edge by the offset (matching the grouped fan-in/fan-out behavior); previously the offset was ignored and the line hugged the box edge. Use `side_route = "edge"` to keep the old edge-hugging route.
- Extended connector labels to grouped side fan-out connectors. `label` can now be used directly on a grouped `type = "side"` fan-out; the label is centered on the offset bus, drawn on the outgoing line, and uses a semi-transparent white background by default so the line remains visually legible.
- Added and expanded alternate flowchart shape examples using `boxEllipseGrob()`, `boxDatabaseGrob()`, `boxDocumentGrob()`, `boxDocumentsGrob()`, `boxDiamondGrob()`, and `boxTapeGrob()` together with `spread()`/`align()` positioning.
- Improved alternate shape helpers so non-rectangular shapes add appropriate text padding and expose visible bounds for connector anchoring. This keeps labels inside ellipse/database/document/tape shapes and lets connectors hit the visible shape edge instead of the surrounding viewport, and `fan_in_top` trunks now terminate exactly on the target's top edge.
- Added `position()` with relative moves.
- Added dotted arrows lines for flowcharts
- Added regex selector for box lists
Expand Down
94 changes: 73 additions & 21 deletions R/boxGrobs_align.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
#' @param reference A [`boxGrob`]/[`boxPropGrob`]/[`coords`] object or a [`unit`][grid::unit] or a
#' numerical value that can be converted into a [`unit`][grid::unit] of `npc` type. If a numeric scalar is
#' provided and is `0` or greater than the number of boxes, it is treated as a coordinate (no error is raised).
#' @param references Optional pair of references. When supplied, the alignment
#' reference is the midpoint between the two references on the selected axis.
#' This is useful for centering side boxes, such as excluded patients, between
#' the main boxes before and after them. Each reference can be anything
#' accepted by `reference`, including a path such as `"assessed"` or
#' `c("arms", 1)`.
#' @param ... A set of boxes.
#' @param position How to align the boxes, differs slightly for vertical and horizontal alignment
#' see the accepted arguments
Expand All @@ -31,7 +37,7 @@
#' @rdname align


alignVertical <- function(reference, ..., position = c("center", "top", "bottom"), subelement = NULL) {
alignVertical <- function(reference = NULL, ..., position = c("center", "top", "bottom"), subelement = NULL, references = NULL) {
position <- match.arg(position)

boxes2align <- list(...)
Expand Down Expand Up @@ -71,7 +77,7 @@ alignVertical <- function(reference, ..., position = c("center", "top", "bottom"
# boxes to align and use its first element as the reference.
if (length(boxes2align) == 0 && is.list(reference) && !inherits(reference, "box")) {
boxes2align <- reference
reference <- boxes2align[[1]]
reference <- if (is.null(references)) boxes2align[[1]] else NULL
}

# If a list of boxes was piped into '...' as a single element (e.g., using
Expand All @@ -81,6 +87,13 @@ alignVertical <- function(reference, ..., position = c("center", "top", "bottom"
boxes2align <- boxes2align[[1]]
}

if (!is.null(references)) {
if (!is.null(reference)) {
stop("Use either `reference` or `references`, not both.", call. = FALSE)
}
prValidateReferencePair(references)
}

if (!is.null(subelement)) {
# Normalize into a list of paths (each path is an atomic vector)
paths <- prResolveSubelementSelector(subelement, boxes2align)
Expand All @@ -103,13 +116,19 @@ alignVertical <- function(reference, ..., position = c("center", "top", "bottom"
)
}

# Resolve the reference path against the original list so recursive calls get an object
ref_for_call <- prResolveReference(reference, boxes2align)
ref_for_call <- prResolveAlignReference(
reference = reference,
references = references,
boxes2align = boxes2align,
axis = "vertical"
)

aligned <- alignVertical(
reference = ref_for_call,
target,
position = position
target_boxes <- if (inherits(target, "box")) list(target) else target
aligned <- prApplyAlign(
boxes2align = prNormalizeAndValidateBoxes(target_boxes),
ref_positions = prConvert2Coords(ref_for_call),
position = position,
axis = "vertical"
)

# If the target was a single box, the recursive call returns a
Expand All @@ -133,8 +152,12 @@ alignVertical <- function(reference, ..., position = c("center", "top", "bottom"
# Normalize and validate boxes now that subelement processing is done
boxes2align <- prNormalizeAndValidateBoxes(boxes2align)

# Resolve reference if it's a path into the boxes
reference <- prResolveReference(reference, boxes2align)
reference <- prResolveAlignReference(
reference = reference,
references = references,
boxes2align = boxes2align,
axis = "vertical"
)
ref_positions <- prConvert2Coords(reference)

# Apply vertical alignment
Expand All @@ -148,11 +171,12 @@ alignVertical <- function(reference, ..., position = c("center", "top", "bottom"
#' @md
#' @export
alignHorizontal <- function(
reference,
reference = NULL,
...,
position = c("center", "left", "right"),
sub_position = c("none", "left", "right"),
subelement = NULL
subelement = NULL,
references = NULL
) {
position <- match.arg(position)
sub_position <- match.arg(sub_position)
Expand Down Expand Up @@ -189,7 +213,7 @@ alignHorizontal <- function(
# boxes to align and use its first element as the reference.
if (length(boxes2align) == 0 && is.list(reference) && !inherits(reference, "box")) {
boxes2align <- reference
reference <- boxes2align[[1]]
reference <- if (is.null(references)) boxes2align[[1]] else NULL
}

# If a list of boxes was piped into '...' as a single element (e.g., using
Expand All @@ -199,6 +223,13 @@ alignHorizontal <- function(
boxes2align <- boxes2align[[1]]
}

if (!is.null(references)) {
if (!is.null(reference)) {
stop("Use either `reference` or `references`, not both.", call. = FALSE)
}
prValidateReferencePair(references)
}

if (!is.null(subelement)) {
# Normalize into a list of paths (each path is an atomic vector)
paths <- prResolveSubelementSelector(subelement, boxes2align)
Expand All @@ -215,14 +246,31 @@ alignHorizontal <- function(
)
}

# Resolve the reference path against the original list so recursive calls get an object
ref_for_call <- prResolveReference(reference, boxes2align)
ref_for_call <- prResolveAlignReference(
reference = reference,
references = references,
boxes2align = boxes2align,
axis = "horizontal"
)
ref_positions <- prConvert2Coords(ref_for_call)
if (sub_position != "none") {
if (sub_position == "left") {
assert_class(ref_positions$left_x, "unit")
ref_positions$x <- ref_positions$left_x
ref_positions$right <- ref_positions$prop_x
} else {
assert_class(ref_positions$right_x, "unit")
ref_positions$x <- ref_positions$right_x
ref_positions$left <- ref_positions$prop_x
}
}

aligned <- alignHorizontal(
reference = ref_for_call,
target,
target_boxes <- if (inherits(target, "box")) list(target) else target
aligned <- prApplyAlign(
boxes2align = prNormalizeAndValidateBoxes(target_boxes),
ref_positions = ref_positions,
position = position,
sub_position = sub_position
axis = "horizontal"
)

# Unwrap single-element aligned box when target is a single box to
Expand All @@ -244,8 +292,12 @@ alignHorizontal <- function(
# Normalize and validate boxes now that subelement processing is done
boxes2align <- prNormalizeAndValidateBoxes(boxes2align)

# Resolve reference if it's a path into the boxes
reference <- prResolveReference(reference, boxes2align)
reference <- prResolveAlignReference(
reference = reference,
references = references,
boxes2align = boxes2align,
axis = "horizontal"
)
ref_positions <- prConvert2Coords(reference)
if (sub_position != "none") {
if (sub_position == "left") {
Expand Down
100 changes: 100 additions & 0 deletions R/boxGrobs_align_pr_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,106 @@ prResolveReference <- function(reference, boxes2align) {
reference
}

prValidateReferencePair <- function(references) {
if (!is.list(references) || length(references) != 2) {
stop("`references` must be a list containing exactly two references.", call. = FALSE)
}
}

prResolveReferenceStrict <- function(reference, boxes2align) {
resolved <- prResolveReference(reference, boxes2align)

if (identical(resolved, reference) &&
(is.atomic(reference) || is.list(reference)) &&
!inherits(reference, "box") &&
!inherits(reference, "coords") &&
!inherits(reference, "unit") &&
!is.numeric(reference)) {
stop(
"The reference '",
paste(reference, collapse = " -> "),
"' was not found in the provided boxes.",
call. = FALSE
)
}

resolved
}

prResolveAlignReferenceArgs <- function(reference, references, boxes2align, axis = c("vertical", "horizontal")) {
axis <- match.arg(axis)

if (is.null(references)) {
return(list(
reference = prResolveReference(reference, boxes2align),
references = NULL
))
}

list(
reference = NULL,
references = lapply(references, prResolveReferenceStrict, boxes2align = boxes2align)
)
}

prResolveAlignReference <- function(reference, references, boxes2align, axis = c("vertical", "horizontal")) {
axis <- match.arg(axis)

if (is.null(references)) {
if (is.null(reference)) {
reference <- boxes2align[[1]]
}
return(prResolveReference(reference, boxes2align))
}

resolved <- prResolveAlignReferenceArgs(
reference = reference,
references = references,
boxes2align = boxes2align,
axis = axis
)$references

prReferencePairMidpoint(resolved, axis = axis)
}

prReferencePairMidpoint <- function(references, axis = c("vertical", "horizontal")) {
axis <- match.arg(axis)
coords_pair <- lapply(references, prConvert2Coords)

c1 <- coords_pair[[1]]
c2 <- coords_pair[[2]]

if (axis == "vertical") {
y <- c1$y + (c2$y - c1$y) * 0.5
return(structure(list(
x = unit(0.5, "npc"),
y = y,
top = y,
bottom = y,
left = unit(0.5, "npc"),
right = unit(0.5, "npc"),
width = unit(0, "npc"),
height = unit(0, "npc"),
half_width = unit(0, "npc"),
half_height = unit(0, "npc")
), class = c("coords", "box_coords", "list")))
}

x <- c1$x + (c2$x - c1$x) * 0.5
structure(list(
x = x,
y = unit(0.5, "npc"),
top = unit(0.5, "npc"),
bottom = unit(0.5, "npc"),
left = x,
right = x,
width = unit(0, "npc"),
height = unit(0, "npc"),
half_width = unit(0, "npc"),
half_height = unit(0, "npc")
), class = c("coords", "box_coords", "list"))
}

# Apply alignment given resolved boxes and ref positions
prApplyAlign <- function(boxes2align, ref_positions, position, axis = c("vertical", "horizontal")) {
axis <- match.arg(axis)
Expand Down
6 changes: 4 additions & 2 deletions R/boxGrobs_boxGrob.R
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ boxGrob <- function(label,
}
)
}
box_fn_bounds <- attr(rect, "box_fn_bounds")

Comment on lines +131 to 132
# Create text grob using the (possibly) adjusted padding
txt <- textGrob(
Expand Down Expand Up @@ -197,7 +198,8 @@ boxGrob <- function(label,
)

structure(gl,
coords = prCreateBoxCoordinates(viewport_data = vp_args),
viewport_data = vp_args
coords = prCreateBoxCoordinates(viewport_data = vp_args, box_fn_bounds = box_fn_bounds),
viewport_data = vp_args,
box_fn_bounds = box_fn_bounds
)
}
Loading
Loading