Skip to content

Commit d6994d2

Browse files
committed
Add circular forest plot with br_show_forest_circle() from #42
Introduces the br_show_forest_circle() function for visualizing regression results as circular (polar) forest plots. Updates documentation, NAMESPACE, and pkgdown config to include the new function, and adds tests and references in related documentation files.
1 parent bc4b51a commit d6994d2

19 files changed

Lines changed: 404 additions & 4 deletions

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export(br_show_coxph_diagnostics)
2929
export(br_show_fitted_line)
3030
export(br_show_fitted_line_2d)
3131
export(br_show_forest)
32+
export(br_show_forest_circle)
3233
export(br_show_forest_ggstats)
3334
export(br_show_forest_ggstatsplot)
3435
export(br_show_nomogram)
@@ -59,6 +60,7 @@ importFrom(ggplot2,ggproto)
5960
importFrom(ggplot2,labs)
6061
importFrom(ggplot2,theme)
6162
importFrom(ggplot2,zeroGrob)
63+
importFrom(grDevices,rainbow)
6264
importFrom(lifecycle,deprecated)
6365
importFrom(mirai,.progress)
6466
importFrom(rlang,.data)

NEWS.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# bregr (development version)
22

3-
* Added `br_show_coxph_diagnostics()` for CoxPH diagnostic plots.
4-
* Added `br_show_nomogram()`.
5-
* Fixed interaction term display and factor variable scaling in `br_show_nomogram()`.
6-
* Added `dry_run` option to `br_pipeline()`.
3+
**Enhancements & New Features:**
4+
5+
- Introduced `br_show_forest_circle()`for circular forest plots.
6+
- Added diagnostic visualization for Cox PH models via `br_show_coxph_diagnostics()`.
7+
- Implemented `br_show_nomogram()`for clinical prediction modeling.
8+
- Added `dry_run`option to `br_pipeline()`for pipeline validation.
9+
10+
**Fixes & Improvements:**
11+
12+
- Resolved interaction term display and factor scaling in `br_show_nomogram()`.
713

814
# bregr 1.1.0
915

R/00-bregr-package.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#' @importFrom rlang .data .env
1111
#' @importFrom utils combn packageDescription packageVersion
1212
#' @importFrom stats cor pchisq quantile predict sd nobs fitted
13+
#' @importFrom grDevices rainbow
1314
#' @rawNamespace if (getRversion() < "4.3.0") importFrom("S7", "@")
1415
## usethis namespace: end
1516
NULL

R/04-show.R

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,3 +1409,277 @@ br_show_nomogram <- function(breg,
14091409
cli::cli_abort("Nomograms are currently supported for Cox regression (coxph) and linear/generalized linear models (lm/glm)")
14101410
}
14111411
}
1412+
1413+
#' Show a circular forest plot for regression results
1414+
#'
1415+
#' @description
1416+
#' `r lifecycle::badge('experimental')`
1417+
#'
1418+
#' This function creates a circular (polar) forest plot from regression results,
1419+
#' providing an alternative visualization to the traditional linear forest plot.
1420+
#' The function uses the same input as [br_show_forest()] but displays the results
1421+
#' in a circular format using [ggplot2::coord_polar()].
1422+
#'
1423+
#' @param breg A regression object with results.
1424+
#' @param rm_controls If `TRUE`, remove control terms.
1425+
#' @param style Character string specifying the style of circular forest plot.
1426+
#' Options are:
1427+
#' - `"points"` (default): Display point estimates with error bars in circular format
1428+
#' - `"bars"`: Display as bars with points overlaid
1429+
#' @param ref_line Logical or numeric. If `TRUE`, shows reference circle at default value
1430+
#' (1 for exponentiated estimates, 0 for regular estimates).
1431+
#' If numeric, shows reference circle at specified value.
1432+
#' If `FALSE`, no reference circle is shown.
1433+
#' @param sort_by Character string specifying how to sort the variables.
1434+
#' Options are:
1435+
#' - `"none"` (default): No sorting, use original order
1436+
#' - `"estimate"`: Sort by effect estimate (ascending)
1437+
#' - `"estimate_desc"`: Sort by effect estimate (descending)
1438+
#' - `"pvalue"`: Sort by p-value (ascending, most significant first)
1439+
#' - `"variable"`: Sort alphabetically by variable name
1440+
#' @param subset Expression for subsetting the results data (`br_get_results(breg)`).
1441+
#' @param log_first Log transformed the estimates and their confident intervals.
1442+
#' @returns A ggplot object
1443+
#' @export
1444+
#' @family br_show
1445+
#' @examples
1446+
#' m <- br_pipeline(mtcars,
1447+
#' y = "mpg",
1448+
#' x = colnames(mtcars)[2:4],
1449+
#' x2 = "vs",
1450+
#' method = "gaussian"
1451+
#' )
1452+
#' br_show_forest_circle(m)
1453+
#' br_show_forest_circle(m, style = "bars")
1454+
#' br_show_forest_circle(m, sort_by = "estimate")
1455+
#' br_show_forest_circle(m, ref_line = FALSE)
1456+
#' br_show_forest_circle(m, ref_line = 0.5)
1457+
#' @testexamples
1458+
#' assert_s3_class(br_show_forest_circle(m), "ggplot")
1459+
br_show_forest_circle <- function(
1460+
breg,
1461+
rm_controls = FALSE,
1462+
style = c("points", "bars"),
1463+
ref_line = TRUE,
1464+
sort_by = c("none", "estimate", "estimate_desc", "pvalue", "variable"),
1465+
subset = NULL,
1466+
log_first = FALSE) {
1467+
assert_breg_obj_with_results(breg)
1468+
assert_bool(rm_controls)
1469+
style <- match.arg(style)
1470+
sort_by <- match.arg(sort_by)
1471+
1472+
# Get the data using br_get_results
1473+
dt <- br_get_results(breg)
1474+
1475+
if (log_first) {
1476+
dt <- dt |> dplyr::mutate(
1477+
estimate = log(.data$estimate),
1478+
conf.high = log(.data$conf.high),
1479+
conf.low = log(.data$conf.low)
1480+
)
1481+
}
1482+
1483+
# Determine reference line value based on exponentiate attribute
1484+
exponentiate <- attr(breg, "exponentiate")
1485+
default_ref_value <- if (exponentiate && !log_first) 1L else 0L
1486+
1487+
# Handle ref_line parameter following br_show_forest design
1488+
if (is.logical(ref_line)) {
1489+
if (ref_line) {
1490+
ref_line_value <- default_ref_value
1491+
show_ref_line <- TRUE
1492+
} else {
1493+
show_ref_line <- FALSE
1494+
ref_line_value <- NULL
1495+
}
1496+
} else if (is.numeric(ref_line)) {
1497+
ref_line_value <- ref_line
1498+
show_ref_line <- TRUE
1499+
} else {
1500+
cli_abort("ref_line must be logical or numeric")
1501+
}
1502+
1503+
if (rm_controls) {
1504+
dt <- dt |> dplyr::filter(.data$Focal_variable == .data$variable)
1505+
}
1506+
1507+
subset <- rlang::enquo(subset)
1508+
if (!rlang::quo_is_null(subset)) {
1509+
dt <- dt |> dplyr::filter(!!subset)
1510+
}
1511+
1512+
# Enhanced data validation and cleaning
1513+
dt <- dt |>
1514+
dplyr::mutate(
1515+
# Handle infinite and missing values
1516+
conf.low = dplyr::case_when(
1517+
is.na(.data$conf.low) | is.infinite(.data$conf.low) ~ .data$estimate,
1518+
TRUE ~ .data$conf.low
1519+
),
1520+
conf.high = dplyr::case_when(
1521+
is.na(.data$conf.high) | is.infinite(.data$conf.high) ~ .data$estimate,
1522+
TRUE ~ .data$conf.high
1523+
),
1524+
# Filter for valid estimates
1525+
valid_estimate = !is.na(.data$estimate) & !is.infinite(.data$estimate) &
1526+
!is.na(.data$conf.low) & !is.na(.data$conf.high)
1527+
) |>
1528+
dplyr::filter(.data$valid_estimate) |>
1529+
dplyr::select(-"valid_estimate")
1530+
1531+
# Check if we have valid data after filtering
1532+
if (nrow(dt) == 0) {
1533+
cli::cli_abort("no valid data to plot")
1534+
}
1535+
1536+
# Apply sorting
1537+
if (sort_by != "none") {
1538+
dt <- switch(sort_by,
1539+
"estimate" = dt |> dplyr::arrange(.data$estimate),
1540+
"estimate_desc" = dt |> dplyr::arrange(dplyr::desc(.data$estimate)),
1541+
"pvalue" = dt |> dplyr::arrange(.data$p.value),
1542+
"variable" = dt |> dplyr::arrange(.data$variable),
1543+
dt # fallback to original order
1544+
)
1545+
}
1546+
1547+
# Create display labels and positioning
1548+
dt <- dt |>
1549+
dplyr::mutate(
1550+
display_label = dplyr::case_when(
1551+
!is.na(.data$label) & .data$label != "" ~ .data$label,
1552+
TRUE ~ .data$variable
1553+
),
1554+
# Create unique labels in case of duplicates
1555+
display_label = make.unique(.data$display_label, sep = "_"),
1556+
x_pos = factor(.data$display_label, levels = unique(.data$display_label))
1557+
)
1558+
1559+
# Handle grouping for colors - robust approach
1560+
has_group <- !is.null(br_get_group_by(breg))
1561+
if (has_group && "Group_variable" %in% colnames(dt) && length(unique(dt$Group_variable)) > 1) {
1562+
color_var <- "Group_variable"
1563+
} else if ("Focal_variable" %in% colnames(dt) && length(unique(dt$Focal_variable)) > 1) {
1564+
color_var <- "Focal_variable"
1565+
} else {
1566+
color_var <- "variable"
1567+
}
1568+
1569+
# Create the base plot
1570+
if (style == "points") {
1571+
# Points style with proper error bars using segments for polar coordinates
1572+
p <- ggplot2::ggplot(dt, ggplot2::aes(x = .data$x_pos)) +
1573+
ggplot2::geom_point(
1574+
ggplot2::aes(y = .data$estimate, color = .data[[color_var]]),
1575+
size = 2
1576+
) +
1577+
ggplot2::geom_segment(
1578+
ggplot2::aes(
1579+
y = .data$conf.low,
1580+
yend = .data$conf.high,
1581+
color = .data[[color_var]]
1582+
),
1583+
linewidth = 0.8
1584+
)
1585+
} else {
1586+
# Bars style
1587+
base_offset <- max(abs(c(dt$conf.low, dt$conf.high, dt$estimate)), na.rm = TRUE) + 1
1588+
1589+
dt <- dt |>
1590+
dplyr::mutate(
1591+
bar_height = 1, # Base height for bars
1592+
point_y = .data$estimate + base_offset, # Offset points above bars
1593+
ci_low = .data$conf.low + base_offset, # Offset CI accordingly
1594+
ci_high = .data$conf.high + base_offset
1595+
)
1596+
1597+
p <- ggplot2::ggplot(dt, ggplot2::aes(x = .data$x_pos)) +
1598+
ggplot2::geom_col(
1599+
ggplot2::aes(y = .data$bar_height, fill = .data[[color_var]]),
1600+
alpha = 0.3, width = 1
1601+
) +
1602+
ggplot2::geom_point(
1603+
ggplot2::aes(y = .data$point_y, color = .data[[color_var]]),
1604+
size = 1.5
1605+
) +
1606+
ggplot2::geom_segment(
1607+
ggplot2::aes(
1608+
y = .data$ci_low,
1609+
yend = .data$ci_high,
1610+
color = .data[[color_var]]
1611+
),
1612+
linewidth = 0.8
1613+
)
1614+
}
1615+
1616+
# Convert to polar coordinates
1617+
p <- p + ggplot2::coord_polar()
1618+
1619+
# Add reference circle if requested
1620+
if (show_ref_line) {
1621+
ref_y <- if (style == "points") {
1622+
ref_line_value
1623+
} else {
1624+
ref_line_value + base_offset
1625+
}
1626+
p <- p + ggplot2::geom_hline(
1627+
yintercept = ref_y,
1628+
linetype = "dashed",
1629+
color = "gray60",
1630+
linewidth = 0.5
1631+
)
1632+
}
1633+
1634+
# Enhanced theming with proper axis display
1635+
axis_label <- if (log_first) "log(Estimate)" else "Estimate"
1636+
1637+
p <- p +
1638+
ggplot2::theme_minimal() +
1639+
ggplot2::theme(
1640+
# Remove default polar grid lines for cleaner visualization
1641+
panel.grid.major.x = ggplot2::element_blank(),
1642+
panel.grid.minor.x = ggplot2::element_blank(),
1643+
panel.grid.minor.y = ggplot2::element_blank(),
1644+
# Keep radial grid lines but make them subtle
1645+
panel.grid.major.y = ggplot2::element_line(
1646+
color = "gray80",
1647+
linewidth = 0.3,
1648+
linetype = "dotted"
1649+
),
1650+
# Display proper variable names on angular axis
1651+
axis.text.x = ggplot2::element_text(size = 8, color = "black"),
1652+
# Display numerical values on radial axis
1653+
axis.text.y = ggplot2::element_text(size = 8, color = "black"),
1654+
axis.title = ggplot2::element_blank(),
1655+
legend.position = "right",
1656+
plot.title = ggplot2::element_text(hjust = 0.5, size = 14),
1657+
legend.title = ggplot2::element_text(size = 10),
1658+
legend.text = ggplot2::element_text(size = 8),
1659+
panel.background = ggplot2::element_blank()
1660+
) +
1661+
ggplot2::labs(
1662+
title = glue::glue("Circular Forest Plot ({axis_label})"),
1663+
color = gsub("_", " ", color_var)
1664+
)
1665+
1666+
# Apply color palette
1667+
n_groups <- length(unique(dt[[color_var]]))
1668+
if (n_groups > 1) {
1669+
# Colors inspired by reference code
1670+
colors <- c("#3cc34e", "#00aeff", "#ff800e", "#6A51A3", "#2B8CBE", "#E31A1C", "#FF7F00", "#33A02C")
1671+
if (n_groups > length(colors)) {
1672+
colors <- rainbow(n_groups)
1673+
}
1674+
colors <- colors[1:n_groups]
1675+
1676+
p <- p + ggplot2::scale_color_manual(values = colors)
1677+
1678+
# Only add fill scale if style uses bars (which uses fill aesthetic)
1679+
if (style == "bars") {
1680+
p <- p + ggplot2::scale_fill_manual(values = colors, guide = "none")
1681+
}
1682+
}
1683+
1684+
return(p)
1685+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ reference:
2626
desc: Visualize results using forest plots and more.
2727
contents:
2828
- br_show_forest
29+
- br_show_forest_circle
2930
- br_show_risk_network
3031
- br_show_forest_ggstats
3132
- br_show_forest_ggstatsplot

man/br_show_coxph_diagnostics.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/br_show_fitted_line.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/br_show_fitted_line_2d.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/br_show_forest.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)