@@ -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+ }
0 commit comments