You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何提取coord_radial()的坐标原点以对齐ggplot2环形图?

ggplot2环形图theta轴对齐问题修复

问题背景

使用自定义函数draw_ggcircle_list拼接两个环形图时,x轴(theta轴)无法对齐,核心原因是无法正确匹配coord_radial()的坐标原点,导致内层图未定位到外层图的原点上,最终出现错位效果:

环形图未对齐效果

原代码如下:

library(ggplot2)

# draw_circle_list will convert all plot into polar coordinate
p1 <- ggplot(mpg, aes(class, displ)) +
    geom_boxplot() +
    theme(plot.background = element_rect(fill = "red")) +
    ggplot2::guides(
        theta = ggplot2::guide_axis_theta(angle = 0),
        r     = ggplot2::guide_axis(angle = 0)
    ) +
    ggplot2::theme(axis.line.theta = ggplot2::element_line())

draw_ggcircle_list <- function(plot_list, inner_radius = 0.1) {
    sizes <- rep_len(1, length(plot_list))

    # for every plot track, all relative to the total radius `1`
    plot_track <- sizes / sum(sizes) * (1 - inner_radius)
    plot_sizes <- 1 - cumsum(c(0, plot_track[-length(plot_track)]))
    plot_inner <- plot_sizes - plot_track
    vp <- plot_table <- NULL
    for (i in rev(seq_along(plot_list))) { # from inner-most to the out-most
        plot_size <- plot_sizes[[i]]
        plot <- .subset2(plot_list, i) +
            ggplot2::coord_radial(
                start = 0.25 * pi, end = 1.75 * pi,
                inner.radius = plot_inner[[i]] / plot_size,
                r.axis.inside = TRUE
            ) +
            ggplot2::ylab(NULL)

        # copied from `ggplot2:::ggplot_gtable`
        data <- ggplot2::ggplot_build(plot)
        plot <- data$plot
        layout <- data$layout
        data <- data$data

        theme <- ggplot2:::plot_theme(plot$theme)
        geom_grobs <- ggplot2:::by_layer(
            function(l, d) l$draw_geom(d, layout),
            plot$layers, data,
            "converting geom to grob"
        )
        gt <- layout$render(geom_grobs, data, theme, plot$labels)
        if (is.null(plot_table)) {
            plot_table <- gt
        } else {
            # for each inner gtable, we insert it to the panel area of the
            # outter gtable
            # how to get the coordinate origin from the `coord_radial()` ?
            origin <- c(0.5, 0.5)
            # extract the panel size of the inner track
            panel_loc <- ggplot2::find_panel(plot_table)
            width <- as.numeric(plot_table$widths[.subset2(panel_loc, "l")]) *
                panel_size / plot_size
            height <- as.numeric(plot_table$heights[.subset2(panel_loc, "t")]) *
                panel_size / plot_size
            vp <- grid::viewport(
                width = grid::unit(width, "npc"),
                height = grid::unit(height, "npc"),
                x = origin[1L], y = origin[2L],
                default.units = "native"
            )
            plot_table$vp <- vp
            # add the inner track to the panel area of the outter track
            out_panel <- ggplot2::find_panel(gt)
            plot_table <- gtable::gtable_add_grob(
                gt,
                plot_table,
                t = .subset2(out_panel, "t"),
                l = .subset2(out_panel, "l"),
                b = .subset2(out_panel, "b"),
                r = .subset2(out_panel, "r"),
                name = paste("track", i, sep = "-")
            )
        }
        panel_size <- plot_size # the last plot panel size
    }
    grid::grid.newpage()
    grid::pushViewport(grid::viewport())
    grid::grid.draw(plot_table)
}
draw_ggcircle_list(list(p1, p1))

修复方案

问题根源在于视口(viewport)的定位和缩放逻辑错误,以及gtable插入顺序不当。以下是修正后的代码:

library(ggplot2)
library(grid)
library(gtable)

# 基础环形图示例
p1 <- ggplot(mpg, aes(class, displ)) +
    geom_boxplot() +
    theme(plot.background = element_rect(fill = "red")) +
    guides(
        theta = guide_axis_theta(angle = 0),
        r     = guide_axis(angle = 0)
    ) +
    theme(axis.line.theta = element_line())

draw_ggcircle_list <- function(plot_list, inner_radius = 0.1) {
    sizes <- rep_len(1, length(plot_list))

    # 计算每个环形的半径范围(相对总半径1)
    plot_track <- sizes / sum(sizes) * (1 - inner_radius)
    plot_sizes <- 1 - cumsum(c(0, plot_track[-length(plot_track)]))
    plot_inner <- plot_sizes - plot_track
    
    plot_table <- NULL
    for (i in rev(seq_along(plot_list))) { # 从外层到内层依次构建
        plot_size <- plot_sizes[[i]]
        current_plot <- .subset2(plot_list, i) +
            coord_radial(
                start = 0.25 * pi, end = 1.75 * pi,
                inner.radius = plot_inner[[i]] / plot_size,
                r.axis.inside = TRUE
            ) +
            ylab(NULL)

        # 直接用ggplot_gtable构建可视化表格,无需手动复制内部逻辑
        gt <- ggplot_gtable(ggplot_build(current_plot))
        
        if (is.null(plot_table)) {
            plot_table <- gt
        } else {
            # 获取外层图的面板位置
            outer_panel <- find_panel(gt)
            # 计算内层图的缩放比例:匹配外层图的半径范围
            scale <- plot_size / plot_sizes[i+1]
            
            # 创建对齐原点的视口:中心为面板中心(即coord_radial的原点)
            vp <- viewport(
                width = unit(scale, "npc"),
                height = unit(scale, "npc"),
                x = unit(0.5, "npc"),
                y = unit(0.5, "npc")
            )
            
            # 给内层图绑定视口
            plot_table$vp <- vp
            
            # 将内层图嵌入到外层图的面板区域
            plot_table <- gtable_add_grob(
                gt,
                plot_table,
                t = outer_panel$t,
                l = outer_panel$l,
                b = outer_panel$b,
                r = outer_panel$r,
                name = paste("track", i, sep = "-")
            )
        }
        panel_size <- plot_size
    }
    grid.newpage()
    grid.draw(plot_table)
}

# 测试修正后的函数
draw_ggcircle_list(list(p1, p1))

关键修改点

  1. 简化gtable构建:直接使用ggplot_gtable(ggplot_build(current_plot))替代手动复制ggplot内部逻辑,避免潜在错误。
  2. 修正视口定位:将视口单位改为npc,以面板中心(0.5,0.5)作为原点,确保内层图与外层图的coord_radial原点对齐。
  3. 正确计算缩放比例:基于环形的半径范围计算缩放因子,保证内层图的尺寸与外层图的半径匹配,避免错位。
  4. 优化gtable插入逻辑:确保内层图正确嵌入到外层图的面板区域,维持层级关系。

内容的提问来源于stack exchange,提问作者Yun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 12:59:51