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 + + +[![CRAN status](https://www.r-pkg.org/badges/version/Gmisc)](https://CRAN.R-project.org/package=Gmisc) +[![CRAN downloads](https://cranlogs.r-pkg.org/badges/Gmisc)](https://CRAN.R-project.org/package=Gmisc) +[![R-CMD-check](https://github.com/gforge/Gmisc/actions/workflows/ci.yaml/badge.svg)](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

+ + +

CRAN status CRAN downloads R-CMD-check

+ + +

Gmisc collects utilities for the graphics and tables +that recur in medical research papers — built so they compose with the +native R pipe (|>):

+ +

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 = "&dagger; 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 @@ -[![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/Gmisc)](https://cran.r-project.org/package=Gmisc) -[![](https://cranlogs.r-pkg.org/badges/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. + + +[![CRAN +status](https://www.r-pkg.org/badges/version/Gmisc)](https://CRAN.R-project.org/package=Gmisc) +[![CRAN +downloads](https://cranlogs.r-pkg.org/badges/Gmisc)](https://CRAN.R-project.org/package=Gmisc) +[![R-CMD-check](https://github.com/gforge/Gmisc/actions/workflows/ci.yaml/badge.svg)](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