How can I put a transformed scale on the right side of a ggplot2?
Asked Answered
D

4

19

I'm creating a graph showing the change in lake levels over time. I've attached a simple example below. I would like to add a scale (tick marks and annotation) on the right side of the plot that shows the elevation in feet. I know ggplot2 won't allow two different scales (see Plot with 2 y axes, one y axis on the left, and another y axis on the right), but because this is a transformation of the same scale, is there a way to do this? I'd prefer to keep using ggplot2 and not to have to revert to the plot() function.

library(ggplot2)
LakeLevels<-data.frame(Day=c(1:365),Elevation=sin(seq(0,2*pi,2*pi/364))*10+100)
p <- ggplot(data=LakeLevels) + geom_line(aes(x=Day,y=Elevation)) + 
  scale_y_continuous(name="Elevation (m)",limits=c(75,125)) 
p
Demonstration answered 24/9, 2013 at 18:13 Comment(2)
No doubt somebody will be along in a minute with some suggestions. In the meantime, here's a +1 for posting your first question as a reproducible example that includes both code and data. Welcome to Stack Overflow.Ellora
I can't recall if the discussions related to this idea made it past the early work that kohske did.Shofar
M
16

You should have a look at this link http://rpubs.com/kohske/dual_axis_in_ggplot2.

I've adapted the code provided there for your example. This fix seems very "hacky", but it gets you part of the way there. The only piece left is figuring out how to add text to the right axis of the graph.

    library(ggplot2)
    library(gtable)
    library(grid)
    LakeLevels<-data.frame(Day=c(1:365),Elevation=sin(seq(0,2*pi,2*pi/364))*10+100)
    p1 <- ggplot(data=LakeLevels) + geom_line(aes(x=Day,y=Elevation)) + 
          scale_y_continuous(name="Elevation (m)",limits=c(75,125))

    p2<-ggplot(data=LakeLevels)+geom_line(aes(x=Day, y=Elevation))+
        scale_y_continuous(name="Elevation (ft)", limits=c(75,125),           
        breaks=c(80,90,100,110,120),
                 labels=c("262", "295", "328", "361", "394"))

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

    #overlap the panel of the 2nd plot on that of the 1st plot

    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)

   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, pp$t, length(g$widths) - 1, pp$b)

   # draw it
   grid.draw(g)

enter image description here

Manzanilla answered 29/9, 2013 at 19:27 Comment(3)
Thank you! That is exactly was I was looking for (as you mentioned, ideally it would an axis label too). This is the first example I've seen with the ggplot_gtable() function, it looks pretty powerful for customizing. Hopefully this functionality will be in the next release of ggplot.Demonstration
Walter, it seems this general approach is now broken using ggplot2 2.1.0 - any chance you could shed light on this? See #36487783Imaimage
@Thomas, a solution based on @Walter's solution and using code from the cowplot package is herePlashy
M
5

I might have found a solution for placing the axis title,with some ideas from Nate Pope's answer which can be found here:
ggplot2: Adding secondary transformed x-axis on top of plot
And a discussion about accessing grobs in gtable here: https://groups.google.com/forum/m/#!topic/ggplot2-dev/AVHHcYqc5uU

In the end, I just added the line

g <- gtable_add_grob(g, g2$grob[[7]], pp$t, length(g$widths), pp$b)

before calling grid.draw(g), which seemed to do the trick.
As I understand it, it takes the y axis title g2$grob[[7]] and places it at the outer most right side. It might not be the pretties solution, but it worked for me.

One last thing. It would be nice to find a way to rotate the axis title.

Regards,

Tim

Muslim answered 13/2, 2014 at 6:16 Comment(0)
S
4

This question has been answered, but the general problem of adding a secondary axis and secondary scale to the right-hand side of a ggplot object is one that comes up all the time. I would like to report below my own tweak on the problem, based on several elements given by various answers in this thread as well as in several other threads (see a partial list of references below).

I have a need for mass production of dual-y-axis plots, so I built a function ggplot_dual_axis(). Here are the features of potential interest:

  1. The code displays gridlines for both the y-left and y-right axes (this is my main contribution though it is trivial)

  2. The code prints a euro symbol and embeds it into the pdf (something I saw there: Plotting Euro Symbol € in ggplot2?)

  3. The code attempts to avoid printing certain elements twice ('attempts' suggests I doubt it fully succeeds)

Unanswered questions:

  • Is there a way to modify the ggplot_dual_axis() function to remove one of the geom_line() or geom_point() or whatever it may be without throwing errors if such geom elements are not present. In pseudo-code something like if has(geom_line) ...

  • How can I call the g2$grobs[[7]] by keyword rather than index? This is what it returns: text[axis.title.y.text.232] My interest in the question stems from my failed attempts to grab the grid lines by applying a similar trick. I think the grid lines are hidden somewhere inside g2$grobs[[4]], but I'm not sure how to access them.

Edit. Question I was able to answer myself: How can I increase the plot margin on the right-hand side, where the 'Euro' label is? Answer: theme(plot.margin = unit(c(1,3,0.5,0.8), "lines")) will do the trick, for instance.

Please do point out any obvious problems or suggest improvements.

Now the code: hopefully it will be useful to someone. As I said, I don't claim originality, it's a combination of things others have already shown.

##' function named ggplot_dual_axis()
##' Takes 2 ggplot plots and makes a dual y-axis plot
##' function takes 2 compulsory arguments and 1 optional argument
##' arg lhs is the ggplot whose y-axis is to be displayed on the left
##' arg rhs is the ggplot whose y-axis is to be displayed on the right
##' arg 'axis.title.y.rhs' takes value "rotate" to rotate right y-axis label
##' The function does as little as possible, namely:
##'  # display the lhs plot without minor grid lines and with a
##'  transparent background to allow grid lines to show
##'  # display the rhs plot without minor grid lines and with a
##'  secondary y axis, a rotated axis label, without minor grid lines
##'  # justify the y-axis label by setting 'hjust = 0' in 'axis.text.y'
##'  # rotate the right plot 'axis.title.y' by 270 degrees, for symmetry
##'  # rotation can be turned off with 'axis.title.y.rhs' option
##'  

ggplot_dual_axis <- function(lhs, rhs, axis.title.y.rhs = "rotate") {
  # 1. Fix the right y-axis label justification
    rhs <- rhs + theme(axis.text.y = element_text(hjust = 0))
  # 2. Rotate the right y-axis label by 270 degrees by default
    if (missing(axis.title.y.rhs) | 
        axis.title.y.rhs %in% c("rotate", "rotated")) {
        rhs <- rhs + theme(axis.title.y = element_text(angle = 270)) 
    }
  # 3a. Use only major grid lines for the left axis
    lhs <- lhs + theme(panel.grid.minor = element_blank())
  # 3b. Use only major grid lines for the right axis
  #     force transparency of the backgrounds to allow grid lines to show
    rhs <- rhs + theme(panel.grid.minor = element_blank(), 
        panel.background = element_rect(fill = "transparent", colour = NA), 
        plot.background = element_rect(fill = "transparent", colour = NA))
# Process gtable objects
  # 4. Extract gtable
    library("gtable") # loads the grid package
    g1 <- ggplot_gtable(ggplot_build(lhs))
    g2 <- ggplot_gtable(ggplot_build(rhs))
  # 5. Overlap the panel of the rhs plot on that of the lhs plot
    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)
  # Tweak axis position and labels
    ia <- which(g2$layout$name == "axis-l")
    ga <- g2$grobs[[ia]]
    ax <- ga$children[["axis"]]  # 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, pp$t, length(g$widths) - 1, pp$b)
    g <- gtable_add_grob(g, g2$grobs[[7]], pp$t, length(g$widths), pp$b)
  # Display plot with arrangeGrob wrapper arrangeGrob(g)
    library("gridExtra")
    grid.newpage()
    return(arrangeGrob(g))
}

And below some fake data and two plots that are intended to be in dollar and euro units. Wouldn't it be cool to have a package that would allow you to make one plot and wrap around it a call to a dual-y-axis plot like so ggplot_dual_axis_er(ggplot_object, currency = c("dollar", "euro")) and it would automatically fetch the exchange rates for you! :-)

# Set directory:
if(.Platform$OS.type == "windows"){
  setwd("c:/R/plots")
} else { 
  setwd("~/R/plots")
}

# Load libraries
library("ggplot2")
library("scales")

# Create euro currency symbol in plot labels, simple version
# avoids loading multiple libraries
# avoids problems with rounding of small numbers, e.g. .0001
labels_euro <- function(x) {# no rounding
paste0("€", format(x, big.mark = ",", decimal.mark = ".", trim = TRUE,
    scientific = FALSE))
} 

labels_dollar <- function(x) {# no rounding: overwrites dollar() of library scales
paste0("$", format(x, big.mark = ",", decimal.mark = ".", trim = TRUE,
    scientific = FALSE))
} 

# Create data
df <- data.frame(
  Year = as.Date(c("2001", "2002", "2003", "2004", "2005", "2006", "2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018"),
    "%Y"), 
  Dollar = c(0, 9000000, 1000000, 8000000, 2000000, 7000000, 3000000, 6000000, 4000000, 5000000, 5000000, 6000000, 4000000, 7000000, 300000, 8000000, 2000000, 9000000))
# set Euro/Dollar exchange rate at 0.8 euros = 1 dollar
df <- cbind(df, Euro = 0.8 * df$Dollar)
# Left y-axis
p1 <- ggplot(data = df, aes(x = Year, y = Dollar)) + 
    geom_line(linestyle = "blank") + # manually remove the line
    theme_bw(20) +                   # make sure font sizes match in both plots
    scale_x_date(labels = date_format("%Y"), breaks = date_breaks("2 years")) + 
    scale_y_continuous(labels = labels_dollar, 
        breaks = seq(from = 0, to = 8000000, by = 2000000))
# Right y-axis
p2 <- ggplot(data = df, aes(x = Year, y = Euro)) + 
    geom_line(color = "blue", linestyle = "dotted", size = 1) + 
    xlab(NULL) +                     # manually remove the label
    theme_bw(20) +                   # make sure font sizes match in both plots
    scale_x_date(labels = date_format("%Y"), breaks = date_breaks("2 years")) +
    scale_y_continuous(labels = labels_euro, 
        breaks = seq(from = 0, to = 7000000, by = 2000000))

# Combine left y-axis with right y-axis
p <- ggplot_dual_axis(lhs = p1, rhs = p2)
p

# Save to PDF
pdf("ggplot-dual-axis-function-test.pdf", 
  encoding = "ISOLatin9.enc", width = 12, height = 8)
p
dev.off()

embedFonts(file = "ggplot-dual-axis-function-test.pdf", 
           outfile = "ggplot-dual-axis-function-test-embedded.pdf")

enter image description here

Partial list of references:

  1. Display two parallel axes on a ggplot (R)
  2. Dual y axis in ggplot2 for multiple panel figure
  3. How can I put a transformed scale on the right side of a ggplot2?
  4. Preserve proportion of graphs using grid.arrange
  5. The perils of aligning plots in ggplot
  6. https://github.com/kohske/ggplot2
Semite answered 22/12, 2014 at 18:42 Comment(6)
I managed to make the plots overlap with facet_wrap but I cannot so far get the axis on the plot. any ideas? The change to get the plot is: pp <- c(subset(g1$layout, grepl("panel",name) , se = t:r)); g <- gtable_add_grob(g1, g2$grobs[grep("panel",g2$layout$name)], pp$t, pp$l, pp$b, pp$l) Suddenly
Have you seen these? #27751237, #27755138Semite
Nope. thanks. But I don't think it is in my near future to really understand how this works. Anyhow after some trial and error I managed to hack something together (also works with no facets): gist.github.com/stanstrup/cfcc8badd8ca4453c8bb The only thing is that if the facet "plot grid" is not filled with plots the axis is still made to the extreme right. For example if you have 7 plots in a 3x3 grid the last two positions has no plots. but the axis will be made all the way to the right. I guess that is not too easy to fix and I can fix it manually. Thanks again for this nice example.Suddenly
Ah one thing. It doesn't work if there is y-axis on all plots separately.Suddenly
You may want to ask a question separately, with ggplot2 and gtable tags, providing a MWE and screenshot of problem and desired result.Semite
OK. thanks for the suggestion. I have done that: #30103908Suddenly
D
2

In order to rotate the axis title add the following to the p2 graph:

p2 <- p2 + theme(axis.title.y=element_text(angle=270))
Diachronic answered 3/8, 2014 at 9:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.