baptiste / gridExtra

Miscellaneous Functions for "Grid" Graphics
http://cran.r-project.org/web/packages/gridExtra/index.html
15 stars 4 forks source link

Using gtable in margins fails because grobHeight / grobWidth doesn't work for gtables #39

Closed stefanedwards closed 7 years ago

stefanedwards commented 7 years ago

Let's assume we want to combine multiple plots with a shared legend[^1]. We use gtable_filter to extract the legend, and apply it to the bottom.

dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
p1 <- qplot(carat, price, data = dsamp, colour = clarity) + theme(legend.position='hidden')
p2 <- qplot(cut, price, data = dsamp, colour = clarity) + theme(legend.position='hidden')
legend <- gtable_filter(ggplotGrob(p1 + theme(legend.position='bottom')), 'guide-box')
grid.arrange(p1, p2, legend, ncol=2, layout_matrix=matrix(c(1,3,2,3), ncol=2))

Except heights do not fit. So we re-try with the bottom argument:

grid.arrange(p1, p2, bottom=legend, ncol=2)

which fails with the message

Error in UseMethod("absolute.units") : 
  no applicable method for 'absolute.units' applied to an object of class "c('unit.list', 'unit')"

We can work around by using

grid.arrange(p1, p2, bottom=legend$grobs[[1]]$grobs[[1]], ncol=2)

This is unwieldy and only works if there is just a single guide. A layer of grobs can be left out if we use the old g_legend to extract the legend gtable.

A solution exists by modifying the arrangeGrob function, where grob heights/widths are calculated to insert relevant rows/columns. Take e.g. lines 166:172:

  if(is.grob(bottom)){
    h <- grobHeight(bottom) + padding
    gt <- gtable_add_rows(gt, heights = h, -1)
    gt <- gtable_add_grob(gt, bottom, 
                          t=nrow(gt), l=1, r=ncol(gt), z=Inf,
                          clip = clip)
  }

and modify as such:

  if(is.grob(bottom)){
    if (is.gtable(bottom)) {
      h <- sum(bottom$heights) + padding
    } else {
      h <- grobHeight(bottom) + padding
    }
    gt <- gtable_add_rows(gt, heights = h, -1)
    gt <- gtable_add_grob(gt, bottom, 
                          t=nrow(gt), l=1, r=ncol(gt), z=Inf,
                          clip = clip)
  }

Or, you know, customise grobHeight to do that for you...


[^1]: We do recall the grid_arrange_shared_legend function in (https://github.com/tidyverse/ggplot2/wiki/Share-a-legend-between-two-ggplot2-graphs.

baptiste commented 7 years ago

I reported the failure of gtable grobWidth/height methods 2 years ago, with no reaction. As far as gridExtra is concerned this is a problem in gtable. An acceptable workaround would be to duplicate such methods in gridExtra but with a definition that works better in such cases. Special-casing gtable grobs in grid.arrange because their grobHeight/width method is poorly defined is not a good option.

stefanedwards commented 7 years ago

Did you report the failure to the gtable package (i.e. Hadley) or the grid package? I agree with you, that ideally it could be the gtable package that defines a grobWidth method for the gtable class.

baptiste commented 7 years ago

to both, but it's really a gtable decision

On 2 May 2017 at 09:27, Stefan Hoj-Edwards notifications@github.com wrote:

Did you report the failure to the gtable package (i.e. Hadley) or the grid package? I agree with you, that ideally it could be the gtable package that defines a grobWidth method for the gtable class.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/baptiste/gridextra/issues/39#issuecomment-298437537, or mute the thread https://github.com/notifications/unsubscribe-auth/AABKGja2z3fSqN1PPcDzabeKUsCu37IHks5r1k5TgaJpZM4NNBcE .

baptiste commented 7 years ago

to both, but it's really a gtable decision. There is a grobWidth / widthDetails method, but I argued it wasn't very useful because it returned a size of 0 in some cases, instead of a more meaningful value corresponding to the cells' content (typically text grobs, IIRC).

stefanedwards commented 7 years ago

Thanks for this.