Skip to content
Open
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
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Gmisc/inst/extdata/Full_test_suite_files/*
^.github$
^.github/
^docs/
^README\.Rmd$
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
any::bench
any::vdiffr
- name: Check package
run: Rscript -e 'devtools::check(max_errors = 5)'
run: Rscript -e 'devtools::check()'
- name: Run lintr
run: Rscript -e 'lintr::lint_dir("R")'
- name: Run tests with coverage
Expand Down
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: Gmisc
Version: 3.3.0
Version: 3.4.0
Title: Descriptive Statistics, Transition Plots, and More
Authors@R: c(
person(given = "Max", family = "Gordon",
Expand Down Expand Up @@ -46,7 +46,8 @@ Suggests:
dplyr,
jsonlite,
testthat,
tidyselect
tidyselect,
webshot2
Encoding: UTF-8
NeedsCompilation: yes
LinkingTo: Rcpp
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ S3method(latex,descMrg)
S3method(length,Gmisc_getDescriptionStatsBy)
S3method(move,Gmisc_list_of_boxes)
S3method(move,default)
S3method(phaseLabel,Gmisc_list_of_boxes)
S3method(phaseLabel,default)
S3method(plot,box)
S3method(plot,connect_boxes)
S3method(plot,connect_boxes_list)
Expand Down Expand Up @@ -106,6 +108,7 @@ export(mergeLists)
export(move)
export(moveBox)
export(pathJoin)
export(phaseLabel)
export(rack_box_fn)
export(retrieve)
export(server_box_fn)
Expand Down
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
NEWS for the Gmisc package

## Changes for 3.4.0

- Added `phaseLabel()` for flowchart box lists: a one-call-per-stage helper that adds a CONSORT-style phase label (e.g. *Allocation*, *Follow-up*, *Analysis*) centred between a stage's arms, sitting slightly above it and drawn on top. The label width adapts to the stage — spanning the central gap (plus a small corner overlap) for two arms, or the full stage width as a banner for three or more arms — and can be set explicitly via `width`. It correctly handles nested arm lists (arms that are themselves lists of boxes) by resolving their merged bounding box.
- Added `on_top` to `insert()` for flowchart box lists. A box inserted with `insert(..., on_top = TRUE)` is drawn on top of the other boxes and connections, regardless of its position in the list, and the marker is preserved through subsequent `move()`/`align()` operations. This is the lower-level overlay mechanism that `phaseLabel()` builds on.
- `insert()` now resolves grouped (list) neighbours via their merged bounding box, so a box can be inserted between grouped stages without error.
- Added a CONSORT phase-label example to the grid-based flowchart vignette showing `phaseLabel()` centred between randomisation arms.
- Updated flowchart examples to emphasize the `flowchart()` + pipe (`|>`) style API in `inst/examples/connectGrob_example.R`, `inst/examples/spreadBox_ex.R`, and `inst/examples/alignBox_ex.R`.
- Clarified spread/connect documentation to state that spread/align return updated objects (no in-place mutation) and that connectors should use the returned boxes.
- Improved interactive example ergonomics by pausing between graph pages in multi-plot examples.

## Changes for 3.3.0

- Added `equalizeWidths()` for flowchart box lists, allowing selected boxes (including nested `subelement` paths and list-of-boxes targets) to share a common width while preserving center positions.
Expand Down
7 changes: 7 additions & 0 deletions R/boxGrobs_connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
#' When connecting to or from a `boxPropGrob`, `subelmnt` controls whether the left
#' or right sub-box x-coordinate is used as the anchor point.
#'
#' ## Using with spread/align
#'
#' `connectGrob()` always uses coordinates from the `start` and `end` objects
#' you pass in. If those objects were positioned with `spread*()`/`align*()`,
#' pass the returned objects (for example by assigning the spread/align result)
#' rather than the original pre-spread variables.
#'
#' @param start A `boxGrob`/`boxPropGrob`, or a list of boxes (many-to-one).
#' @param end A `boxGrob`/`boxPropGrob`, or a list of boxes (one-to-many).
#' @param type Connector type, see Details.
Expand Down
20 changes: 18 additions & 2 deletions R/boxGrobs_print.Gmisc_list_of_boxes.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
#'
#' @param x A list of a set of [`boxGrob`]/[`boxPropGrob`] to plot
#' @param ... Ignored argument
#'
#' @details Boxes marked with the `draw_on_top` attribute (for example via
#' `insert(..., on_top = TRUE)`) are drawn last — after the other boxes and
#' after any stored connections — so they remain visible even when they
#' overlap surrounding boxes. All other boxes keep their list order.
#' @export
print.Gmisc_list_of_boxes <- function(x, ...) {
# Draw boxes
for (box in x) {
draw_element <- function(box) {
if (is.grob(box)) {
grid.draw(box)
} else if (inherits(box, "Gmisc_list_of_boxes")) {
Expand All @@ -23,6 +27,13 @@ print.Gmisc_list_of_boxes <- function(x, ...) {
}
}

on_top <- vapply(x, function(box) isTRUE(attr(box, "draw_on_top")), logical(1))

# Draw regular boxes in list order
for (box in x[!on_top]) {
draw_element(box)
}

# Draw stored connections
conns <- attr(x, "connections")
if (!is.null(conns)) {
Expand All @@ -31,5 +42,10 @@ print.Gmisc_list_of_boxes <- function(x, ...) {
}
}

# Draw overlay boxes last so they stay on top of boxes and connections
for (box in x[on_top]) {
draw_element(box)
}

invisible(x)
}
40 changes: 34 additions & 6 deletions R/boxGrobs_s3_append.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,32 @@ append.Gmisc_list_of_boxes <- function(x, values, after = length(x)) {
#' @param ... Not used.
#' @param after The name or index of the box after which to insert.
#' @param before The name or index of the box before which to insert.
#' @param on_top If `TRUE` the inserted box is marked to be drawn *on top* of the
#' other boxes (after them and after any connections), regardless of where it
#' sits in the list. This is useful for overlay boxes such as CONSORT-style
#' stage headings that should remain visible even when they overlap the
#' surrounding boxes. The marker is preserved through subsequent
#' [`move`]/[`align`] operations.
#'
#' @return The updated list of boxes with the new element inserted.
#' @export
#' @family flowchart components
insert <- function(x, element, ..., after = NULL, before = NULL) {
insert <- function(x, element, ..., after = NULL, before = NULL, on_top = FALSE) {
UseMethod("insert")
}

#' @export
#' @rdname insert
insert.default <- function(x, element, ..., after = NULL, before = NULL) {
insert.default <- function(x, element, ..., after = NULL, before = NULL, on_top = FALSE) {
if (is.list(x) && !inherits(x, "box")) {
return(insert(prConvertListToBoxList(x), element, ..., after = after, before = before))
return(insert(prConvertListToBoxList(x), element, ..., after = after, before = before, on_top = on_top))
}
stop("insert() expects a list of boxes as first argument")
}

#' @export
#' @rdname insert
insert.Gmisc_list_of_boxes <- function(x, element, ..., after = NULL, before = NULL) {
insert.Gmisc_list_of_boxes <- function(x, element, ..., after = NULL, before = NULL, on_top = FALSE) {
if (!xor(is.null(after), is.null(before))) {
stop("You must specify either 'after' or 'before' (but not both).")
}
Expand All @@ -83,6 +89,10 @@ insert.Gmisc_list_of_boxes <- function(x, element, ..., after = NULL, before = N
stop("inserted element must be a box")
}

if (isTRUE(on_top)) {
attr(element, "draw_on_top") <- TRUE
}

# Find insertion index
idx <- NULL
if (!is.null(after)) {
Expand Down Expand Up @@ -116,8 +126,26 @@ insert.Gmisc_list_of_boxes <- function(x, element, ..., after = NULL, before = N

if (!is.null(prev_box) && !is.null(next_box)) {
# Position between
pc <- coords(prev_box)
nc <- coords(next_box)
pc <- tryCatch(
prConvert2Coords(prev_box),
error = function(e) {
stop(
"insert() could not determine coordinates for the previous flowchart element: ",
conditionMessage(e),
call. = FALSE
)
}
)
nc <- tryCatch(
prConvert2Coords(next_box),
error = function(e) {
stop(
"insert() could not determine coordinates for the next flowchart element: ",
conditionMessage(e),
call. = FALSE
)
}
)

# Determine orientation?
# Heuristic: mostly different X -> horizontal split
Expand Down
126 changes: 126 additions & 0 deletions R/boxGrobs_s3_phaseLabel.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#' Add a phase label to a flowchart stage (S3)
#'
#' Adds a label box that sits just above a *stage* and is centred between that
#' stage's arms, drawn on top of the surrounding boxes. This is the typical
#' CONSORT "phase" banner such as *Allocation*, *Follow-up* and *Analysis* that
#' spans the randomisation arms.
#'
#' Unlike [`insert`], the label is positioned relative to the stage it labels
#' (`reference`), not relative to a neighbouring element, and it is marked to be
#' drawn on top (see `on_top` in [`insert`]) so it stays visible even where it
#' overlaps the stage boxes.
#'
#' @param x A `Gmisc_list_of_boxes` (or a `list` of boxes, which is converted).
#' @param reference The name or index of the **stage to label**. The stage may be
#' a single [`boxGrob`] or a `list` of arm boxes.
#' @param label The label, either a string (wrapped with [`boxGrob`]) or a
#' pre-built [`boxGrob`]/[`boxPropGrob`].
#' @param ... Passed on to [`boxGrob`] when `label` is a string (e.g. `box_gp`,
#' `txt_gp`).
#' @param width Optional fixed label width as a [`unit`][grid::unit] (a numeric is
#' treated as millimetres). When `NULL` (default) the width is derived from the
#' stage: for a two-arm stage the label spans the gap between the arms plus
#' `overlap` of each inner top corner; for a single- or 3+-arm stage the label
#' spans the full width of the stage (a banner across all arms).
#' @param overlap How far the label overlaps the stage, as a fraction
#' (default `0.07`, i.e. ~7%). It controls the vertical dip below the stage's top
#' edge and, for the derived two-arm width, the horizontal corner lap.
#' @param name Name for the inserted label element. Defaults to
#' `paste0(reference, "_label")`.
#'
#' @return The updated `Gmisc_list_of_boxes` with the label added after the
#' referenced stage.
#' @seealso [`insert`] for the general `on_top` overlay mechanism this builds on.
#' @export
#' @family flowchart components
#' @examples
#' library(grid)
#' grid.newpage()
#'
#' flowchart(
#' rando = boxGrob("Randomised\nN = 100", x = .5, y = .8),
#' arms = list(
#' boxGrob("Intervention\nn = 50", x = .3, y = .4),
#' boxGrob("Control\nn = 50", x = .7, y = .4)
#' )
#' ) |>
#' phaseLabel("arms", "Allocation", box_gp = gpar(fill = "#c8daf7")) |>
#' connect("rando", "arms", type = "N") |>
#' print()
phaseLabel <- function(x, ...) {
UseMethod("phaseLabel")
}

#' @export
#' @rdname phaseLabel
phaseLabel.default <- function(x, ...) {
if (is.list(x) && !inherits(x, "box")) {
return(phaseLabel(prConvertListToBoxList(x), ...))
}
stop("phaseLabel() requires a list of boxes (Gmisc_list_of_boxes).")
}

#' @export
#' @rdname phaseLabel
phaseLabel.Gmisc_list_of_boxes <- function(x, reference, label, ..., width = NULL, overlap = 0.07, name = NULL) {
# Resolve the referenced stage to a list index
idx <- if (is.character(reference)) match(reference, names(x)) else as.integer(reference)
if (length(idx) != 1 || is.na(idx) || idx < 1 || idx > length(x)) {
stop("phaseLabel(): could not find stage '", reference, "' in the flowchart.", call. = FALSE)
}
stage <- x[[idx]]

# Stage geometry (bounding box of all arms) and arm count
rc <- prConvert2Coords(stage)
n_arms <- if (inherits(stage, "box") || !is.list(stage)) 1L else length(stage)

# Build the label box
lab <- if (inherits(label, "box")) label else boxGrob(label, ...)

to_npc_x <- function(u) convertX(u, "npc", valueOnly = TRUE)
to_npc_y <- function(u) convertY(u, "npc", valueOnly = TRUE)
to_npc_w <- function(u) convertWidth(u, "npc", valueOnly = TRUE)
to_npc_h <- function(u) convertHeight(u, "npc", valueOnly = TRUE)

auto_w <- to_npc_w(coords(lab)$width)

# Determine the target width (in npc)
if (!is.null(width)) {
if (is.numeric(width)) width <- unit(width, "mm")
if (!is.unit(width)) stop("`width` must be a unit or numeric.", call. = FALSE)
target_w <- to_npc_w(width)
} else if (n_arms == 2) {
# Narrow label spanning the central gap plus a corner lap on each inner edge
c1 <- prConvert2Coords(stage[[1]])
c2 <- prConvert2Coords(stage[[2]])
x1 <- to_npc_x(c1$x)
x2 <- to_npc_x(c2$x)
left_coords <- if (x1 <= x2) c1 else c2
right_coords <- if (x1 <= x2) c2 else c1
gap <- to_npc_x(right_coords$left) - to_npc_x(left_coords$right)
arm_w <- to_npc_w(left_coords$width)
target_w <- max(gap + 2 * overlap * arm_w, auto_w)
} else {
# Banner spanning the full stage width
target_w <- max(to_npc_w(rc$width), auto_w)
}

# Apply the width (no-op if it already matches the box)
lab <- prSetBoxDimensions(lab, width = unit(target_w, "npc"))
attr(lab, "draw_on_top") <- TRUE

# Position: centred on the stage, bottom edge dipping `overlap` into the top
dip <- overlap * to_npc_h(rc$height)
half_h <- to_npc_h(coords(lab)$half_height)
new_y <- to_npc_y(rc$top) - dip + half_h
lab <- moveBox(lab, x = rc$x, y = unit(new_y, "npc"), space = "absolute", just = "center")

# Name and append after the stage (without repositioning, unlike insert())
if (is.null(name)) {
ref_name <- if (is.character(reference)) reference else names(x)[idx]
name <- if (is.null(ref_name) || !nzchar(ref_name)) paste0("phase_", idx) else paste0(ref_name, "_label")
}
to_ins <- list(lab)
names(to_ins) <- name
append(x, to_ins, after = idx)
}
6 changes: 6 additions & 0 deletions R/boxGrobs_spread.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
#' The function will return the original list with the targeted element(s)
#' replaced by their spread version(s).
#'
#' @details
#' `spreadVertical()`/`spreadHorizontal()` return updated box objects. They do
#' not mutate box objects that are already bound to variables. To use the new
#' coordinates in subsequent operations (for example `connectGrob()`), assign
#' the result and use those returned boxes.
#'
#' @return A `list` with the boxes that have been spread.
#'
#' @md
Expand Down
Loading
Loading