How to use facets with a dual y-axis ggplot

Dan picture Dan · Nov 13, 2014 · Viewed 12k times · Source

I have been trying to extend my scenario from here to make use of facets (specifically facet_grid()).

I have seen this example, however I can't seem to get it to work for my geom_bar() and geom_point() combo. I attempted to use the code from the example just changing from facet_wrap to facet_grid which also seemed to make the first layer not show.

I am very much a novice when it comes to grid and grobs so if someone can give some guidance on how to make P1 show up with the left y axis and P2 show up on the right y axis that would be great.

Data

library(ggplot2)
library(gtable)
library(grid)
library(data.table)
library(scales)

grid.newpage()

dt.diamonds <- as.data.table(diamonds)

d1 <- dt.diamonds[,list(revenue = sum(price),
                        stones = length(price)),
                  by=c("clarity","cut")]

setkey(d1, clarity,cut)

p1 & p2

p1 <- ggplot(d1, aes(x=clarity,y=revenue, fill=cut)) +
  geom_bar(stat="identity") +
  labs(x="clarity", y="revenue") +
  facet_grid(. ~ cut) +
  scale_y_continuous(labels=dollar, expand=c(0,0)) + 
  theme(axis.text.x = element_text(angle = 90, hjust = 1),
        axis.text.y = element_text(colour="#4B92DB"), 
        legend.position="bottom")

p2 <- ggplot(d1, aes(x=clarity, y=stones, colour="red")) +
  geom_point(size=6) + 
  labs(x="", y="number of stones") + expand_limits(y=0) +
  scale_y_continuous(labels=comma, expand=c(0,0)) +
  scale_colour_manual(name = '',values =c("red","green"), labels = c("Number of Stones"))+
  facet_grid(. ~ cut) +
  theme(axis.text.y = element_text(colour = "red")) +
  theme(panel.background = element_rect(fill = NA),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.border = element_rect(fill=NA,colour="grey50"),
        legend.position="bottom")

Attempt to combine (based on example linked above) This fails in the first for loop, I suspect to the hard coding of geom_point.points, however I don't know how to make it suit my charts (or fluid enough to suit a variety of charts)

# extract gtable
g1 <- ggplot_gtable(ggplot_build(p1))
g2 <- ggplot_gtable(ggplot_build(p2))

combo_grob <- g2
pos <- length(combo_grob) - 1
combo_grob$grobs[[pos]] <- cbind(g1$grobs[[pos]],
                                 g2$grobs[[pos]], size = 'first')

panel_num <- length(unique(d1$cut))
for (i in seq(panel_num))
{
   grid.ls(g1$grobs[[i + 1]])
  panel_grob <- getGrob(g1$grobs[[i + 1]], 'geom_point.points',
                        grep = TRUE, global = TRUE)
  combo_grob$grobs[[i + 1]] <- addGrob(combo_grob$grobs[[i + 1]], 
                                       panel_grob)
}       


pos_a <- grep('axis_l', names(g1$grobs))
axis <- g1$grobs[pos_a]
for (i in seq(along = axis))
{
  if (i %in% c(2, 4))
  {
    pp <- c(subset(g1$layout, name == paste0('panel-', i), se = t:r))

    ax <- axis[[1]]$children[[2]]
    ax$widths <- rev(ax$widths)
    ax$grobs <- rev(ax$grobs)
    ax$grobs[[1]]$x <- ax$grobs[[1]]$x - unit(1, "npc") + unit(0.5, "cm")
    ax$grobs[[2]]$x <- ax$grobs[[2]]$x - unit(1, "npc") + unit(0.8, "cm")
    combo_grob <- gtable_add_cols(combo_grob, g2$widths[g2$layout[pos_a[i],]$l], length(combo_grob$widths) - 1)
    combo_grob <- gtable_add_grob(combo_grob, ax,  pp$t, length(combo_grob$widths) - 1, pp$b)
  }
}

pp <- c(subset(g1$layout, name == 'ylab', se = t:r))

ia <- which(g1$layout$name == "ylab")
ga <- g1$grobs[[ia]]
ga$rot <- 270
ga$x <- ga$x - unit(1, "npc") + unit(1.5, "cm")

combo_grob <- gtable_add_cols(combo_grob, g2$widths[g2$layout[ia,]$l], length(combo_grob$widths) - 1)
combo_grob <- gtable_add_grob(combo_grob, ga, pp$t, length(combo_grob$widths) - 1, pp$b)
combo_grob$layout$clip <- "off"

grid.draw(combo_grob)

EDIT to attempt to make workable for facet_wrap

The following code still works with facet_grid using ggplot2 2.0.0

g1 <- ggplot_gtable(ggplot_build(p1))
g2 <- ggplot_gtable(ggplot_build(p2))

pp <- c(subset(g1$layout, name == "panel", se = t:r))
g <- gtable_add_grob(g1, g2$grobs[which(g2$layout$name == "panel")], pp$t,
                     pp$l, pp$b, pp$l)
# axis tweaks
ia <- which(g2$layout$name == "axis-l")
ga <- g2$grobs[[ia]]
ax <- ga$children[[2]]
ax$widths <- rev(ax$widths)
ax$grobs <- rev(ax$grobs)
ax$grobs[[1]]$x <- ax$grobs[[1]]$x - unit(1, "npc") + unit(0.15, "cm")
g <- gtable_add_cols(g, g2$widths[g2$layout[ia, ]$l], length(g$widths) - 1)
g <- gtable_add_grob(g, ax, unique(pp$t), length(g$widths) - 1)

# Add second y-axis title
ia <- which(g2$layout$name == "ylab")
ax <- g2$grobs[[ia]]
# str(ax) # you can change features (size, colour etc for these -
# change rotation below
ax$rot <- 90
g <- gtable_add_cols(g, g2$widths[g2$layout[ia, ]$l], length(g$widths) - 1)
g <- gtable_add_grob(g, ax, unique(pp$t), length(g$widths) - 1)

# Add legend to the code
leg1 <- g1$grobs[[which(g1$layout$name == "guide-box")]]
leg2 <- g2$grobs[[which(g2$layout$name == "guide-box")]]

g$grobs[[which(g$layout$name == "guide-box")]] <-
  gtable:::cbind_gtable(leg1, leg2, "first")

grid.draw(g)

Answer

Axeman picture Axeman · Nov 22, 2016

Now that ggplot2 has secondary axis support this has become much much easier in many (but not all) cases. No grob manipulation needed.

Even though it is supposed to only allow for simple linear transformations of the same data, such as different measurement scales, we can manually rescale one of the variables first to at least get a lot more out of that property.

library(tidyverse)

max_stones <- max(d1$stones)
max_revenue <- max(d1$revenue)

d2 <- gather(d1, 'var', 'val', stones:revenue) %>% 
  mutate(val = if_else(var == 'revenue', as.double(val), val / (max_stones / max_revenue)))

ggplot(mapping = aes(clarity, val)) +
  geom_bar(aes(fill = cut), filter(d2, var == 'revenue'), stat = 'identity') +
  geom_point(data = filter(d2, var == 'stones'), col = 'red') +
  facet_grid(~cut) +
  scale_y_continuous(sec.axis = sec_axis(trans = ~ . * (max_stones / max_revenue),
                                         name = 'number of stones'),
                     labels = dollar) +
  theme(axis.text.x = element_text(angle = 90, hjust = 1),
        axis.text.y = element_text(color = "#4B92DB"),
        axis.text.y.right = element_text(color = "red"),
        legend.position="bottom") +
  ylab('revenue')

enter image description here

It also works nicely with facet_wrap:

enter image description here

Other complications, such as scales = 'free' and space = 'free' are also done easily. The only restriction is that the relationship between the two axes is equal for all facets.