Introduction to mirrorslope

What a mirror slope plot shows

The mirrored slopegraph was designed by Kati Maharry and is described in Maharry and Sullivan (2026, manuscript in preparation). It was first used in the supplementary appendix of the HOPE-3 trial report (McDonald et al., Lancet 2026, doi:10.1016/S0140-6736(26)01385-1). This package implements the display.

A mirror slope plot compares two treatment arms over a single observation interval. Both arms share one x-axis and converge at a central endpoint, so the between-arm gap at that endpoint is the primary visual quantity. The reference arm is drawn on the left and the comparator on the right; both mean lines meet at the centre, corresponding to the endpoint visit.

The x-axis is positional, not numeric: it encodes baseline versus endpoint for each arm and should not be read as elapsed time.

Because mirror_slope_plot() returns a standard ggplot object, the full ggplot2 ecosystem remains available for further customisation.

The pipeline

A plot is built in stages. Each stage returns a mirror_data object, so the steps compose naturally with the pipe. The object stores everything available; the plot call exposes only what is requested.

  1. prep_mirror_data() validates and structures the analysis data.
  2. add_summary() attaches the group-level estimates that define the mean lines.
  3. add_precision() optionally attaches per-arm standard errors or intervals.
  4. add_difference() optionally attaches the between-arm difference and its precision and p-value.
  5. mirror_slope_plot() renders the figure.

A minimal example

We simulate a small change-from-baseline dataset with two arms and two visits.

set.seed(1)
n <- 40
df <- data.frame(
  USUBJID = rep(paste0("S", seq_len(n)), each = 2),
  TRTP    = rep(c(rep("Placebo", n / 2), rep("Active", n / 2)), each = 2),
  AVISIT  = rep(c("Baseline", "Week 24"), times = n),
  AVAL    = c(rbind(rep(0, n),
                    c(rnorm(n / 2, -10, 5), rnorm(n / 2, -6, 5)))),
  stringsAsFactors = FALSE
)
head(df, 4)
#>   USUBJID    TRTP   AVISIT       AVAL
#> 1      S1 Placebo Baseline   0.000000
#> 2      S1 Placebo  Week 24 -13.132269
#> 3      S2 Placebo Baseline   0.000000
#> 4      S2 Placebo  Week 24  -9.081783

Prepare the data and attach the summary estimates:

md <- prep_mirror_data(
  df,
  trt_levels    = c("Placebo", "Active"),
  time_baseline = "Baseline",
  time_endpoint = "Week 24",
  value_label   = "% Change from Baseline"
)

md <- add_summary(
  md,
  estimates  = c("Placebo" = -10.2, "Active" = -5.9),
  stat_label = "LS-Mean"
)

mirror_slope_plot(
  md,
  trt_colors = mirror_palette("colorblind"),
  title      = "Percent Change from Baseline at Week 24"
)

Adding the difference and relative benefit

Attaching a difference enables the annotation zone to the right of the panel. show_difference displays the difference value; show_slowing adds the derived relative benefit. Either one also draws the shaded band between the two means.

md <- add_difference(
  md,
  estimate = 4.3,
  ci_lower = 1.2,
  ci_upper = 7.4,
  p_value  = 0.011
)

mirror_slope_plot(
  md,
  trt_colors      = mirror_palette("colorblind"),
  show_difference = TRUE,
  show_slowing    = TRUE,
  title           = "Percent Change from Baseline at Week 24",
  footnote        = "LS-means from a mixed model. Positive difference favours the active arm."
)

Showing per-arm precision

add_precision() stores standard errors or confidence intervals per arm. Setting show_precision = TRUE draws a precision bar at each endpoint marker.

md <- add_precision(
  md,
  ci = list("Placebo" = c(-13.3, -7.1),
            "Active"  = c(-8.4, -3.4))
)

mirror_slope_plot(
  md,
  trt_colors      = mirror_palette("colorblind"),
  show_difference = TRUE,
  show_slowing    = TRUE,
  show_precision  = TRUE,
  title           = "Percent Change from Baseline at Week 24"
)

Individual subject trajectories

show_spaghetti = TRUE overlays each subject’s baseline-to-endpoint line beneath the group means. For large samples, reduce spaghetti_alpha to control overplotting.

mirror_slope_plot(
  md,
  trt_colors      = mirror_palette("colorblind"),
  show_spaghetti  = TRUE,
  spaghetti_alpha = 0.2,
  show_difference = TRUE,
  show_slowing    = TRUE,
  title           = "With individual trajectories"
)

Inspecting the object

Printing a mirror_data object summarises the arms, analysed N, statistic label, and which optional elements are attached. summary() returns per-arm, per-timepoint descriptives.

md
#> <mirror_data> schema 1.0
#>   Reference (left)  : Placebo  (n=20)
#>   Comparator (right): Active  (n=20)
#>   Timepoints        : Baseline  ->  Week 24
#>   Observations      : 80 OBS rows
#>   Summary           : yes (LS-Mean)
#>   Precision         : yes
#>   Difference        : yes
summary(md)
#>       ARM TIMEROLE  N      MEAN       MIN        MAX
#> 1  Active BASELINE 20  0.000000   0.00000  0.0000000
#> 2 Placebo BASELINE 20  0.000000   0.00000  0.0000000
#> 3  Active ENDPOINT 20 -6.032358 -15.94676  0.7933978
#> 4 Placebo ENDPOINT 20 -9.047381 -21.07350 -2.0235960

Next steps

See the Clinical workflow with ADaM data vignette for a realistic data-preparation pattern, and the Customising the plot vignette for theming, palettes, and combining multiple panels.