diff --git a/.Rbuildignore b/.Rbuildignore
index cdcd10c..ce92312 100644
--- a/.Rbuildignore
+++ b/.Rbuildignore
@@ -26,3 +26,4 @@ Gmisc/inst/extdata/Full_test_suite_files/*
^.github$
^.github/
^docs/
+^README\.Rmd$
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 01a158f..81f44b0 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -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
diff --git a/DESCRIPTION b/DESCRIPTION
index 595c2dc..6791017 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -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",
@@ -46,7 +46,8 @@ Suggests:
dplyr,
jsonlite,
testthat,
- tidyselect
+ tidyselect,
+ webshot2
Encoding: UTF-8
NeedsCompilation: yes
LinkingTo: Rcpp
diff --git a/NAMESPACE b/NAMESPACE
index 51d07ab..bc64e3e 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -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)
@@ -106,6 +108,7 @@ export(mergeLists)
export(move)
export(moveBox)
export(pathJoin)
+export(phaseLabel)
export(rack_box_fn)
export(retrieve)
export(server_box_fn)
diff --git a/NEWS.md b/NEWS.md
index 5ea42a7..67465a2 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -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.
diff --git a/R/boxGrobs_connect.R b/R/boxGrobs_connect.R
index f106fc0..2b95c90 100644
--- a/R/boxGrobs_connect.R
+++ b/R/boxGrobs_connect.R
@@ -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.
diff --git a/R/boxGrobs_print.Gmisc_list_of_boxes.R b/R/boxGrobs_print.Gmisc_list_of_boxes.R
index 44118e5..9e67451 100644
--- a/R/boxGrobs_print.Gmisc_list_of_boxes.R
+++ b/R/boxGrobs_print.Gmisc_list_of_boxes.R
@@ -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")) {
@@ -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)) {
@@ -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)
}
diff --git a/R/boxGrobs_s3_append.R b/R/boxGrobs_s3_append.R
index 4a32005..ec79d92 100644
--- a/R/boxGrobs_s3_append.R
+++ b/R/boxGrobs_s3_append.R
@@ -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).")
}
@@ -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)) {
@@ -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
diff --git a/R/boxGrobs_s3_phaseLabel.R b/R/boxGrobs_s3_phaseLabel.R
new file mode 100644
index 0000000..5d25cbb
--- /dev/null
+++ b/R/boxGrobs_s3_phaseLabel.R
@@ -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)
+}
diff --git a/R/boxGrobs_spread.R b/R/boxGrobs_spread.R
index fc98680..47e7f05 100644
--- a/R/boxGrobs_spread.R
+++ b/R/boxGrobs_spread.R
@@ -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
diff --git a/README.Rmd b/README.Rmd
new file mode 100644
index 0000000..25d5a74
--- /dev/null
+++ b/README.Rmd
@@ -0,0 +1,228 @@
+---
+output: github_document
+---
+
+
+
+```{r setup, include = FALSE}
+knitr::opts_chunk$set(
+ collapse = TRUE,
+ comment = "#>",
+ fig.path = "man/figures/README-",
+ out.width = "100%",
+ dpi = 150,
+ message = FALSE,
+ warning = FALSE
+)
+options(boxGrobTxtPadding = grid::unit(2, "mm"))
+```
+
+# Gmisc
+
+
+[](https://CRAN.R-project.org/package=Gmisc)
+[](https://CRAN.R-project.org/package=Gmisc)
+[](https://github.com/gforge/Gmisc/actions/workflows/ci.yaml)
+
+
+**Gmisc** collects utilities for the graphics and tables that recur in medical
+research papers — built so they compose with the native R pipe (`|>`):
+
+- **Descriptive "Table 1"** — `getDescriptionStatsBy()` + `htmlTable()` for
+ publication-ready, copy-paste descriptive tables.
+- **Grid-based flowcharts** — a `flowchart() |> spread() |> move() |> connect()`
+ pipeline for CONSORT-style diagrams, including `phaseLabel()` headings.
+- **Transition plots** — the `Transition` class for visualising how
+ observations move between categories over time.
+- **SVD variable selection** — `getSvdMostInfluential()` for picking influential
+ variables.
+- **Bézier arrows** — smooth arrows complementing the `grid` package.
+
+## Installation
+
+```r
+# From CRAN
+install.packages("Gmisc")
+
+# Development version from GitHub
+# install.packages("remotes")
+remotes::install_github("gforge/Gmisc")
+```
+
+## Flowcharts & CONSORT diagrams
+
+Build a flowchart as a named list of boxes, position the columns with
+`spread()`/`move()`, then add the connectors. Parallel arms are just lists, and
+`phaseLabel()` drops a CONSORT phase heading ("Allocation", "Follow-up", …)
+between the arms — centred, slightly overlapping, and drawn on top.
+
+```{r flowchart, fig.width = 8, fig.height = 7}
+library(Gmisc)
+library(grid)
+
+main_gp <- gpar(fill = "white", col = "black", lwd = 1)
+head_gp <- gpar(fill = "#c8daf7", col = "#2f5f9f", lwd = 1)
+con_gp <- gpar(col = "#4f86c6", fill = "#4f86c6", lwd = 1.8)
+sw <- unit(70, "mm")
+
+flowchart(
+ rando = boxGrob("Randomised\nN = 100", box_gp = main_gp),
+ groups = list(
+ boxGrob("Allocated to intervention\nn = 50", width = sw, box_gp = main_gp),
+ boxGrob("Allocated to control\nn = 50", width = sw, box_gp = main_gp)
+ ),
+ followup = list(
+ boxGrob("Lost to follow-up\nn = 1", width = sw, box_gp = main_gp),
+ boxGrob("Lost to follow-up\nn = 2", width = sw, box_gp = main_gp)
+ ),
+ analysis = list(
+ boxGrob("Analysed\nn = 49", width = sw, box_gp = main_gp),
+ boxGrob("Analysed\nn = 48", width = sw, box_gp = main_gp)
+ )
+) |>
+ spread(axis = "y", margin = unit(0.04, "npc")) |>
+ move(subelement = list(c("groups", 1), c("followup", 1), c("analysis", 1)), x = 0.27) |>
+ move(subelement = list(c("groups", 2), c("followup", 2), c("analysis", 2)), x = 0.73) |>
+ phaseLabel("groups", "Allocation", box_gp = head_gp) |>
+ phaseLabel("followup", "Follow-up", box_gp = head_gp) |>
+ phaseLabel("analysis", "Analysis", box_gp = head_gp) |>
+ connect("rando", "groups", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
+ connect("groups", "followup", type = "v", lty_gp = con_gp, arrow_size = 3) |>
+ connect("followup", "analysis", type = "v", lty_gp = con_gp, arrow_size = 3)
+```
+
+See `vignette("Grid-based_flowcharts", package = "Gmisc")` for the full API.
+
+## Descriptive "Table 1"
+
+`getDescriptionStatsBy()` summarises variables split by a grouping column. It is
+often used with `mergeDesc()` to group related variables into sections, and
+pipes straight into `htmlTable()` for a publication-ready table.
+
+```{r table1-code, eval = FALSE}
+library(dplyr)
+library(Gmisc)
+
+# A custom wrapper to keep statistics and formatting consistent
+# (e.g., same digits, p-values, and header count)
+get_stats <- function(data, ...) {
+ res <- data |>
+ getDescriptionStatsBy(...,
+ by = am,
+ statistics = TRUE,
+ digits = 1,
+ header_count = TRUE)
+ if (is.list(res)) {
+ return(do.call(rbind, res))
+ }
+ return(res)
+}
+
+mtcars_prep <- mtcars |>
+ mutate(am = factor(am, labels = c("Automatic", "Manual")),
+ gear = factor(gear),
+ cyl = factor(cyl)) |>
+ set_column_labels(mpg = "Gas",
+ wt = "Weight",
+ hp = "Horsepower",
+ cyl = "Cylinders",
+ gear = "Gears") |>
+ set_column_units(mpg = "Miles/gallon",
+ wt = "103 lbs",
+ hp = "hp")
+
+# Group variables and merge them into a single table
+mergeDesc(
+ "Main" = mtcars_prep |> get_stats(mpg, wt),
+ "Engine" = mtcars_prep |> get_stats(hp, cyl),
+ "Transmission" = mtcars_prep |> get_stats(gear)
+) |>
+ htmlTable(caption = "Baseline characteristics by transmission",
+ tfoot = "† Statistics: Mean (SD) for continuous; n (%) for categorical")
+```
+
+```{r table1-snapshot, include = FALSE}
+library(dplyr)
+library(Gmisc)
+
+get_stats <- function(data, ...) {
+ res <- data |>
+ getDescriptionStatsBy(...,
+ by = am,
+ statistics = TRUE,
+ digits = 1,
+ header_count = TRUE)
+ if (is.list(res)) {
+ return(do.call(rbind, res))
+ }
+ return(res)
+}
+
+mtcars_prep <- mtcars |>
+ mutate(am = factor(am, labels = c("Automatic", "Manual")),
+ gear = factor(gear),
+ cyl = factor(cyl)) |>
+ set_column_labels(mpg = "Gas",
+ wt = "Weight",
+ hp = "Horsepower",
+ cyl = "Cylinders",
+ gear = "Gears") |>
+ set_column_units(mpg = "Miles/gallon",
+ wt = "103 lbs",
+ hp = "hp")
+
+tab <- mergeDesc(
+ "Main" = mtcars_prep |> get_stats(mpg, wt),
+ "Engine" = mtcars_prep |> get_stats(hp, cyl),
+ "Transmission" = mtcars_prep |> get_stats(gear)
+) |>
+ htmlTable(caption = "Baseline characteristics by transmission",
+ tfoot = "† Statistics: Mean (SD) for continuous; n (%) for categorical")
+
+if (!dir.exists("man/figures")) dir.create("man/figures", recursive = TRUE)
+table_html <- paste(capture.output(print(tab)), collapse = "\n")
+html_file <- tempfile(fileext = ".html")
+writeLines(
+ paste0("
",
+ "",
+ table_html, ""),
+ html_file
+)
+invisible(webshot2::webshot(html_file, "man/figures/README-table1.png", selector = "table", zoom = 1.5))
+```
+
+```{r table1-img, echo = FALSE, out.width = "70%"}
+knitr::include_graphics("man/figures/README-table1.png")
+```
+
+See `vignette("Descriptives", package = "Gmisc")` for the many formatting options.
+
+## Transition plots
+
+The `Transition` class shows how observations move between classes over time;
+a third dimension can be encoded as a colour split within each box.
+
+```{r transition, fig.width = 6, fig.height = 6, out.width = "70%"}
+set.seed(1)
+n <- 100
+sex <- sample(c("Male", "Female"), n, replace = TRUE)
+before <- sample(1:3, n, replace = TRUE)
+# Most cases improve one class, some stay, a few worsen
+after <- pmin(pmax(before - sample(c(-1, 0, 1), n, replace = TRUE, prob = c(.15, .35, .5)), 1), 3)
+
+lbl <- c("A", "B", "C")
+tbl <- table(factor(before, 1:3, lbl), factor(after, 1:3, lbl), sex)
+
+transitions <- getRefClass("Transition")$new(tbl, label = c("Before surgery", "1 year after"))
+transitions$title <- "Charnley class before vs. after surgery"
+transitions$clr_bar <- "bottom"
+transitions$render()
+```
+
+See `vignette("Transition-class", package = "Gmisc")` for customisation.
+
+## Learn more
+
+- Project page:
+- Vignettes: `browseVignettes("Gmisc")`
+- Bugs & feature requests:
diff --git a/README.html b/README.html
new file mode 100644
index 0000000..085cc3e
--- /dev/null
+++ b/README.html
@@ -0,0 +1,760 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Gmisc
+
+
+

+
+
+Gmisc collects utilities for the graphics and tables
+that recur in medical research papers — built so they compose with the
+native R pipe (|>):
+
+- Descriptive “Table 1” —
+
getDescriptionStatsBy() + htmlTable() for
+publication-ready, copy-paste descriptive tables.
+- Grid-based flowcharts — a
+
flowchart() |> spread() |> move() |> connect()
+pipeline for CONSORT-style diagrams, including phaseLabel()
+headings.
+- Transition plots — the
Transition
+class for visualising how observations move between categories over
+time.
+- SVD variable selection —
+
getSvdMostInfluential() for picking influential
+variables.
+- Bézier arrows — smooth arrows complementing the
+
grid package.
+
+Installation
+# From CRAN
+install.packages("Gmisc")
+
+# Development version from GitHub
+# install.packages("remotes")
+remotes::install_github("gforge/Gmisc")
+Flowcharts & CONSORT
+diagrams
+Build a flowchart as a named list of boxes, position the columns with
+spread()/move(), then add the connectors.
+Parallel arms are just lists, and phaseLabel() drops a
+CONSORT phase heading (“Allocation”, “Follow-up”, …) between the arms —
+centred, slightly overlapping, and drawn on top.
+library(Gmisc)
+library(grid)
+
+main_gp <- gpar(fill = "white", col = "black", lwd = 1)
+head_gp <- gpar(fill = "#c8daf7", col = "#2f5f9f", lwd = 1)
+con_gp <- gpar(col = "#4f86c6", fill = "#4f86c6", lwd = 1.8)
+sw <- unit(70, "mm")
+
+flowchart(
+ rando = boxGrob("Randomised\nN = 100", box_gp = main_gp),
+ groups = list(
+ boxGrob("Allocated to intervention\nn = 50", width = sw, box_gp = main_gp),
+ boxGrob("Allocated to control\nn = 50", width = sw, box_gp = main_gp)
+ ),
+ followup = list(
+ boxGrob("Lost to follow-up\nn = 1", width = sw, box_gp = main_gp),
+ boxGrob("Lost to follow-up\nn = 2", width = sw, box_gp = main_gp)
+ ),
+ analysis = list(
+ boxGrob("Analysed\nn = 49", width = sw, box_gp = main_gp),
+ boxGrob("Analysed\nn = 48", width = sw, box_gp = main_gp)
+ )
+) |>
+ spread(axis = "y", margin = unit(0.04, "npc")) |>
+ move(subelement = list(c("groups", 1), c("followup", 1), c("analysis", 1)), x = 0.27) |>
+ move(subelement = list(c("groups", 2), c("followup", 2), c("analysis", 2)), x = 0.73) |>
+ phaseLabel("groups", "Allocation", box_gp = head_gp) |>
+ phaseLabel("followup", "Follow-up", box_gp = head_gp) |>
+ phaseLabel("analysis", "Analysis", box_gp = head_gp) |>
+ connect("rando", "groups", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
+ connect("groups", "followup", type = "v", lty_gp = con_gp, arrow_size = 3) |>
+ connect("followup", "analysis", type = "v", lty_gp = con_gp, arrow_size = 3)
+
+
+See vignette("Grid-based_flowcharts", package = "Gmisc")
+for the full API.
+Descriptive “Table 1”
+getDescriptionStatsBy() summarises variables split by a
+grouping column. It is often used with mergeDesc() to group
+related variables into sections, and pipes straight into
+htmlTable() for a publication-ready table.
+library(dplyr)
+library(Gmisc)
+
+# A custom wrapper to keep statistics and formatting consistent
+# (e.g., same digits, p-values, and header count)
+get_stats <- function(data, ...) {
+ res <- data |>
+ getDescriptionStatsBy(...,
+ by = am,
+ statistics = TRUE,
+ digits = 1,
+ header_count = TRUE)
+ if (is.list(res)) {
+ return(do.call(rbind, res))
+ }
+ return(res)
+}
+
+mtcars_prep <- mtcars |>
+ mutate(am = factor(am, labels = c("Automatic", "Manual")),
+ gear = factor(gear),
+ cyl = factor(cyl)) |>
+ set_column_labels(mpg = "Gas",
+ wt = "Weight",
+ hp = "Horsepower",
+ cyl = "Cylinders",
+ gear = "Gears") |>
+ set_column_units(mpg = "Miles/gallon",
+ wt = "10<sup>3</sup> lbs",
+ hp = "hp")
+
+# Group variables and merge them into a single table
+mergeDesc(
+ "Main" = mtcars_prep |> get_stats(mpg, wt),
+ "Engine" = mtcars_prep |> get_stats(hp, cyl),
+ "Transmission" = mtcars_prep |> get_stats(gear)
+) |>
+ htmlTable(caption = "Baseline characteristics by transmission",
+ tfoot = "† Statistics: Mean (SD) for continuous; n (%) for categorical")
+
+
+See vignette("Descriptives", package = "Gmisc") for the
+many formatting options.
+Transition plots
+The Transition class shows how observations move between
+classes over time; a third dimension can be encoded as a colour split
+within each box.
+set.seed(1)
+n <- 100
+sex <- sample(c("Male", "Female"), n, replace = TRUE)
+before <- sample(1:3, n, replace = TRUE)
+# Most cases improve one class, some stay, a few worsen
+after <- pmin(pmax(before - sample(c(-1, 0, 1), n, replace = TRUE, prob = c(.15, .35, .5)), 1), 3)
+
+lbl <- c("A", "B", "C")
+tbl <- table(factor(before, 1:3, lbl), factor(after, 1:3, lbl), sex)
+
+transitions <- getRefClass("Transition")$new(tbl, label = c("Before surgery", "1 year after"))
+transitions$title <- "Charnley class before vs. after surgery"
+transitions$clr_bar <- "bottom"
+transitions$render()
+
+
+See vignette("Transition-class", package = "Gmisc") for
+customisation.
+Learn more
+
+
+
+
diff --git a/README.md b/README.md
index 1a60554..317a58b 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,174 @@
-[](https://cran.r-project.org/package=Gmisc)
-[](https://cran.r-project.org/package=Gmisc)
-# The Gmisc package
+
-Tools for making:
+# Gmisc
-- the descriptive "Table 1" used in medical articles
-- transition plot for showing changes between categories
-- flow charts based on the grid package
-- a method for variable selection based on the SVD
-- Bézier lines with arrows complementing the ones in the 'grid' package, and more.
+
+
+[](https://CRAN.R-project.org/package=Gmisc)
+[](https://CRAN.R-project.org/package=Gmisc)
+[](https://github.com/gforge/Gmisc/actions/workflows/ci.yaml)
+
+
+**Gmisc** collects utilities for the graphics and tables that recur in
+medical research papers — built so they compose with the native R pipe
+(`|>`):
+
+- **Descriptive “Table 1”** — `getDescriptionStatsBy()` + `htmlTable()`
+ for publication-ready, copy-paste descriptive tables.
+- **Grid-based flowcharts** — a
+ `flowchart() |> spread() |> move() |> connect()` pipeline for
+ CONSORT-style diagrams, including `phaseLabel()` headings.
+- **Transition plots** — the `Transition` class for visualising how
+ observations move between categories over time.
+- **SVD variable selection** — `getSvdMostInfluential()` for picking
+ influential variables.
+- **Bézier arrows** — smooth arrows complementing the `grid` package.
+
+## Installation
+
+``` r
+# From CRAN
+install.packages("Gmisc")
+
+# Development version from GitHub
+# install.packages("remotes")
+remotes::install_github("gforge/Gmisc")
+```
+
+## Flowcharts & CONSORT diagrams
+
+Build a flowchart as a named list of boxes, position the columns with
+`spread()`/`move()`, then add the connectors. Parallel arms are just
+lists, and `phaseLabel()` drops a CONSORT phase heading (“Allocation”,
+“Follow-up”, …) between the arms — centred, slightly overlapping, and
+drawn on top.
+
+``` r
+library(Gmisc)
+library(grid)
+
+main_gp <- gpar(fill = "white", col = "black", lwd = 1)
+head_gp <- gpar(fill = "#c8daf7", col = "#2f5f9f", lwd = 1)
+con_gp <- gpar(col = "#4f86c6", fill = "#4f86c6", lwd = 1.8)
+sw <- unit(70, "mm")
+
+flowchart(
+ rando = boxGrob("Randomised\nN = 100", box_gp = main_gp),
+ groups = list(
+ boxGrob("Allocated to intervention\nn = 50", width = sw, box_gp = main_gp),
+ boxGrob("Allocated to control\nn = 50", width = sw, box_gp = main_gp)
+ ),
+ followup = list(
+ boxGrob("Lost to follow-up\nn = 1", width = sw, box_gp = main_gp),
+ boxGrob("Lost to follow-up\nn = 2", width = sw, box_gp = main_gp)
+ ),
+ analysis = list(
+ boxGrob("Analysed\nn = 49", width = sw, box_gp = main_gp),
+ boxGrob("Analysed\nn = 48", width = sw, box_gp = main_gp)
+ )
+) |>
+ spread(axis = "y", margin = unit(0.04, "npc")) |>
+ move(subelement = list(c("groups", 1), c("followup", 1), c("analysis", 1)), x = 0.27) |>
+ move(subelement = list(c("groups", 2), c("followup", 2), c("analysis", 2)), x = 0.73) |>
+ phaseLabel("groups", "Allocation", box_gp = head_gp) |>
+ phaseLabel("followup", "Follow-up", box_gp = head_gp) |>
+ phaseLabel("analysis", "Analysis", box_gp = head_gp) |>
+ connect("rando", "groups", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
+ connect("groups", "followup", type = "v", lty_gp = con_gp, arrow_size = 3) |>
+ connect("followup", "analysis", type = "v", lty_gp = con_gp, arrow_size = 3)
+```
+
+
+
+See `vignette("Grid-based_flowcharts", package = "Gmisc")` for the full
+API.
+
+## Descriptive “Table 1”
+
+`getDescriptionStatsBy()` summarises variables split by a grouping
+column. It is often used with `mergeDesc()` to group related variables
+into sections, and pipes straight into `htmlTable()` for a
+publication-ready table.
+
+``` r
+library(dplyr)
+library(Gmisc)
+
+# A custom wrapper to keep statistics and formatting consistent
+# (e.g., same digits, p-values, and header count)
+get_stats <- function(data, ...) {
+ res <- data |>
+ getDescriptionStatsBy(...,
+ by = am,
+ statistics = TRUE,
+ digits = 1,
+ header_count = TRUE)
+ if (is.list(res)) {
+ return(do.call(rbind, res))
+ }
+ return(res)
+}
+
+mtcars_prep <- mtcars |>
+ mutate(am = factor(am, labels = c("Automatic", "Manual")),
+ gear = factor(gear),
+ cyl = factor(cyl)) |>
+ set_column_labels(mpg = "Gas",
+ wt = "Weight",
+ hp = "Horsepower",
+ cyl = "Cylinders",
+ gear = "Gears") |>
+ set_column_units(mpg = "Miles/gallon",
+ wt = "103 lbs",
+ hp = "hp")
+
+# Group variables and merge them into a single table
+mergeDesc(
+ "Main" = mtcars_prep |> get_stats(mpg, wt),
+ "Engine" = mtcars_prep |> get_stats(hp, cyl),
+ "Transmission" = mtcars_prep |> get_stats(gear)
+) |>
+ htmlTable(caption = "Baseline characteristics by transmission",
+ tfoot = "† Statistics: Mean (SD) for continuous; n (%) for categorical")
+```
+
+
+
+See `vignette("Descriptives", package = "Gmisc")` for the many
+formatting options.
+
+## Transition plots
+
+The `Transition` class shows how observations move between classes over
+time; a third dimension can be encoded as a colour split within each
+box.
+
+``` r
+set.seed(1)
+n <- 100
+sex <- sample(c("Male", "Female"), n, replace = TRUE)
+before <- sample(1:3, n, replace = TRUE)
+# Most cases improve one class, some stay, a few worsen
+after <- pmin(pmax(before - sample(c(-1, 0, 1), n, replace = TRUE, prob = c(.15, .35, .5)), 1), 3)
+
+lbl <- c("A", "B", "C")
+tbl <- table(factor(before, 1:3, lbl), factor(after, 1:3, lbl), sex)
+
+transitions <- getRefClass("Transition")$new(tbl, label = c("Before surgery", "1 year after"))
+transitions$title <- "Charnley class before vs. after surgery"
+transitions$clr_bar <- "bottom"
+transitions$render()
+```
+
+
+
+See `vignette("Transition-class", package = "Gmisc")` for customisation.
+
+## Learn more
+
+- Project page:
+- Vignettes: `browseVignettes("Gmisc")`
+- Bugs & feature requests:
diff --git a/inst/examples/alignBox_ex.R b/inst/examples/alignBox_ex.R
index cc83d07..eef6f8e 100644
--- a/inst/examples/alignBox_ex.R
+++ b/inst/examples/alignBox_ex.R
@@ -8,18 +8,18 @@ box <- boxGrob("A cool reference box",
)
# Create a group of boxes to align
-boxes <- list(
+boxes <- flowchart(
another_box = boxGrob("A horizontal box", x = .1, y = .5),
yet_another_box = boxGrob("Another horizontal box", x = .8, y = .3)
)
-# Align the group and then individual boxes within that group
-# (do not pipe the list into the function, as the first argument is `reference`)
-aligned_boxes <- alignHorizontal(boxes, reference = box, position = "right") |>
- alignVertical(reference = .5, position = "center")
+# Align the group in pipe style
+aligned_boxes <- boxes |>
+ align(axis = "x", reference = box, position = "right") |>
+ align(axis = "y", reference = .5, position = "center")
# Example: align a nested element inside a complex list using a deep path
-complex_list <- list(
+complex_list <- flowchart(
arms = list(
early = list(boxGrob("Early", x = .2, y = .4)),
late = list(boxGrob("Late", x = .8, y = .2))
@@ -32,7 +32,8 @@ complex_list <- list(
# Align the first detail element to the early arm by deep path
complex_list <- complex_list |>
- alignHorizontal(
+ align(
+ axis = "x",
reference = c("arms", "early"),
position = "center",
subelement = c("detail", 1)
diff --git a/inst/examples/connectGrob_example.R b/inst/examples/connectGrob_example.R
index adce31d..228eb3a 100644
--- a/inst/examples/connectGrob_example.R
+++ b/inst/examples/connectGrob_example.R
@@ -1,18 +1,31 @@
library(grid)
+
+# In interactive sessions, pause between pages produced by this example.
+if (interactive()) {
+ old_ask <- grDevices::devAskNewPage(TRUE)
+ on.exit(grDevices::devAskNewPage(old_ask), add = TRUE)
+}
+
grid.newpage()
-# Initiate the boxes that we want to connect
-boxes <- list(
+# Build a flowchart object
+boxes <- flowchart(
start = boxGrob("Top", x = .5, y = .8),
end = boxGrob("Bottom", x = .5, y = .2),
side = boxPropGrob("Side", "Left", "Right", prop = .3, x = .2, y = .8),
exclude = boxGrob("Exclude:\n - Too sick\n - Prev. surgery", x = .8, y = .5, just = "left")
)
-# Connect the boxes and print/plot them
-connectGrob(boxes$start, boxes$end, "vertical")
-connectGrob(boxes$start, boxes$side, "horizontal")
-connectGrob(boxes$start, boxes$exclude, "L")
+# Connect using the pipe-friendly S3 API
+boxes <- boxes |>
+ connect(from = "start", to = "end", type = "vertical") |>
+ connect(from = "start", to = "side", type = "horizontal") |>
+ connect(from = "start", to = "exclude", type = "L")
+
+print(boxes)
+
+# Start a fresh page for split-box connector examples.
+grid.newpage()
# We can also connect to/from lists
side_boxes <- list(
@@ -23,14 +36,43 @@ side_boxes <- list(
connectGrob(boxes$side, side_boxes$left, "v", "l")
connectGrob(boxes$side, side_boxes$right, "v", "r")
+# Start a fresh page for fan-in example.
+grid.newpage()
+
# Fan-in center example: multiple starts into one center bus and single trunk
-list(
- boxes$start,
- boxGrob("S2", x = .3, y = .7),
- boxGrob("S3", x = .7, y = .7)
+flowchart(
+ start = boxes$start,
+ S2 = boxGrob("S2", x = .3, y = .7),
+ S3 = boxGrob("S3", x = .7, y = .7),
+ end = boxes$end
) |>
- connectGrob(boxes$end, type = "fan_in_center")
+ connect(from = c("start", "S2", "S3"), to = "end", type = "fan_in_center") |>
+ print()
+
+# Start a fresh page for spread/assignment example.
+grid.newpage()
+
+# When using spread/align, use the returned objects for connectors
+visits <- flowchart(
+ visit1 = boxGrob("Visit 1", x = .1, y = .35),
+ visit2 = boxGrob("Visit 2", x = .2, y = .35),
+ visit3 = boxGrob("Visit 3", x = .8, y = .35)
+)
+
+# Incorrect pattern (no assignment): spread result is discarded
+visits |>
+ spread(axis = "x", from = .05, to = .95, type = "between")
+
+# This connector uses original coordinates because 'visits' was unchanged
+connectGrob(visits$visit1, visits$visit2, type = "horizontal")
+
+# Correct pattern: assign returned boxes and connect those
+visits_spread <- visits |>
+ spread(axis = "x", from = .05, to = .95, type = "between")
+connectGrob(visits_spread$visit1, visits_spread$visit2, type = "horizontal")
# Print the boxes
boxes
side_boxes
+visits
+visits_spread
diff --git a/inst/examples/spreadBox_ex.R b/inst/examples/spreadBox_ex.R
index e8c43c8..ba81a97 100644
--- a/inst/examples/spreadBox_ex.R
+++ b/inst/examples/spreadBox_ex.R
@@ -7,13 +7,14 @@ end <- boxGrob("Bottom", x = .5, y = .2)
side <- boxPropGrob("Side", "Left", "Right", prop = .3, x = .2, y = .8)
exclude <- boxGrob("Exclude:\n - Too sick\n - Prev. surgery", x = .8, y = .5, just = "left")
-# We can chain the spread operations and print the result
-spreadVertical(
+# We can chain the spread operations from a flowchart object
+flowchart(
start = start,
middle = list(side, exclude),
end = end
) |>
- spreadHorizontal(subelement = "middle", from = 0.2, to = 0.8)
+ spread(axis = "y") |>
+ spread(axis = "x", subelement = "middle", from = 0.2, to = 0.8)
# Use device-level paging in interactive sessions so users can inspect the first plot
# This will prompt before creating the next page; we restore the previous setting afterwards
@@ -27,5 +28,8 @@ if (interactive()) {
}
# Example: spread a nested subelement by deep path and print sequentially
-list(grp = list(middle = list(side, exclude))) |>
- spreadHorizontal(subelement = c("grp", "middle"), from = 0.2, to = 0.8)
+flowchart(
+ anchor = boxGrob("Anchor", x = .5, y = .15),
+ grp = list(middle = list(side, exclude))
+) |>
+ spread(axis = "x", subelement = c("grp", "middle"), from = 0.2, to = 0.8)
diff --git a/man/align.Rd b/man/align.Rd
index a4f8240..c5f1e92 100644
--- a/man/align.Rd
+++ b/man/align.Rd
@@ -80,18 +80,18 @@ box <- boxGrob("A cool reference box",
)
# Create a group of boxes to align
-boxes <- list(
+boxes <- flowchart(
another_box = boxGrob("A horizontal box", x = .1, y = .5),
yet_another_box = boxGrob("Another horizontal box", x = .8, y = .3)
)
-# Align the group and then individual boxes within that group
-# (do not pipe the list into the function, as the first argument is `reference`)
-aligned_boxes <- alignHorizontal(boxes, reference = box, position = "right") |>
- alignVertical(reference = .5, position = "center")
+# Align the group in pipe style
+aligned_boxes <- boxes |>
+ align(axis = "x", reference = box, position = "right") |>
+ align(axis = "y", reference = .5, position = "center")
# Example: align a nested element inside a complex list using a deep path
-complex_list <- list(
+complex_list <- flowchart(
arms = list(
early = list(boxGrob("Early", x = .2, y = .4)),
late = list(boxGrob("Late", x = .8, y = .2))
@@ -104,7 +104,8 @@ complex_list <- list(
# Align the first detail element to the early arm by deep path
complex_list <- complex_list |>
- alignHorizontal(
+ align(
+ axis = "x",
reference = c("arms", "early"),
position = "center",
subelement = c("detail", 1)
@@ -130,6 +131,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/append.Rd b/man/append.Rd
index 76a4527..a0dc326 100644
--- a/man/append.Rd
+++ b/man/append.Rd
@@ -41,6 +41,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/box.Rd b/man/box.Rd
index dacc62a..5927b83 100644
--- a/man/box.Rd
+++ b/man/box.Rd
@@ -126,6 +126,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/boxHeaderGrob.Rd b/man/boxHeaderGrob.Rd
index 7f25138..da260ee 100644
--- a/man/boxHeaderGrob.Rd
+++ b/man/boxHeaderGrob.Rd
@@ -102,6 +102,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/boxPropGrob.Rd b/man/boxPropGrob.Rd
index 483f29f..4ad69ca 100644
--- a/man/boxPropGrob.Rd
+++ b/man/boxPropGrob.Rd
@@ -98,6 +98,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/boxShapes.Rd b/man/boxShapes.Rd
index 4d7e4be..d79eb31 100644
--- a/man/boxShapes.Rd
+++ b/man/boxShapes.Rd
@@ -231,6 +231,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/connect.Rd b/man/connect.Rd
index b3e2787..8791cb6 100644
--- a/man/connect.Rd
+++ b/man/connect.Rd
@@ -182,23 +182,44 @@ the label away from the connector.
When connecting to or from a \code{boxPropGrob}, \code{subelmnt} controls whether the left
or right sub-box x-coordinate is used as the anchor point.
}
+
+\subsection{Using with spread/align}{
+
+\code{connectGrob()} always uses coordinates from the \code{start} and \code{end} objects
+you pass in. If those objects were positioned with \verb{spread*()}/\verb{align*()},
+pass the returned objects (for example by assigning the spread/align result)
+rather than the original pre-spread variables.
+}
}
\examples{
library(grid)
+
+# In interactive sessions, pause between pages produced by this example.
+if (interactive()) {
+ old_ask <- grDevices::devAskNewPage(TRUE)
+ on.exit(grDevices::devAskNewPage(old_ask), add = TRUE)
+}
+
grid.newpage()
-# Initiate the boxes that we want to connect
-boxes <- list(
+# Build a flowchart object
+boxes <- flowchart(
start = boxGrob("Top", x = .5, y = .8),
end = boxGrob("Bottom", x = .5, y = .2),
side = boxPropGrob("Side", "Left", "Right", prop = .3, x = .2, y = .8),
exclude = boxGrob("Exclude:\n - Too sick\n - Prev. surgery", x = .8, y = .5, just = "left")
)
-# Connect the boxes and print/plot them
-connectGrob(boxes$start, boxes$end, "vertical")
-connectGrob(boxes$start, boxes$side, "horizontal")
-connectGrob(boxes$start, boxes$exclude, "L")
+# Connect using the pipe-friendly S3 API
+boxes <- boxes |>
+ connect(from = "start", to = "end", type = "vertical") |>
+ connect(from = "start", to = "side", type = "horizontal") |>
+ connect(from = "start", to = "exclude", type = "L")
+
+print(boxes)
+
+# Start a fresh page for split-box connector examples.
+grid.newpage()
# We can also connect to/from lists
side_boxes <- list(
@@ -209,17 +230,46 @@ side_boxes <- list(
connectGrob(boxes$side, side_boxes$left, "v", "l")
connectGrob(boxes$side, side_boxes$right, "v", "r")
+# Start a fresh page for fan-in example.
+grid.newpage()
+
# Fan-in center example: multiple starts into one center bus and single trunk
-list(
- boxes$start,
- boxGrob("S2", x = .3, y = .7),
- boxGrob("S3", x = .7, y = .7)
+flowchart(
+ start = boxes$start,
+ S2 = boxGrob("S2", x = .3, y = .7),
+ S3 = boxGrob("S3", x = .7, y = .7),
+ end = boxes$end
) |>
- connectGrob(boxes$end, type = "fan_in_center")
+ connect(from = c("start", "S2", "S3"), to = "end", type = "fan_in_center") |>
+ print()
+
+# Start a fresh page for spread/assignment example.
+grid.newpage()
+
+# When using spread/align, use the returned objects for connectors
+visits <- flowchart(
+ visit1 = boxGrob("Visit 1", x = .1, y = .35),
+ visit2 = boxGrob("Visit 2", x = .2, y = .35),
+ visit3 = boxGrob("Visit 3", x = .8, y = .35)
+)
+
+# Incorrect pattern (no assignment): spread result is discarded
+visits |>
+ spread(axis = "x", from = .05, to = .95, type = "between")
+
+# This connector uses original coordinates because 'visits' was unchanged
+connectGrob(visits$visit1, visits$visit2, type = "horizontal")
+
+# Correct pattern: assign returned boxes and connect those
+visits_spread <- visits |>
+ spread(axis = "x", from = .05, to = .95, type = "between")
+connectGrob(visits_spread$visit1, visits_spread$visit2, type = "horizontal")
# Print the boxes
boxes
side_boxes
+visits
+visits_spread
}
\seealso{
[`connectGrob`]
@@ -238,6 +288,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/coords.Rd b/man/coords.Rd
index 7a89a28..3f388ff 100644
--- a/man/coords.Rd
+++ b/man/coords.Rd
@@ -34,6 +34,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/distance.Rd b/man/distance.Rd
index 102ef95..b7ee148 100644
--- a/man/distance.Rd
+++ b/man/distance.Rd
@@ -65,6 +65,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/equalizeWidths.Rd b/man/equalizeWidths.Rd
index 5dff12a..dbccc3a 100644
--- a/man/equalizeWidths.Rd
+++ b/man/equalizeWidths.Rd
@@ -54,6 +54,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/figures/README-flowchart-1.png b/man/figures/README-flowchart-1.png
new file mode 100644
index 0000000..517dcd0
Binary files /dev/null and b/man/figures/README-flowchart-1.png differ
diff --git a/man/figures/README-table1.png b/man/figures/README-table1.png
new file mode 100644
index 0000000..0b5ff54
Binary files /dev/null and b/man/figures/README-table1.png differ
diff --git a/man/figures/README-transition-1.png b/man/figures/README-transition-1.png
new file mode 100644
index 0000000..b563c75
Binary files /dev/null and b/man/figures/README-transition-1.png differ
diff --git a/man/flowchart.Rd b/man/flowchart.Rd
index c60f575..bacf0e7 100644
--- a/man/flowchart.Rd
+++ b/man/flowchart.Rd
@@ -55,6 +55,7 @@ Other flowchart components:
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/insert.Rd b/man/insert.Rd
index 20342e3..35513bc 100644
--- a/man/insert.Rd
+++ b/man/insert.Rd
@@ -6,11 +6,11 @@
\alias{insert.Gmisc_list_of_boxes}
\title{Insert element into a list of boxes (S3)}
\usage{
-insert(x, element, ..., after = NULL, before = NULL)
+insert(x, element, ..., after = NULL, before = NULL, on_top = FALSE)
-\method{insert}{default}(x, element, ..., after = NULL, before = NULL)
+\method{insert}{default}(x, element, ..., after = NULL, before = NULL, on_top = FALSE)
-\method{insert}{Gmisc_list_of_boxes}(x, element, ..., after = NULL, before = NULL)
+\method{insert}{Gmisc_list_of_boxes}(x, element, ..., after = NULL, before = NULL, on_top = FALSE)
}
\arguments{
\item{x}{A `Gmisc_list_of_boxes`.}
@@ -22,6 +22,13 @@ insert(x, element, ..., after = NULL, before = NULL)
\item{after}{The name or index of the box after which to insert.}
\item{before}{The name or index of the box before which to insert.}
+
+\item{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.}
}
\value{
The updated list of boxes with the new element inserted.
@@ -45,6 +52,7 @@ Other flowchart components:
\code{\link[=flowchart]{flowchart()}},
\code{\link[=move]{move()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/move.Rd b/man/move.Rd
index e6bf531..98f642c 100644
--- a/man/move.Rd
+++ b/man/move.Rd
@@ -41,6 +41,7 @@ Other flowchart components:
\code{\link[=flowchart]{flowchart()}},
\code{\link[=insert]{insert()}},
\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/moveBox.Rd b/man/moveBox.Rd
index 7f2cde2..9af878d 100644
--- a/man/moveBox.Rd
+++ b/man/moveBox.Rd
@@ -108,6 +108,7 @@ Other flowchart components:
\code{\link[=flowchart]{flowchart()}},
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
+\code{\link[=phaseLabel]{phaseLabel()}},
\code{\link[=spread]{spread()}}
}
\concept{flowchart components}
diff --git a/man/phaseLabel.Rd b/man/phaseLabel.Rd
new file mode 100644
index 0000000..6dbb38c
--- /dev/null
+++ b/man/phaseLabel.Rd
@@ -0,0 +1,91 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/boxGrobs_s3_phaseLabel.R
+\name{phaseLabel}
+\alias{phaseLabel}
+\alias{phaseLabel.default}
+\alias{phaseLabel.Gmisc_list_of_boxes}
+\title{Add a phase label to a flowchart stage (S3)}
+\usage{
+phaseLabel(x, ...)
+
+\method{phaseLabel}{default}(x, ...)
+
+\method{phaseLabel}{Gmisc_list_of_boxes}(x, reference, label, ..., width = NULL, overlap = 0.07, name = NULL)
+}
+\arguments{
+\item{x}{A `Gmisc_list_of_boxes` (or a `list` of boxes, which is converted).}
+
+\item{...}{Passed on to [`boxGrob`] when `label` is a string (e.g. `box_gp`,
+`txt_gp`).}
+
+\item{reference}{The name or index of the **stage to label**. The stage may be
+a single [`boxGrob`] or a `list` of arm boxes.}
+
+\item{label}{The label, either a string (wrapped with [`boxGrob`]) or a
+pre-built [`boxGrob`]/[`boxPropGrob`].}
+
+\item{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).}
+
+\item{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.}
+
+\item{name}{Name for the inserted label element. Defaults to
+`paste0(reference, "_label")`.}
+}
+\value{
+The updated `Gmisc_list_of_boxes` with the label added after the
+ referenced stage.
+}
+\description{
+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.
+}
+\details{
+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.
+}
+\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()
+}
+\seealso{
+[`insert`] for the general `on_top` overlay mechanism this builds on.
+
+Other flowchart components:
+\code{\link[=align]{align()}},
+\code{\link[=append]{append()}},
+\code{\link[=boxGrob]{boxGrob()}},
+\code{\link[=boxHeaderGrob]{boxHeaderGrob()}},
+\code{\link[=boxPropGrob]{boxPropGrob()}},
+\code{\link{boxShapes}},
+\code{\link[=connectGrob]{connectGrob()}},
+\code{\link[=coords]{coords()}},
+\code{\link[=distance]{distance()}},
+\code{\link[=equalizeWidths]{equalizeWidths()}},
+\code{\link[=flowchart]{flowchart()}},
+\code{\link[=insert]{insert()}},
+\code{\link[=move]{move()}},
+\code{\link[=moveBox]{moveBox()}},
+\code{\link[=spread]{spread()}}
+}
+\concept{flowchart components}
diff --git a/man/prGetBoxAxisDefaults.Rd b/man/prGetBoxAxisDefaults.Rd
index d77fdc2..e0b78ef 100644
--- a/man/prGetBoxAxisDefaults.Rd
+++ b/man/prGetBoxAxisDefaults.Rd
@@ -2,8 +2,7 @@
% Please edit documentation in R/boxGrobs_spread_npc_norm_helpers.R
\name{prGetBoxAxisDefaults}
\alias{prGetBoxAxisDefaults}
-\title{Get default from/to values for an axis
-Get default from/to values for an axis}
+\title{Get default from/to values for an axis}
\usage{
prGetBoxAxisDefaults(axis = c("x", "y"))
}
diff --git a/man/print.Gmisc_list_of_boxes.Rd b/man/print.Gmisc_list_of_boxes.Rd
index e652f50..562da94 100644
--- a/man/print.Gmisc_list_of_boxes.Rd
+++ b/man/print.Gmisc_list_of_boxes.Rd
@@ -15,3 +15,9 @@
Outputs a list of boxes as produced by either the
spread or align functions for boxGrobs.
}
+\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.
+}
diff --git a/man/spread.Rd b/man/spread.Rd
index 07b1ab2..7b07d85 100644
--- a/man/spread.Rd
+++ b/man/spread.Rd
@@ -79,6 +79,11 @@ direction within a given span.
The span can be defined explicitly using \code{from} / \code{to}, or implicitly by the
current viewport. Numeric values are interpreted as proportions of the viewport
(\code{npc} units).
+
+\code{spreadVertical()}/\code{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 \code{connectGrob()}), assign
+the result and use those returned boxes.
}
\examples{
library(grid)
@@ -90,13 +95,14 @@ end <- boxGrob("Bottom", x = .5, y = .2)
side <- boxPropGrob("Side", "Left", "Right", prop = .3, x = .2, y = .8)
exclude <- boxGrob("Exclude:\n - Too sick\n - Prev. surgery", x = .8, y = .5, just = "left")
-# We can chain the spread operations and print the result
-spreadVertical(
+# We can chain the spread operations from a flowchart object
+flowchart(
start = start,
middle = list(side, exclude),
end = end
) |>
- spreadHorizontal(subelement = "middle", from = 0.2, to = 0.8)
+ spread(axis = "y") |>
+ spread(axis = "x", subelement = "middle", from = 0.2, to = 0.8)
# Use device-level paging in interactive sessions so users can inspect the first plot
# This will prompt before creating the next page; we restore the previous setting afterwards
@@ -110,8 +116,11 @@ if (interactive()) {
}
# Example: spread a nested subelement by deep path and print sequentially
-list(grp = list(middle = list(side, exclude))) |>
- spreadHorizontal(subelement = c("grp", "middle"), from = 0.2, to = 0.8)
+flowchart(
+ anchor = boxGrob("Anchor", x = .5, y = .15),
+ grp = list(middle = list(side, exclude))
+) |>
+ spread(axis = "x", subelement = c("grp", "middle"), from = 0.2, to = 0.8)
}
\seealso{
Other flowchart components:
@@ -128,6 +137,7 @@ Other flowchart components:
\code{\link[=flowchart]{flowchart()}},
\code{\link[=insert]{insert()}},
\code{\link[=move]{move()}},
-\code{\link[=moveBox]{moveBox()}}
+\code{\link[=moveBox]{moveBox()}},
+\code{\link[=phaseLabel]{phaseLabel()}}
}
\concept{flowchart components}
diff --git a/tests/testthat/test-connectGrob-spread-assignment.R b/tests/testthat/test-connectGrob-spread-assignment.R
new file mode 100644
index 0000000..8ee2d4d
--- /dev/null
+++ b/tests/testthat/test-connectGrob-spread-assignment.R
@@ -0,0 +1,57 @@
+library(testthat)
+
+test_that("spreadHorizontal returns updated boxes without mutating originals", {
+ b1 <- boxGrob("Visit 1", x = 0.1, y = 0.7)
+ b2 <- boxGrob("Visit 2", x = 0.2, y = 0.7)
+ b3 <- boxGrob("Visit 3", x = 0.9, y = 0.7)
+
+ b2_x_before <- convertX(coords(b2)$x, "npc", valueOnly = TRUE)
+
+ spread_res <- spreadHorizontal(
+ b1,
+ b2,
+ b3,
+ from = unit(0.05, "npc"),
+ to = unit(0.95, "npc"),
+ type = "between"
+ )
+
+ b2_x_after <- convertX(coords(b2)$x, "npc", valueOnly = TRUE)
+ b2_x_spread <- convertX(coords(spread_res[[2]])$x, "npc", valueOnly = TRUE)
+
+ expect_equal(b2_x_after, b2_x_before)
+ expect_true(abs(b2_x_spread - b2_x_before) > 1e-6)
+})
+
+
+test_that("connectGrob uses post-spread coordinates when updated objects are used", {
+ visit1 <- boxGrob("Visit 1", x = 0.05, y = 0.45)
+ visit5 <- boxGrob("Visit 5", x = 0.95, y = 0.45)
+ visit2 <- boxGrob("Visit 2", x = 0.20, y = 0.45)
+ visit3 <- boxGrob("Visit 3", x = 0.50, y = 0.45)
+ visit4 <- boxGrob("Visit 4", x = 0.70, y = 0.45)
+
+ spread_visits <- alignVertical(
+ reference = visit1,
+ visit2, visit3, visit4,
+ position = "top"
+ ) |>
+ spreadHorizontal(
+ from = visit1,
+ to = visit5,
+ type = "between"
+ )
+
+ con_old <- connectGrob(visit1, visit2, type = "horizontal")
+ con_new <- connectGrob(visit1, spread_visits[[1]], type = "horizontal")
+
+ x_old_end <- convertX(attr(con_old, "line")$x[2], "npc", valueOnly = TRUE)
+ x_new_end <- convertX(attr(con_new, "line")$x[2], "npc", valueOnly = TRUE)
+
+ x_old_expected <- convertX(coords(visit2)$left, "npc", valueOnly = TRUE)
+ x_new_expected <- convertX(coords(spread_visits[[1]])$left, "npc", valueOnly = TRUE)
+
+ expect_equal(x_old_end, x_old_expected, tolerance = 1e-6)
+ expect_equal(x_new_end, x_new_expected, tolerance = 1e-6)
+ expect_true(abs(x_old_end - x_new_end) > 1e-6)
+})
\ No newline at end of file
diff --git a/tests/testthat/test-s3-api-design.R b/tests/testthat/test-s3-api-design.R
index 5fd1cf5..d7a2274 100644
--- a/tests/testthat/test-s3-api-design.R
+++ b/tests/testthat/test-s3-api-design.R
@@ -152,6 +152,191 @@ test_that("S3 Mutations: insert", {
expect_equal(length(l_ins3), 3)
})
+test_that("S3 Mutations: insert positions boxes between grouped stages", {
+ fc <- flowchart(
+ rando = boxGrob("Randomised N = 100", x = .5, y = .9),
+ groups = list(
+ boxGrob("Group1\nn = 50", x = .3, y = .7),
+ boxGrob("Group2\nn = 50", x = .7, y = .7)
+ ),
+ groups2 = list(
+ boxGrob("Excluded\nn = 1", x = .3, y = .5),
+ boxGrob("Excluded\nn = 2", x = .7, y = .5)
+ ),
+ groups3 = list(
+ boxGrob("Analysed\nn = 49", x = .3, y = .3),
+ boxGrob("Analysed\nn = 48", x = .7, y = .3)
+ )
+ )
+
+ inserted <- insert(fc, boxGrob("Followup"), after = "groups")
+ expect_equal(length(inserted), 5)
+ expect_equal(names(inserted), c("rando", "groups", "", "groups2", "groups3"))
+ expect_s3_class(inserted[[3]], "box")
+
+ prev_coords <- prConvert2Coords(fc$groups)
+ next_coords <- prConvert2Coords(fc$groups2)
+ expected_x <- convertX(prev_coords$x, "npc", valueOnly = TRUE) +
+ (convertX(next_coords$x, "npc", valueOnly = TRUE) -
+ convertX(prev_coords$x, "npc", valueOnly = TRUE)) * .5
+ expected_y <- convertY(prev_coords$y, "npc", valueOnly = TRUE) +
+ (convertY(next_coords$y, "npc", valueOnly = TRUE) -
+ convertY(prev_coords$y, "npc", valueOnly = TRUE)) * .5
+
+ inserted_coords <- coords(inserted[[3]])
+ expect_equal(convertX(inserted_coords$x, "npc", valueOnly = TRUE), expected_x, tolerance = .01)
+ expect_equal(convertY(inserted_coords$y, "npc", valueOnly = TRUE), expected_y, tolerance = .01)
+})
+
+test_that("S3 Mutations: insert preserves names between grouped stages", {
+ fc <- flowchart(
+ groups = list(
+ boxGrob("Group1", x = .3, y = .7),
+ boxGrob("Group2", x = .7, y = .7)
+ ),
+ groups2 = list(
+ boxGrob("Excluded 1", x = .3, y = .5),
+ boxGrob("Excluded 2", x = .7, y = .5)
+ )
+ )
+
+ inserted <- insert(fc, list(followup = boxGrob("Followup")), after = "groups")
+
+ expect_equal(names(inserted), c("groups", "followup", "groups2"))
+ expect_s3_class(inserted$followup, "box")
+ expect_equal(convertY(coords(inserted$followup)$y, "npc", valueOnly = TRUE), .6, tolerance = .01)
+})
+
+test_that("S3 Mutations: insert(on_top = TRUE) marks the box and survives align/move", {
+ fc <- flowchart(
+ rando = boxGrob("Randomised", x = .5, y = .9),
+ groups = list(
+ boxGrob("Group1", x = .3, y = .6),
+ boxGrob("Group2", x = .7, y = .6)
+ )
+ )
+
+ inserted <- insert(
+ fc,
+ list(heading = boxGrob("Allocation")),
+ after = "rando",
+ on_top = TRUE
+ )
+
+ # The marker is set on the inserted box, not on the existing ones
+ expect_true(isTRUE(attr(inserted$heading, "draw_on_top")))
+ expect_null(attr(inserted$rando, "draw_on_top"))
+
+ # Default (no on_top) leaves the box unmarked
+ plain <- insert(fc, list(plain = boxGrob("Plain")), after = "rando")
+ expect_false(isTRUE(attr(plain$plain, "draw_on_top")))
+
+ # The marker survives subsequent positioning (align + move both go via moveBox)
+ positioned <- inserted |>
+ align(axis = "x", reference = "groups", subelement = "heading", position = "center") |>
+ align(axis = "y", reference = "groups", subelement = "heading", position = "top") |>
+ move(subelement = "heading", y = unit(0.01, "npc"), space = "relative")
+ expect_true(isTRUE(attr(positioned$heading, "draw_on_top")))
+
+ # Drawing the marked flowchart completes without error
+ grid::grid.newpage()
+ expect_silent(print(positioned))
+})
+
+test_that("S3 Mutations: phaseLabel places a stage label above the stage", {
+ pdf(NULL)
+ on.exit(dev.off())
+ grid.newpage()
+
+ sw <- unit(40, "mm")
+ fc <- flowchart(
+ rando = boxGrob("Randomised", x = .5, y = .9),
+ groups = list(
+ boxGrob("Intervention", x = .3, y = .6, width = sw),
+ boxGrob("Control", x = .7, y = .6, width = sw)
+ )
+ ) |>
+ phaseLabel("groups", "Allocation")
+
+ # Added after the stage, named, and marked to draw on top
+ expect_equal(names(fc), c("rando", "groups", "groups_label"))
+ expect_s3_class(fc$groups_label, "box")
+ expect_true(isTRUE(attr(fc$groups_label, "draw_on_top")))
+
+ # Centred on the stage horizontally
+ stage_x <- convertX(prConvert2Coords(fc$groups)$x, "npc", valueOnly = TRUE)
+ label_x <- convertX(coords(fc$groups_label)$x, "npc", valueOnly = TRUE)
+ expect_equal(label_x, stage_x, tolerance = .01)
+
+ # Bottom edge dips just below the stage top (sits slightly above, overlapping)
+ stage_top <- convertY(prConvert2Coords(fc$groups)$top, "npc", valueOnly = TRUE)
+ label_bottom <- convertY(coords(fc$groups_label)$bottom, "npc", valueOnly = TRUE)
+ expect_lt(label_bottom, stage_top)
+ expect_gt(label_bottom, stage_top - 0.1)
+})
+
+test_that("S3 Mutations: phaseLabel width adapts to arm count and honours width", {
+ pdf(NULL)
+ on.exit(dev.off())
+ grid.newpage()
+
+ sw <- unit(40, "mm")
+
+ # Two arms -> narrow label (gap + corner lap), narrower than the full stage
+ fc2 <- flowchart(
+ g = list(boxGrob("A", x = .3, y = .5, width = sw),
+ boxGrob("B", x = .7, y = .5, width = sw))
+ ) |>
+ phaseLabel("g", "Label")
+ stage_w2 <- convertWidth(prConvert2Coords(fc2$g)$width, "npc", valueOnly = TRUE)
+ label_w2 <- convertWidth(coords(fc2$g_label)$width, "npc", valueOnly = TRUE)
+ expect_lt(label_w2, stage_w2)
+
+ # Three arms -> banner spanning the full stage width
+ fc3 <- flowchart(
+ g = list(boxGrob("A", x = .2, y = .5, width = sw),
+ boxGrob("B", x = .5, y = .5, width = sw),
+ boxGrob("C", x = .8, y = .5, width = sw))
+ ) |>
+ phaseLabel("g", "Label")
+ stage_w3 <- convertWidth(prConvert2Coords(fc3$g)$width, "npc", valueOnly = TRUE)
+ label_w3 <- convertWidth(coords(fc3$g_label)$width, "npc", valueOnly = TRUE)
+ expect_equal(label_w3, stage_w3, tolerance = .02)
+
+ # Explicit width override is honoured
+ fc4 <- flowchart(
+ g = list(boxGrob("A", x = .3, y = .5, width = sw),
+ boxGrob("B", x = .7, y = .5, width = sw))
+ ) |>
+ phaseLabel("g", "Label", width = unit(25, "mm"))
+ expect_equal(convertWidth(coords(fc4$g_label)$width, "mm", valueOnly = TRUE), 25, tolerance = .5)
+
+ # Drawing a multi-arm banner completes without error
+ expect_silent(print(fc3))
+})
+
+test_that("S3 Mutations: phaseLabel supports nested arm lists", {
+ pdf(NULL)
+ on.exit(dev.off())
+ grid.newpage()
+
+ # Flowchart with nested arms (a list of boxes as one arm)
+ fc <- flowchart(
+ rando = boxGrob("Randomised", x = .5, y = .8),
+ arms = list(
+ list(boxGrob("Arm 1a", x = .3, y = .4),
+ boxGrob("Arm 1b", x = .4, y = .4)),
+ boxGrob("Arm 2", x = .7, y = .4)
+ )
+ )
+
+ # Should not error
+ expect_silent({
+ fc_labeled <- phaseLabel(fc, "arms", "Allocation")
+ })
+ expect_equal(names(fc_labeled), c("rando", "arms", "arms_label"))
+})
+
test_that("Complex chaining example", {
# Mock components
org_cohort <- boxGrob("Stockholm", x = .5, y = .9)
diff --git a/vignettes/Grid-based_flowcharts.Rmd b/vignettes/Grid-based_flowcharts.Rmd
index 2aa9459..11f9392 100644
--- a/vignettes/Grid-based_flowcharts.Rmd
+++ b/vignettes/Grid-based_flowcharts.Rmd
@@ -71,6 +71,79 @@ flowchart(
smooth = TRUE)
```
+# CONSORT phase labels between grouped stages
+
+CONSORT diagrams use phase labels such as allocation, follow-up, and analysis.
+These are not part of the patient flow itself — they label a *stage* and sit
+just above it, centred between the randomisation arms.
+
+`phaseLabel()` does this in one call per stage:
+
+- It references the **stage it labels** (e.g. `"groups"`), so there is no
+ confusing "insert after X" step.
+- It places the label slightly above the stage, overlapping the top corners by
+ `overlap` (≈ 7% by default), and marks it to be drawn **on top** so it stays
+ visible.
+- The width adapts to the stage: for two arms the label spans the central gap
+ plus a small corner lap; for three or more arms it becomes a banner across the
+ full stage width. Pass `width` to override.
+
+The arms are given a clear central gap (boxes spread to `x = 0.27`/`0.73`). Equal
+arm widths give a symmetric corner overlap — use `equalizeWidths()` if the arms
+differ. For custom overlays beyond phase labels, the lower-level
+`insert(..., on_top = TRUE)` is still available.
+
+```{r consort-stage-headings, fig.height = 7, fig.width = 8}
+old_opts <- options(boxGrobTxtPadding = unit(2, "mm"))
+
+main_box_gp <- gpar(fill = "white", col = "black", lwd = 1)
+heading_gp <- gpar(fill = "#c8daf7", col = "#2f5f9f", lwd = 1)
+con_gp <- gpar(col = "#4f86c6", fill = "#4f86c6", lwd = 1.8)
+side_width <- unit(70, "mm")
+
+flowchart(
+ rando = boxGrob("Randomised\nN = 100", box_gp = main_box_gp),
+ groups = list(
+ boxGrob("Allocated to intervention\nn = 50",
+ width = side_width, box_gp = main_box_gp),
+ boxGrob("Allocated to control\nn = 50",
+ width = side_width, box_gp = main_box_gp)
+ ),
+ followup = list(
+ boxGrob("Lost to follow-up\nn = 1",
+ width = side_width, box_gp = main_box_gp),
+ boxGrob("Lost to follow-up\nn = 2",
+ width = side_width, box_gp = main_box_gp)
+ ),
+ analysis = list(
+ boxGrob("Analysed\nn = 49",
+ width = side_width, box_gp = main_box_gp),
+ boxGrob("Analysed\nn = 48",
+ width = side_width, box_gp = main_box_gp)
+ )
+) |>
+ spread(axis = "y", margin = unit(0.04, "npc")) |>
+ # Give the two arms a clear central gap for the labels to sit in
+ move(subelement = list(c("groups", 1),
+ c("followup", 1),
+ c("analysis", 1)),
+ x = 0.27) |>
+ move(subelement = list(c("groups", 2),
+ c("followup", 2),
+ c("analysis", 2)),
+ x = 0.73) |>
+ # One call per stage: centred between the arms, slightly above, drawn on top
+ phaseLabel("groups", "Allocation", box_gp = heading_gp) |>
+ phaseLabel("followup", "Follow-up", box_gp = heading_gp) |>
+ phaseLabel("analysis", "Analysis", box_gp = heading_gp) |>
+ connect("rando", "groups", type = "N", lty_gp = con_gp, arrow_size = 3,
+ smooth = TRUE) |>
+ connect("groups", "followup", type = "v", lty_gp = con_gp, arrow_size = 3) |>
+ connect("followup", "analysis", type = "v", lty_gp = con_gp, arrow_size = 3)
+
+options(old_opts)
+```
+
# CONSORT-style flowchart
A CONSORT diagram represents patient flow through a clinical trial. This example