2

I am using facet_wrap and was also able to plot the secondary y-axis. However the labels are not getting plotted near the axis, rather they are plotted very far. I realise it all will get resolved if I understand how to manipulate the coordinate system of the gtable (t,b,l,r) of the grobs. Could someone explain how and what they actually depict - t:r = c(4,8,4,4) means what.

There are many links for secondary yaxis with ggplot, however when nrow/ncol is more than 1, they fails. So please teach me the basics of grid geometry and grobs location management.

Edit : the Code

this is the final code written by me :

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

# Data  
diamonds$cut <- sample(letters[1:13], nrow(diamonds), replace = TRUE)
dt.diamonds <- as.data.table(diamonds) 
d1 <- dt.diamonds[,list(revenue = sum(price),
                    stones = length(price)),
              by=c("clarity", "cut")]
setkey(d1, clarity, cut)

# The facet_wrap plots
p1 <- ggplot(d1, aes(x = clarity, y = revenue, fill = cut)) +
geom_bar(stat = "identity") +
labs(x = "clarity", y = "revenue") +
facet_wrap( ~ 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 = 4) + 
  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_wrap( ~ 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")


# Get the ggplot grobs
xx <- ggplot_build(p1)
g1 <- ggplot_gtable(xx)

yy <- ggplot_build(p2)
g2 <- ggplot_gtable(yy)

nrow = length(unique(xx$panel$layout$ROW))
ncol = length(unique(xx$panel$layout$COL))
npanel = length(xx$panel$layout$PANEL)

pp <- c(subset(g1$layout, grepl("panel", g1$layout$name), se = t:r))
g <- gtable_add_grob(g1, g2$grobs[grepl("panel", g1$layout$name)], 
                     pp$t, pp$l, pp$b, pp$l)

hinvert_title_grob <- function(grob){
  widths <- grob$widths
  grob$widths[1] <- widths[3]
  grob$widths[3] <- widths[1]
  grob$vp[[1]]$layout$widths[1] <- widths[3]
  grob$vp[[1]]$layout$widths[3] <- widths[1]

  grob$children[[1]]$hjust <- 1 - grob$children[[1]]$hjust 
  grob$children[[1]]$vjust <- 1 - grob$children[[1]]$vjust 
  grob$children[[1]]$x <- unit(1, "npc") - grob$children[[1]]$x
  grob
}

j = 1
k = 0

for(i in 1:npanel){
  if ((i %% ncol == 0) || (i == npanel)){
    k = k + 1
    index <- which(g2$layout$name == "axis_l-1")  # Which grob
    yaxis <- g2$grobs[[index]]                    # Extract the grob
    ticks <- yaxis$children[[2]]
    ticks$widths <- rev(ticks$widths)
    ticks$grobs <- rev(ticks$grobs)
    ticks$grobs[[1]]$x <- ticks$grobs[[1]]$x - unit(1, "npc")
    ticks$grobs[[2]] <- hinvert_title_grob(ticks$grobs[[2]])
    yaxis$children[[2]] <- ticks
    if (k == 1)#to ensure just once d secondary axisis printed 
      g <- gtable_add_cols(g,g2$widths[g2$layout[index,]$l],
              max(pp$r[j:i]))
      g <- gtable_add_grob(g,yaxis,max(pp$t[j:i]),max(pp$r[j:i])+1,
                 max(pp$b[j:i])
                     , max(pp$r[j:i]) + 1, clip = "off", name = "2ndaxis")
     j = i + 1
  }
}

# inserts the label for 2nd y-axis 
loc_1st_yaxis_label <- c(subset(g$layout, grepl("ylab", g$layout$name), se  
                       = t:r)) 
loc_2nd_yaxis_max_r <- c(subset(g$layout, grepl("2ndaxis", g$layout$name), 
                      se = t:r))
zz <- max(loc_2nd_yaxis_max_r$r)+1
loc_1st_yaxis_label$l <- zz
loc_1st_yaxis_label$r <- zz

index <- which(g2$layout$name == "ylab") 
ylab <- g2$grobs[[index]]                # Extract that grob
ylab <- hinvert_title_grob(ylab)  
ylab$children[[1]]$rot <- ylab$children[[1]]$rot + 180
g <- gtable_add_grob(g, ylab, loc_1st_yaxis_label$t, loc_1st_yaxis_label$l
                     , loc_1st_yaxis_label$b, loc_1st_yaxis_label$r
                     , clip = "off", name = "2ndylab")
grid.draw(g)

@Sandy here is the code and its output

only trouble was that in the last row the secondary y-axis labels are inside the panels.I tried to solve this but not able to

joel.wilson
  • 8,243
  • 5
  • 28
  • 48
  • "i realised it all will get resolved if i understand how to manipulate the coordinate system of the grid(t,b,l,r) of the grobs." I doubt that. I've never had to edit these for tasks like this. – Roland Jun 23 '16 at 06:59
  • @Roland then what should be the approach Sir? I would like to learn d basics .Please suggest and direct me with the correct steps – joel.wilson Jun 23 '16 at 09:32
  • You might be able to take something from [this](http://stackoverflow.com/questions/26917689/how-to-use-facets-with-a-dual-y-axis-ggplot/37336658#37336658) – Sandy Muspratt Jun 23 '16 at 12:09
  • @Sandy..i actually referred to your code and learnt a lot!! However I want to generalise your case by removing the nrow=1 argument from the facet_wrap so that for each new row I have the secondary y axis tick marks and tick mark labels and a common y axis label. There the code fails and I would like to learn how to get over it. – joel.wilson Jun 23 '16 at 12:31
  • @Sandy could you explain the logic behind using max(pp$t) and similarly for b, l, r and adding 1 to it. I would like to understand the logic behind ur code.. – joel.wilson Jun 23 '16 at 13:51
  • First point: Look where you are adding the new y axis label. You're trying to put it into the same column that contains the new y axis ticks and tick mark labels. Also, the coding looks overly complex. The same code as the original will work, but be sure to set up new columns in the same order as the original. That is, deal with the y axis label first, then deal with the tick marks and tick mark labels. – Sandy Muspratt Jun 27 '16 at 01:15
  • Second point: The y axis in the shortened row - into what column is that being added? What is the width of that column? Note, it's not wide enough. Can that column be widened? Probably not; the resulting chart will look funny. Remember the advice to learn some grid? That will come in handy here. One way to position that last y axis is to wrap the axis in a viewport: you can set the position, the justification, and the width of the viewport; then put it into the column to the right of the panel. With clipping turned off, it doesn't matter that column is too narrow. – Sandy Muspratt Jun 27 '16 at 01:22
  • Like this: `yaxis_float = yaxis; yaxis_float$vp = viewport(x=0, just = "left", width = g2$widths[g2$layout[index,]$l]); g = gtable_add_grob(g, yaxis_float, t = 16, l = 5) ` – Sandy Muspratt Jun 27 '16 at 01:23
  • Finally: the reversing and inverting of the y axis material does not need to go into the loop. – Sandy Muspratt Jun 27 '16 at 01:31
  • U have truly been nice to me Sir..@Sandy thank you though its wrong for such comments – joel.wilson Jun 27 '16 at 17:20

1 Answers1

13

There were problems with your gtable_add_cols() and gtable_add_grob() commands. I added comments below.

Updated to ggplot2 v2.2.0

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

diamonds$cut <- sample(letters[1:4], nrow(diamonds), replace = TRUE)
dt.diamonds <- as.data.table(diamonds)
d1 <- dt.diamonds[,list(revenue = sum(price),
                        stones = length(price)),
                  by=c("clarity", "cut")]
setkey(d1, clarity, cut)

# The facet_wrap plots
p1 <- ggplot(d1, aes(x = clarity, y = revenue, fill = cut)) +
  geom_bar(stat = "identity") +
  labs(x = "clarity", y = "revenue") +
  facet_wrap( ~ cut, nrow = 2) +
  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 = 4) + 
  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_wrap( ~ cut, nrow = 2) +
  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")



# Get the ggplot grobs
g1 <- ggplotGrob(p1)
g2 <- ggplotGrob(p2)


# Grab the panels from g2 and overlay them onto the panels of g1
pp <- c(subset(g1$layout, grepl("panel", g1$layout$name), select = t:r))
g <- gtable_add_grob(g1, g2$grobs[grepl("panel", g1$layout$name)], 
                     pp$t, pp$l, pp$b, pp$l)


# Function to invert labels
hinvert_title_grob <- function(grob){
widths <- grob$widths
grob$widths[1] <- widths[3]
grob$widths[3] <- widths[1]
grob$vp[[1]]$layout$widths[1] <- widths[3]
grob$vp[[1]]$layout$widths[3] <- widths[1]

grob$children[[1]]$hjust <- 1 - grob$children[[1]]$hjust 
grob$children[[1]]$vjust <- 1 - grob$children[[1]]$vjust 
grob$children[[1]]$x <- unit(1, "npc") - grob$children[[1]]$x
grob
}

 # Get the y label from g2, and invert it
 index <- which(g2$layout$name == "ylab-l") 
 ylab <- g2$grobs[[index]]                # Extract that grob
 ylab <- hinvert_title_grob(ylab) 


 # Put the y label into g, to the right of the right-most panel
 # Note: Only one column and one y label
 g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l], pos = max(pp$r))

 g <-gtable_add_grob(g,ylab, t = min(pp$t), l = max(pp$r)+1, 
                             b = max(pp$b), r = max(pp$r)+1,
                   clip = "off", name = "ylab-r")


 # Get the y axis from g2, reverse the tick marks and the tick mark labels, 
 # and invert the tick mark labels 
 index <- which(g2$layout$name == "axis-l-1-1")  # Which grob
 yaxis <- g2$grobs[[index]]                    # Extract the grob

 ticks <- yaxis$children[[2]]
 ticks$widths <- rev(ticks$widths)
 ticks$grobs <- rev(ticks$grobs)

 plot_theme <- function(p) {
   plyr::defaults(p$theme, theme_get())
 }

 tml <- plot_theme(p1)$axis.ticks.length   # Tick mark length
 ticks$grobs[[1]]$x <- ticks$grobs[[1]]$x - unit(1, "npc") + tml

 ticks$grobs[[2]] <- hinvert_title_grob(ticks$grobs[[2]])
 yaxis$children[[2]] <- ticks


# Put the y axis into g, to the right of the right-most panel
# Note: Only one column, but two y axes - one for each row of the facet_wrap plot
 g <- gtable_add_cols(g, g2$widths[g2$layout[index, ]$l], pos = max(pp$r))

 nrows = length(unique(pp$t)) # Number of rows
 g <- gtable_add_grob(g, rep(list(yaxis), nrows), 
               t = unique(pp$t), l = max(pp$r)+1,
               b = unique(pp$b), r = max(pp$r)+1, 
               clip = "off", name = paste0("axis-r-", 1:nrows))



# Get the legends
leg1 <- g1$grobs[[which(g1$layout$name == "guide-box")]]
leg2 <- g2$grobs[[which(g2$layout$name == "guide-box")]]

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

grid.newpage()
grid.draw(g)

enter image description here


SO is not a tutorial site, and this might incur the wrath of other SO users, but there is too much for a comment.

Draw a graph with one plot panel only (i.e., no facetting),

library(ggplot2)

p <- ggplot(mtcars, aes(x = mpg, y = disp)) + geom_point()

Get the ggplot grob.

g <- ggplotGrob(p)

Explore the plot grob:
1) gtable_show_layout() give a diagram of the plot's gtable layout. The big space in the middle is the location of the plot panel. Columns to the left of and below the panel contain the y and x axes. And there is a margin surrounding the whole plot. The indices give the location of each cell in the array. Note, for instance, that the panel is located in the third row of the fourth column.

gtable_show_layout(g)  

2) The layout dataframe. g$layout returns a dataframe which contains the names of the grobs contained in the plot along with their locations within the gtable: t, l, b, and r (standing for top, left, right, and bottom). Note, for instance, that the panel is located at t=3, l=4, b=3, r=4. That is the same panel location that was obtained above from the diagram.

 g$layout

3) The diagram of the layout tries to give the heights and widths of the rows and columns, but they tend to overlap. Instead, use g$widths and g$heights. The 1null width and height is the width and height of the plot panel. Note that 1null is the 3rd height and the 4th width - 3 and 4 again.

Now draw a facet_wrap and a facet_grid plot.

p1 <- ggplot(mtcars, aes(x = mpg, y = disp)) + geom_point() +
   facet_wrap(~ carb, nrow = 1)

p2 <- ggplot(mtcars, aes(x = mpg, y = disp)) + geom_point() +
   facet_grid(. ~ carb)

g1 <- ggplotGrob(p1)
g2 <- ggplotGrob(p2)

The two plots look the same, but their gtables differ. Also, the names of the component grobs differ.

Often it is convenient to get a subset of the layout dataframe containing the indices (i.e., t, l, b, and r) of grobs of a common type; say all the panels.

pp1 <- subset(g1$layout, grepl("panel", g1$layout$name), select = t:r)
pp2 <- subset(g2$layout, grepl("panel", g2$layout$name), select = t:r)

Note for instance that all the panels are in row 4 (pp1$t, pp2$t).
pp1$r refers to the columns that contain the plot panels;
pp1$r + 1 refers to the columns to the right of the panels;
max(pp1$r) refers to the right most column that contains a panel;
max(pp1$r) + 1 refers to the column to the right of the right most column that contains a panel;
and so forth.

Finally, draw a facet_wrap plot with more than one row.

p3 <- ggplot(mtcars, aes(x = mpg, y = disp)) + geom_point() +
   facet_wrap(~ carb, nrow = 2)
g3 <- ggplotGrob(p3)

Explore the plot as before, but also subset the layout data frame to contain the indices of the panels.

pp3 <- subset(g3$layout, grepl("panel", g3$layout$name), select = t:r)

As you would expect, pp3 tells you that the plot panels are located in three columns (4, 7, and 10) and two rows (4 and 8).

These indices are used when adding rows or columns to the gtable, and when adding grobs to a gtable. Check these commands with ?gtable_add_rows and gtable_add_grob.

Also, learn some grid, especially how to construct grobs, and the use of units (some resources are given in the r-grid tag here on SO.

Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122
  • i have posted my output as my answer below. Would you help me in reducing the gap between the secondary-axis labels and tick mark labels? – joel.wilson Jun 25 '16 at 09:55
  • is there a way to replace using the gtable:::cbind_gtable() to combine the 2 gtables legends? because in the help page ?':::' they prefer not using the triple colon operator. – joel.wilson Sep 13 '16 at 10:33