---
title: "Advanced Workflows"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Advanced Workflows}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
## Use ragg for better font rendering if available
if (requireNamespace("ragg", quietly = TRUE)) {
  knitr::opts_chunk$set(
    dev = "ragg_png",
    fig.retina = 1,
    collapse = TRUE,
    comment = "#>",
    message = FALSE,
    warning = FALSE,
    out.width = "100%",
    dpi = 150
  )
} else {
  knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    message = FALSE,
    warning = FALSE,
    out.width = "100%",
    dpi = 150
  )
}

## Dynamic figure sizing: queue_flow() stashes recommended dimensions from
## recdims(), and the opts_hook on the NEXT chunk (with use_rec_dims = TRUE)
## applies them before knitr opens the graphics device.
.flow_dims <- new.env(parent = emptyenv())
.flow_dims$width <- NULL
.flow_dims$height <- NULL

knitr::opts_hooks$set(use_rec_dims = function(options) {
  if (isTRUE(options$use_rec_dims)) {
    if (!is.null(.flow_dims$width))  options$fig.width  <- .flow_dims$width
    if (!is.null(.flow_dims$height)) options$fig.height <- .flow_dims$height
    .flow_dims$width <- NULL
    .flow_dims$height <- NULL
  }
  options
})

queue_flow <- function(flow, ...) {
  ## Measure on the same device family that renders the figures (ragg, set
  ## via dev = "ragg_png" above) so that non-default fonts -- whose metrics
  ## differ between devices -- are sized consistently and the canvas is not
  ## cropped. Falls back to recdims()'s default pdf measurement otherwise.
  md <- if (requireNamespace("ragg", quietly = TRUE)) {
    function() {
      tf <- tempfile(fileext = ".png")
      ragg::agg_png(tf, width = 10, height = 10, units = "in", res = 150)
      tf
    }
  } else NULL
  dims <- selecta::recdims(flow, ..., .measure_dev = md)
  .flow_dims$width  <- dims["width"]
  .flow_dims$height <- dims["height"]
  invisible(flow)
}

## ---- DOT rendering helper (mirrors graphviz_export.Rmd) ----
## Pipes a DOT string through the system Graphviz binary into SVG, then
## post-processes the SVG to expand Graphviz's single-name font-family
## attribute (Helvetica or Times) to a cross-platform sans-serif chain
## (Helvetica on macOS, Arial on Windows, Liberation Sans / DejaVu Sans
## on Linux), inlining the result at full text-column width. Falls back
## to DiagrammeR::grViz() when the binary is unavailable.
.dot_available <- nzchar(Sys.which("dot"))
.sans_chain    <- "Helvetica, Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif"

render_dot <- function(dot_str, width = "100%", fmt = c("svg", "png"),
                       dpi = 150, sans_serif = TRUE) {
  fmt <- match.arg(fmt)
  if (.dot_available) {
    out <- paste0(knitr::fig_path(paste0(".", fmt)))
    fig_dir <- dirname(out)
    if (!dir.exists(fig_dir)) dir.create(fig_dir, recursive = TRUE)
    dot_in <- tempfile(fileext = ".dot")
    writeLines(dot_str, dot_in)
    args <- c(paste0("-T", fmt))
    if (fmt == "png") args <- c(args, paste0("-Gdpi=", dpi))
    args <- c(args, shQuote(dot_in), "-o", shQuote(out))
    system2("dot", args, stdout = NULL, stderr = NULL)

    if (isTRUE(sans_serif) && fmt == "svg" && file.exists(out)) {
      svg_text <- paste(readLines(out, warn = FALSE), collapse = "\n")
      svg_text <- gsub('font-family="(Helvetica|Times)[^"]*"',
                       sprintf('font-family="%s"', .sans_chain),
                       svg_text, perl = TRUE)
      svg_text <- gsub("font-family='(Helvetica|Times)[^']*'",
                       sprintf("font-family=\"%s\"", .sans_chain),
                       svg_text, perl = TRUE)
      writeLines(svg_text, out)
    }

    knitr::include_graphics(out, dpi = NA)
  } else if (requireNamespace("DiagrammeR", quietly = TRUE)) {
    DiagrammeR::grViz(dot_str, width = width)
  } else {
    cat(dot_str)
  }
}
```

The core vignettes cover the principal EQUATOR topologies: single-stream selection, enrollment with permanent parallel stratification, top-level source convergence, and split-and-recombine analysis. This article showcases additional, less common flow diagram structures covered by `selecta`, including factorial layouts, hierarchical (nested) exclusion reasons, and other complex configurations.

> *n.b.:* To ensure correct font rendering and figure sizing, the `grid`-based diagrams below are displayed using a vignette-only helper function (`queue_flow()`) that applies recommended dimensions from `recdims()` via the [`ragg`](https://ragg.r-lib.org/) graphics device, with the standard output function applied afterwards (`flowchart()`). In practice, replace this `queue_flow()`/`flowchart()` workflow with a call to `flowsave()` for equivalent printed results:
>
> ```{r, eval = FALSE}
> flowsave(flow, "factorial.pdf")
> flowsave(flow, "factorial.png", dpi = 300)
> ```
>
> Using `flowsave()` ensures that the figure dimensions are always large enough to accommodate the diagram content, and it is the preferred method for saving flow diagram outputs in `selecta`.

---

# Preliminaries

```{r setup}
library(selecta)
library(data.table)
```

The manual examples in this vignette are constructed from summary counts. The two data-driven examples build small synthetic datasets inline to illustrate how the same diagrams arise from row-level data.

---

# Factorial (Multi-Split) Designs

A factorial design randomizes (or stratifies) each participant on two factors at once, so that every level of the first factor is crossed with every level of the second. A two-by-two trial, for example, assigns each participant to one of two antiviral arms *and* to one of two adjuvant arms, yielding four cells.

In `selecta`, a factorial layout is expressed by chaining two split steps. The first `allocate()` (or `stratify()`) divides the cohort into the first-factor arms; the second split then divides *each* of those arms into the second-factor sub-arms. The second split is supplied a single count vector whose entries enumerate the cells in parent-major order—all sub-arms of the first parent, then all sub-arms of the second, and so on:

```{r, eval = FALSE}
enroll(n = 480) |>
  allocate(labels = c("Drug A", "Drug B"), n = c(240, 240)) |>   # factor 1
  allocate(labels = c("Vaccine", "Placebo"),                     # factor 2
           n = c(120, 120,        # Drug A: Vaccine, Placebo
                 120, 120)) |>    # Drug B: Vaccine, Placebo
  endpoint("Analyzed")
```

Two nested split levels are the maximum; a third consecutive split is refused, since deeper nesting is not part of any EQUATOR diagram and rarely reads clearly on a page. A level can be released with `combine()` (see below), after which a further split is permitted.

## **Example 1:** A Two-by-Two Factorial Trial

The canonical factorial CONSORT diagram crosses two binary randomizations. The first `allocate()` carries a `label`, which names the allocation box drawn between the randomized cohort and the first-factor arms; the second `allocate()` needs no label, as its sub-arms hang directly beneath their parents. A subsequent `exclude()` is given one count per cell, producing a side box for each leaf arm:

```{r}
example1 <- enroll(n = 480, label = "Randomized") |>
    phase("Allocation") |>
    allocate(labels = c("Drug A", "Drug B"), n = c(240, 240),
             label = "Antiviral assignment") |>
    allocate(labels = c("Vaccine", "Placebo"), n = c(120, 120, 120, 120)) |>
    phase("Follow-up") |>
    exclude("Discontinued", n = c(8, 6, 7, 9)) |>
    phase("Analysis") |>
    endpoint("Primary analysis")
```

```{r, echo = FALSE}
queue_flow(example1)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example1)
```

The `grid` engine treats the four second-level sub-arms as leaf columns, centers each first-level parent over its pair of children, and centers the trunk over all leaves. The per-cell exclusion boxes are splayed outward in the manner of a standard two-arm diagram: within each parent, the first sub-arm's box is placed to the left and the second sub-arm's box to the right, keeping the central channel clear.

## **Example 2:** Larger Factorial Grids

The same two-split construction scales to any cell count. A three-by-three design supplies three first-level arms and a nine-element count vector for the second split, again in parent-major order:

```{r}
example2 <- enroll(n = 900, label = "Randomized") |>
    phase("Allocation") |>
    allocate(labels = c("Low", "Medium", "High"), n = c(300, 300, 300),
             label = "Dose tier") |>
    allocate(labels = c("Schedule A", "Schedule B", "Schedule C"),
             n = rep(100L, 9L)) |>
    phase("Analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example2)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example2)
```

Each parent is centered over its three children, and the fan connectors are symmetric about every parent. Odd and even sub-arm counts are both handled: with three children the parent sits over the middle one, and with two it sits over the midpoint of the pair.

## **Example 3:** Asymmetric Factorial Designs

The two factors need not have the same number of levels. A two-by-three design pairs two first-level strategies with three intensity levels each; the second split therefore receives a six-element vector (two parents times three sub-arms):

```{r}
example3 <- enroll(n = 600, label = "Randomized") |>
    phase("Allocation") |>
    allocate(labels = c("Surgical", "Medical"), n = c(300, 300),
             label = "Primary strategy") |>
    allocate(labels = c("Low", "Standard", "Intensive"),
             n = c(100, 100, 100,      # Surgical
                   100, 100, 100)) |>  # Medical
    phase("Analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example3)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example3)
```

## **Example 4:** Factorial Designs from Row-Level Data

In data mode, each split step receives a column name rather than explicit labels and counts. A factorial layout is produced by crossing two categorical columns: the first `allocate()` splits on the first factor, and the second `allocate()` splits each resulting arm on the second. The cell counts, and any data-driven exclusion counts, are computed from the data. The dataset below cross-classifies 800 patients by antiviral assignment and adjuvant assignment, with eight discontinuations per cell:

```{r}
n_cell <- 200L
fac_data <- data.table(
    id           = sprintf("P%04d", seq_len(4L * n_cell)),
    antiviral    = rep(c("Drug A", "Drug B"), each = 2L * n_cell),
    adjuvant     = rep(rep(c("Vaccine", "Placebo"), each = n_cell), times = 2L),
    discontinued = rep(c(rep(TRUE, 8L), rep(FALSE, n_cell - 8L)), times = 4L)
)

example4 <- enroll(fac_data, id = "id", label = "Randomized") |>
    phase("Allocation") |>
    allocate("antiviral", label = "Antiviral assignment") |>
    allocate("adjuvant") |>
    phase("Follow-up") |>
    exclude("Discontinued", criterion = discontinued == TRUE) |>
    phase("Analysis") |>
    endpoint("Primary analysis")
```

```{r, echo = FALSE}
queue_flow(example4)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example4)
```

Because the counts are derived from the data, the diagram is reproducible and auditable: re-running the pipeline against an updated dataset refreshes every count automatically. The sub-arm columns are ordered by their factor levels, so the leaf order follows the sorted values of the second column.

## **Example 5:** Pooling Twice into a Single Cohort

A factorial split can be collapsed one level at a time, and `combine()` may be applied more than once in sequence. The first `combine()` after the second split draws converging arrows that pool the second-factor sub-arms back into their first-factor parents, releasing the nested level and leaving one stream per first-factor arm. A second `combine()` then pools those streams in turn, merging the parallel arms into a single analysis cohort. The optional `sublabel` prints a second line of explanatory text beneath the merged box:

```{r}
example5 <- enroll(n = 360, label = "Randomized") |>
    phase("Allocation") |>
    allocate(labels = c("Concurrent", "Sequential"), n = c(180, 180),
             label = "Timing strategy") |>
    allocate(labels = c("Agent A", "Agent B"), n = c(90, 90, 90, 90)) |>
    phase("Pooling") |>
    combine("Pooled by timing") |>
    combine("Combined analysis cohort",
            sublabel = "Both timing strategies merged") |>
    phase("Analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example5)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example5)
```

Each `combine()` releases one level of structure: the first returns the diagram to two parallel timing streams, and the second merges those streams into one. Because a released level permits a further split, the two operations can be interleaved with `allocate()` or `stratify()` to express designs that cross, pool, and re-split factors at successive stages.

## **Example 6:** Factorial Layouts via the DOT Engine

The Graphviz/DOT engine renders factorial diagrams with the same nesting and outboard exclusion boxes. Passing `engine = "dot"` to `flowchart()` returns the DOT source for the two-by-two trial from Example 1:

```{r}
example6 <- flowchart(example1, engine = "dot")
```

```{r, echo = FALSE, out.width = "100%"}
render_dot(example6)
```

The DOT engine centers each first-level parent over its pair of sub-arms and splays the two per-cell exclusion boxes outward, matching the `grid` engine's treatment. Orthogonal routing (`ortho = TRUE`), count-first labels (`count_first = TRUE`), and the typography options described in the [Graphviz Export](graphviz_export.html) vignette all apply to factorial diagrams as well.

---

# Hierarchical (Nested) Exclusion Reasons

An ordinary `exclude()` step may attach a breakdown of reasons through its `reasons` argument. When `reasons` is a flat named vector, each entry is a single reason with its count. When a reason is itself composed of finer sub-reasons, `reasons` accepts a **named list**: each element is named for a broad reason category, and its value is either a named vector of sub-reasons or a single count for a category with no further breakdown. The rendered side box lists each category as a bulleted parent with its sub-reasons indented beneath as en-dashed entries.

A flat named vector and an unnamed list serve different purposes and should not be confused with the nested form. A flat vector (`reasons = c("Reason" = n, ...)`) gives a single-level breakdown; an **unnamed** list (`reasons = list(vec1, vec2)`) supplies one flat vector *per arm* after `allocate()` or `stratify()` (see the [Split-and-Recombine](split_recombine.html) vignette). The nested form here uses a **named** list on a single stream.

## **Example 7:** Manual Nested Reasons

The example below removes 250 participants before enrollment, grouped under three categories. Two categories carry sub-reasons; the third ("Administrative") reason is a single count with no breakdown. The sub-reason counts sum to their category total, and the category totals sum to the step count:

```{r}
example7 <- enroll(n = 1000, label = "Assessed for eligibility") |>
    phase("Screening") |>
    exclude("Excluded", n = 250,
            reasons = list(
                "Did not meet inclusion criteria" = c(
                    "Outside age range"    = 70,
                    "Comorbid condition"   = 55,
                    "Insufficient washout" = 25),
                "Declined to participate" = c(
                    "Time commitment" = 40,
                    "Travel burden"   = 20),
                "Administrative" = 40),
            included_label = "Enrolled") |>
    phase("Analysis") |>
    endpoint("Analysis cohort")
```

```{r, echo = FALSE}
queue_flow(example7)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example7)
```

By default, a category whose only member is a single sub-reason is collapsed onto one line; the `collapse_singletons = FALSE` argument keeps such a category and its lone sub-reason on separate lines.

## **Example 8:** Two-Column Reasons from Row-Level Data

In data mode, a hierarchical breakdown is produced by naming two columns—a reason column and a sub-reason column—in the `reasons` argument: `reasons = c("reason", "subreason")`. The excluded rows are cross-tabulated by the two columns into the same nested structure, with each reason's sub-reasons and their counts derived from the data:

```{r}
review_data <- data.table(
    record_id = sprintf("R%04d", seq_len(1000L)),
    excluded  = c(rep(TRUE, 220L), rep(FALSE, 780L)),
    reason    = c(rep("Ineligible study design", 130L),
                  rep("Insufficient reporting", 90L),
                  rep(NA_character_, 780L)),
    subreason = c(rep("Case report", 70L), rep("Narrative review", 60L),
                  rep("No usable outcome", 50L), rep("No variance estimate", 40L),
                  rep(NA_character_, 780L))
)

example8 <- enroll(review_data, id = "record_id",
                    label = "Records identified") |>
    phase("Screening") |>
    exclude("Records excluded", criterion = excluded == TRUE,
            reasons = c("reason", "subreason"),
            included_label = "Records retained") |>
    phase("Synthesis") |>
    endpoint("Studies in synthesis")
```

```{r, echo = FALSE}
queue_flow(example8)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example8)
```

A single column name (`reasons = "reason"`) yields a flat breakdown instead; the second column is what introduces the second level. The same two-column form may be supplied after a split, in which case the cross-tabulation is performed per arm with a shared sub-reason ordering.

## **Example 9:** Nested Reasons via the DOT Engine

The Graphviz/DOT engine renders the same two-level breakdown, with bulleted parent reasons and en-dashed sub-reasons inside the exclusion node:

```{r}
example9 <- flowchart(example7, engine = "dot")
```

```{r, echo = FALSE, out.width = "100%"}
render_dot(example9)
```

The plain-text label path used here centers reliably across fonts and backends, prefixing each parent reason with a bullet and each sub-reason with an en-dash. Passing `bullets = FALSE` to `flowchart()` removes those markers and separates the levels by indentation alone; this applies to flat and nested reason breakdowns alike, as well as to the per-source counts of a PRISMA flow. For inline italic and bold emphasis, `formatting = "rich"` switches to Graphviz's HTML-like labels, as described in the [Graphviz Export](graphviz_export.html) vignette.

---

# Visual Customization

Every `grid` rendering function—`flowchart()`, `flowsave()`, and the measurement helper `recdims()`—accepts a common set of parameters controlling the appearance of the diagram. These apply uniformly across all flow topologies. The Graphviz/DOT engine has its own styling arguments, documented in the [Graphviz Export](graphviz_export.html) vignette.

| Parameter       | Description                                         | Default                                           |
|:----------------|:----------------------------------------------------|:--------------------------------------------------|
| `cex`           | Font size multiplier for main text                  | 0.85                                              |
| `cex_side`      | Font size multiplier for side boxes                 | Same as `cex`                                     |
| `cex_phase`     | Font size multiplier for phase labels               | 0.9                                               |
| `count_first`   | Bold count before label in all boxes                | `FALSE`                                           |
| `box_fill`      | Fill color for main boxes                           | `"white"`                                         |
| `side_fill`     | Fill color for side (exclusion) boxes               | `"white"`                                         |
| `border_col`    | Border color for all boxes                          | `"black"`                                         |
| `arrow_col`     | Color for connector arrows                          | `"black"`                                         |
| `phase_fill`    | Fill color for phase strips                         | `"black"`                                         |
| `phase_text_col`| Text color for phase labels                         | `"white"`                                         |
| `font_family`   | Font family for all text                            | `"Helvetica"`                                     |
| `phase_multiline` | Wrap long phase labels across lines to fit their band | `TRUE`                                        |
| `phase_max_lines` | Maximum wrapped lines per phase label             | `3`                                               |
| `number_format` | Locale preset (`"us"`, `"eu"`, `"space"`, `"none"`) or `c(big, decimal)` pair | `options(selecta.number_format)`, falls back to `"us"` |
| `vpad`          | Vertical spacing between elements (inches)          | 0.25 (or `options(selecta.vpad)`)                 |
| `margin`        | Fixed margin on all sides (inches)                  | 0.25                                              |

The examples below apply these parameters to a representative two-arm trial:

```{r}
example10 <- enroll(n = 600, label = "Assessed for eligibility") |>
    phase("Enrollment") |>
    exclude("Excluded", n = 120,
            reasons = c("Did not meet criteria"   = 80,
                        "Declined to participate" = 40),
            included_label = "Randomized") |>
    phase("Allocation") |>
    allocate(labels = c("Intervention", "Control"), n = c(240, 240)) |>
    phase("Follow-up") |>
    exclude("Discontinued", n = c(18, 22)) |>
    phase("Analysis") |>
    endpoint("Analyzed")
```

## **Example 10:** Custom Font Sizes

For poster presentations or supplementary figures, font sizes can be scaled independently for the main text, side boxes, and phase labels:

```{r, echo = FALSE}
queue_flow(example10, cex = 1.0, cex_side = 0.8, cex_phase = 1.0)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example10, cex = 1.0, cex_side = 0.8, cex_phase = 1.0)
```

## **Example 11:** Custom Colors

Six independent color parameters control the appearance of the diagram. The example below applies a coordinated blue palette in which the main and side boxes share a pale blue fill, the borders and arrows are rendered in a deeper navy, and the phase strips invert this with a navy fill and white text:

```{r, echo = FALSE}
queue_flow(example10,
           box_fill       = "#f0f5ff",   # main flow boxes
           side_fill      = "#e8eef9",   # exclusion side boxes
           border_col     = "#1a365d",   # box borders (all)
           arrow_col      = "#2c5282",   # connector arrows
           phase_fill     = "#2c5282",   # vertical phase strips
           phase_text_col = "#ffffff")   # phase strip text
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example10,
          box_fill       = "#f0f5ff",
          side_fill      = "#e8eef9",
          border_col     = "#1a365d",
          arrow_col      = "#2c5282",
          phase_fill     = "#2c5282",
          phase_text_col = "#ffffff")
```

Each parameter accepts any color specification recognized by `grDevices` (named colors, hex codes, or `rgb()` calls). The defaults (`box_fill = "white"`, `side_fill = "white"`, `border_col = "black"`, `arrow_col = "black"`, `phase_fill = "black"`, `phase_text_col = "white"`) reproduce the standard EQUATOR style.

## **Example 12:** Font Family

The `font_family` argument sets the typeface used for every element of a grid-rendered diagram. It accepts the portable generic families recognized by the R graphics device—`"sans"`, `"serif"`, and `"mono"`—as well as any installed system font name; the default, `"Helvetica"`, is a sans-serif face. Generic families are recommended for reproducibility, as they resolve to an appropriate face on every platform. The example below renders the trial in a serif typeface:

```{r, echo = FALSE}
queue_flow(example10, font_family = "serif")
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example10, font_family = "serif")
```

Because box dimensions are derived from the metrics of the selected font, the layout adjusts automatically to the chosen typeface; no manual resizing is required. The same argument is accepted by `flowsave()` and, for the Graphviz engine, by the DOT export functions, where the serif family corresponds to `"Times-Roman"`.

## **Example 13:** Regional Number Formatting

Counts are formatted with a US thousands separator by default (`1,200`). Three additional presets are available: EU-style formatting (`1.200`), SI/ISO 31-0 thin spaces (`1 200`), and no separator at all (`1200`). The `number_format` argument applies to any rendering function. The example below uses a manually constructed cohort sized to make the separators visually prominent at every node, rendered in EU style:

```{r}
example13 <- enroll(n = 25840, label = "Patients screened") |>
    phase("Screening") |>
    exclude("Did not meet eligibility criteria", n = 8420,
            reasons = c("Age outside range"     = 3210,
                        "Comorbidity exclusion" = 2840,
                        "Concurrent treatment"  = 2370),
            included_label = "Eligible") |>
    exclude("Declined to participate", n = 1820,
            included_label = "Consented") |>
    phase("Allocation") |>
    allocate(labels = c("Active", "Standard of care"),
             n = c(7800, 7800)) |>
    phase("Follow-up") |>
    exclude("Lost to follow-up", n = c(1240, 1310)) |>
    exclude("Discontinued intervention", n = c(250, 180)) |>
    phase("Analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example13, number_format = "eu")
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example13, number_format = "eu")
```

A custom two-element vector `c(big.mark, decimal.mark)` is also accepted in place of a named preset.

## Global Options

Two appearance settings may be fixed for an entire session rather than passed to every call. The number format and the vertical padding between elements each have a global option that propagates to every subsequent `flowchart()`, `flowsave()`, and `recdims()` call, as well as to the DOT engine:

```{r, eval = FALSE}
options(selecta.number_format = "space")   # SI/ISO thin-space separators
options(selecta.vpad = 0.35)               # looser vertical spacing (default 0.25)
```

Increasing `vpad` is useful for vertically dense diagrams—deep factorial designs, or split-and-recombine layouts with large per-stratum side boxes—where the default spacing would otherwise crowd the boxes. Per-call overrides take precedence over the global option.

---

# Multi-Line Phase Labels

Phase labels are drawn rotated within the vertical strips at the left margin, so a descriptive label may be longer than the group of boxes it spans. By default such a label is wrapped across several stacked lines, occupying additional strip width rather than forcing the surrounding diagram taller; the `phase_max_lines` argument caps the number of wrapped lines, with any remainder collapsed into the final line. The example below pairs each stage with an explanatory phrase:

## **Example 14:** Wrapped Phase Labels

```{r}
example14 <- enroll(n = 1200, label = "Assessed for eligibility") |>
    phase("Enrollment and baseline assessment") |>
    exclude("Excluded", n = 300,
            reasons = c("Not meeting criteria" = 160,
                        "Declined to participate" = 90,
                        "Other reasons" = 50),
            included_label = "Eligible cohort") |>
    phase("Randomized allocation to study arms") |>
    allocate(labels = c("Drug A", "Placebo"), n = c(450, 450)) |>
    phase("Post-randomization follow-up") |>
    exclude("Lost to follow-up", n = c(20, 20)) |>
    phase("Intention-to-treat analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example14)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example14)
```

Wrapping may be disabled with `phase_multiline = FALSE`, which forces every label onto a single line. A line break may also be placed explicitly with the newline character `"\n"`, which is honored whether or not automatic wrapping is active. Explicit breaks afford precise control over where a label divides:

## **Example 15:** Explicit Line Breaks

```{r}
example15 <- enroll(n = 1200, label = "Assessed for eligibility") |>
    phase("Enrollment\nand\nbaseline assessment") |>
    exclude("Excluded", n = 300, included_label = "Eligible cohort") |>
    phase("Allocation") |>
    allocate(labels = c("Drug A", "Placebo"), n = c(450, 450)) |>
    phase("Analysis") |>
    endpoint("Analyzed")
```

```{r, echo = FALSE}
queue_flow(example15)
```

```{r, use_rec_dims = TRUE, echo = TRUE}
flowchart(example15)
```

A label short enough to sit beside its boxes is left untouched, and the strip retains its standard width; wrapping engages only where a label would otherwise exceed the height available to it.

---

# Further Reading

- [Enrollment Diagrams](enrollment_diagrams.html): CONSORT, STROBE, and STARD diagrams with permanent parallel arms
- [Systematic Reviews](systematic_reviews.html): PRISMA and MOOSE diagrams with top-level source convergence
- [Split-and-Recombine Diagrams](split_recombine.html): Split topologies that fan out and converge back to a single stream
- [Graphviz Export](graphviz_export.html): DOT output for Graphviz/DiagrammeR rendering
