Search code examples
rlayoutgridr-grid

Center align bottom legend viewport or grob relative to plot area with grid package


I'm trying to align a legend at the bottom of a chart, centered respectively to that chart. But I'm having trouble aligning it. The pictures below show the current rendering, where you can clearly see the legend is misaligned (red line for guidance).

enter image description here enter image description here

library(grid)
draw <- function() {
    masterLayout <- grid.layout(
        nrow    = 4,
        ncol    = 1,
        heights = unit(c(0.1, 0.7, 0.1, 0.1), rep("null", 4)))

    vp1 <- viewport(layout.pos.row=1, layout.pos.col = 1, name="title")
    vp2 <- viewport(layout.pos.row=2, layout.pos.col = 1, name="plot")
    vp3 <- viewport(layout.pos.row=3, layout.pos.col = 1, name="legend")
    vp4 <- viewport(layout.pos.row=4, layout.pos.col = 1, name="caption")

    pushViewport(
        vpTree(viewport(layout = masterLayout, name = "master"),
                        vpList(vp1, vp2, vp3, vp4)))

    ## Draw main plot
    seekViewport("plot")
    pushViewport(viewport(width=unit(.8, "npc")))
    grid.rect(gp=gpar("fill"="red")) # dummy chart
    popViewport(2)

    ## Draw legend
    seekViewport("legend")

    colors <- list(first="red", second="green", third="blue")
    data.names <- names(colors)

    legend.cols <- length(data.names)
    pushViewport(viewport(
        width  = unit(0.8, "npc"),
        layout = grid.layout(ncol=legend.cols * 2,
                             nrow=1,
                             widths=unit(2.5, "cm"),
                             heights=unit(0.25, "npc"))))

    idx <- 0
    for(name in data.names)  {
        idx <- idx + 1
        pushViewport(viewport(layout.pos.row=1, layout.pos.col=idx))
        grid.circle(x=0, r=0.35, gp=gpar(fill=colors[[name]], col=NA))
        popViewport()

        idx <- idx + 1
        pushViewport(viewport(layout.pos.row=1, layout.pos.col=idx))
        grid.text(x=unit(-0.8, "npc"), "text", just="left")
        popViewport()
    }

    popViewport(2)
}
draw()

Solution

  • I don't understand why you're doing so much with individual viewports. It makes it very complex. I would have thought it was much easier to have one viewport for the legend and then control the x coordinate of the text and circles relative to that. Something like this; I'm not sure it's exactly what you want but it feels it should be easy to control if you need to tweak it:

    library(grid)
    draw <- function() {
        masterLayout <- grid.layout(
            nrow    = 4,
            ncol    = 1,
            heights = unit(c(0.1, 0.7, 0.1, 0.1), rep("null", 4)))
    
        vp1 <- viewport(layout.pos.row=1, layout.pos.col = 1, name="title")
        vp2 <- viewport(layout.pos.row=2, layout.pos.col = 1, name="plot")
        vp3 <- viewport(layout.pos.row=3, layout.pos.col = 1, name="legend")
        vp4 <- viewport(layout.pos.row=4, layout.pos.col = 1, name="caption")
    
        pushViewport(
            vpTree(viewport(layout = masterLayout, name = "master"),
                            vpList(vp1, vp2, vp3, vp4)))
    
        ## Draw main plot
        seekViewport("plot")
        pushViewport(viewport(width=unit(.8, "npc")))
        grid.rect(gp=gpar("fill"="red")) # dummy chart
        popViewport(2)
    
        ## Draw legend
        seekViewport("legend")
    
        colors <- list(first="red", second="green", third="blue")
        lab_centers <- seq(from = 0.2, to = 0.8, length = length(colors))
        disp <- 0.03 # how far to left of centre circle is, and to right text is, in each label
    
        for(i in 1:length(colors)){
          grid.circle(x = lab_centers[i] - disp, r = 0.1, gp=gpar(fill = colors[[i]], col=NA))
          grid.text("text", x = lab_centers[i] + disp)
    
        }
    
    
        popViewport(2)
    }
    draw()
    grid.lines(c(0.5, 0.5), c(0, 1))
    

    enter image description here

    If your legend labels aren't all the same length, you probably need to left align them and tweak the way I've used a disp parameter but shouldn't be too hard.