How to arrange plots with shared axes?

2019-07-21 19:41发布

问题:

I am trying to create an arrangement of three scatterplots with shared axes and marginal histograms. This seems like it should be simple, but it's giving me fits. I have tried approaches with gridExtra and gtable, both of which have gotten the general arrangement like I want but with alignments and plot sizes that were off.

There are many other posts related to this question and I have experimented with the answers given to many of them, especially answers from @baptiste here and here. The latter's approach to controlling width might be the key to the alignments, but I haven't understood it well enough to adapt it to my problem.

A minimal working example follows, preceded by its result. The result has quite a few problems:

  • The bottom right plot is bigger than the other two. This has been an issue with all approaches, including grid.arrange(), with which I placed a blank rectangle grob to balance the scatter plots.
  • The sizes of the maringal histograms are way off and not what I would expect from the code, which is that their narrow dimension would be about 1/4 the size of the scatter plots' width.
  • The spacing of the plots is too wide.
  • None of the marginal histograms align correctly with the scatter plots.
  • The marginal histogram on the right (c) is so narrow that it doesn't show up at all in the png, yet it does show up (though still too narrow) in the RStudio plot viewer, though only when "zoomed".

Bonus points for an answer that will align the plots' axes correctly even if the width of the axes labels varies between plots as such would be much more adaptable for future problems.

library(ggplot2)
library(grid)
library(gtable)
data <- data.frame(a = rnorm(100, 30, 3), b = rnorm(100, 40, 5), c=rnorm(100, 50, 3))
b_a.scatter <- ggplot(data, aes(x=a, y=b)) + geom_point() + coord_equal(ratio=1, xlim=c(0,100), ylim=c(0,100)) +
  theme(
    axis.text.x  = element_blank(),
    axis.title.x = element_blank(),
    axis.ticks.x = element_blank(),
    plot.margin  = unit(c(1,0,-0.5,1), "cm")
  )

c_a.scatter <- ggplot(data, aes(x=a, y=c)) + geom_point() + coord_equal(ratio=1, xlim=c(0,100), ylim=c(0,100)) +
  theme(plot.margin = unit(c(-0.5,0,0.5,1), "cm"))

c_b.scatter <- ggplot(data, aes(x=b, y=c)) + geom_point() + coord_equal(ratio=1, xlim=c(0,100), ylim=c(0,100)) +
  theme(
    axis.text.y  = element_blank(),
    axis.title.y = element_blank(),
    axis.ticks.y = element_blank(),
    plot.margin  = unit(c(-0.5,0,0.5,0), "cm")
  )

a.hist <- ggplot(data, aes(x=a)) + geom_histogram() + coord_equal(xlim=c(0,100), ratio=1/4) +
  theme(
    axis.text.x  = element_blank(),
    axis.title.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.text.y  = element_blank(),
    axis.title.y = element_blank(),
    axis.ticks.y = element_blank(),
    plot.margin = unit(c(0,0,1,1), "cm")
  )

b.hist <- ggplot(data, aes(x=b)) + geom_histogram() + coord_equal(xlim=c(0,100), ratio=1/4) +
  theme(
    axis.text.x  = element_blank(),
    axis.title.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.text.y  = element_blank(),
    axis.title.y = element_blank(),
    axis.ticks.y = element_blank(),
    plot.margin = unit(c(0,0,1,0), "cm")
  )

c.hist <- ggplot(data, aes(x=c)) + geom_histogram() + coord_flip(xlim=c(0,100)) +
  theme(
    axis.text.x  = element_blank(),
    axis.title.x = element_blank(),
    axis.ticks.x = element_blank(),
    plot.margin = unit(c(0,1,0,0), "cm")
  )

blankPanel <- grid.rect(gp=gpar(col="white"))

gt <- gtable(widths  = unit(rep(1,9), "null"),
             heights = unit(rep(1,9), "null"),
             respect=T)
gl <- list(ggplotGrob(b_a.scatter),
             ggplotGrob(c_a.scatter), ggplotGrob(c_b.scatter), ggplotGrob(c.hist),
             ggplotGrob(a.hist), ggplotGrob(b.hist))

gt <- gtable_add_grob(gt, gl,
                l=c(1,1,5,9,1,5),
                r=c(4,4,8,9,4,8),
                t=c(1,5,5,5,9,9),
                b=c(4,8,8,8,9,9))

grid.newpage()
png('multiplot.png')
grid.draw(gt)
dev.off()

回答1:

Here's one approach: combine graphs column by column using rbind, then cbind the three columns. Dummy gtables are provided for the empty cells, they only contain layout information.

library(ggplot2)
library(gtable)
d <- data.frame(a = rnorm(100, 30, 3), 
                b = rnorm(100, 40, 5), 
                c=rnorm(100, 50, 3))

theme_set(theme_bw() + 
            theme(plot.background=element_rect(colour="red",size = 2)))
## define simpler plots
a <- ggplot(d, aes(x=a, y=b)) + geom_point() + xlim(0,100)+ ylim(0,90)
b <- ggplot(d, aes(x=a, y=c)) + geom_point() + xlim(0,100) + ylim(0,90)
c <- ggplot(d, aes(x=b, y=c)) + geom_point() + xlim(0,100) + ylim(0,90)
ah <- ggplot(d, aes(x=a)) + geom_histogram() + 
  xlim(0,100)+ ylim(0,2000000) 
bh <- ggplot(d, aes(x=b)) + geom_histogram() + 
  xlim(0,100) + ylim(0,2000000) 
ch <- ggplot(d, aes(x=c)) + geom_histogram() + 
  coord_flip(xlim=c(0,200000))

pl <- lapply(list(a,b,c,ah,bh,ch), ggplotGrob)

## function to create a dummy table (no grobs, zero size) of the right dim for (r/c)bind
dummy_gtable <- function(g){
  gtable(widths=unit(rep(0,ncol(g)), 'null'), heights=unit(rep(0, nrow(g)), 'null'))
}

left <- rbind(pl[[1]],pl[[2]],pl[[4]])
middle <- rbind(dummy_gtable(pl[[1]]),pl[[3]],pl[[5]])
right <- rbind(dummy_gtable(pl[[1]]),pl[[6]], dummy_gtable(pl[[5]]))
grid.newpage()
grid.draw(cbind(left, middle, right))

Note that I'm using cbind and rbind from my experimental fork of gtable, because the released version does not use unit.pmax as unit comparison for widths and heights. Custom functions could be borrowed from another question to keep using the stable gtable version.