Search code examples
rggplot2polar-coordinates

How to extract the coodinate origin in `coord_radial()`


I want to align two circular plot in ggplot2 (draw_ggcircle_list), but I cannot extract the coordinate origin (0, 0) of the coord_radial(). Now following two plots won't aligned by the x-axis (theta-axis), since the inner plot is not put in the coordinate origin of the outter plot:

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))

enter image description here


Solution

  • I have fixed this, here is the code I used. I've built a new layout option in my package ggalign that arranges multiple plots in circular tracks. Each plot fits into its designated circle track while sharing the same coordinate origin (0, 0) based on this idea.

    
    draw_ggcircle_list <- function(plot_list, start = 0L, end = 2 * pi,
                                   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
        plot_table <- origin <- 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 = start, end = end,
                    inner.radius = plot_inner[[i]] / plot_size,
                    r.axis.inside = TRUE
                ) +
                ggplot2::labs(x = NULL, y = 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)
    
            # 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 <- layout$coord$transform(
            #     data.frame(x = 0.5, y = 0.5),
            #     panel_params = layout$panel_params[[1L]]
            # )
            # For bbox, `ggplot2::polar_bbox` always take (0.5, 0.5) as origin
            bbox <- layout$panel_params[[1L]]$bbox
            just <- c(
                scales::rescale(0.5, from = bbox$x),
                scales::rescale(0.5, from = bbox$y)
            )
    
            if (is.null(plot_table)) {
                plot_table <- gt
            } else {
                # define the panel size of the inner track
                rescale_factor <- last_plot_size / plot_size
    
                # just using panel spacing as the spacer between two plots
                spacing <- ggplot2::calc_element("panel.spacing.y", theme)
    
                plot_table <- grid::editGrob(plot_table, vp = grid::viewport(
                    width = grid::unit(rescale_factor, "npc") - spacing,
                    height = grid::unit(rescale_factor, "npc") - spacing,
                    x = origin[1L], y = origin[2L],
                    just = just,
                    default.units = "native",
                    clip = "off"
                ))
    
                # 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 = "inner-track"
                )
            }
            origin <- just
            last_plot_size <- plot_size # the last plot panel size
        }
        plot_table
    }
    
    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())
    
    grid::grid.draw(draw_ggcircle_list(list(p1, p1)))
    

    enter image description here