6

I need to create a dual-y plot using sec.axis, but just can't get both axis to scale properly.

I've been following instructions found in this thread: ggplot with 2 y axes on each side and different scales

But everytime I change the lower limit in ylim.prim to anything other than 0, it messes up the the entire plot. For visualization reasons, I need very specific y limits for both axes. Also, when I change geom_col to geom_line, it messes up the limits for the secondary axis, too.

climate <- tibble(
  Month = 1:12,
  Temp = c(23,23,24,24,24,23,23,23,23,23,23,23),
  Precip = c(101,105,100,101,102, 112, 101, 121, 107, 114, 108, 120)
  )

ylim.prim <- c(0, 125)   # in this example, precipitation
ylim.sec <- c(15, 30)    # in this example, temperature

b <- diff(ylim.prim)/diff(ylim.sec)
a <- b*(ylim.prim[1] - ylim.sec[1])

ggplot(climate, aes(Month, Precip)) +
  geom_col() +
  geom_line(aes(y = a + Temp*b), color = "red") +
  scale_y_continuous("Precipitation", sec.axis = sec_axis(~ (. - a)/b, name = "Temperature"),) +
  scale_x_continuous("Month", breaks = 1:12)  

GGplot1

ylim.prim <- c(0, 125)   # in this example, precipitation
ylim.sec <- c(15, 30)    # in this example, temperature

b <- diff(ylim.prim)/diff(ylim.sec)
a <- b*(ylim.prim[1] - ylim.sec[1])

ggplot(climate, aes(Month, Precip)) +
  geom_line() +
  geom_line(aes(y = a + Temp*b), color = "red") +
  scale_y_continuous("Precipitation", sec.axis = sec_axis(~ (. - a)/b, name = "Temperature"),) +
  scale_x_continuous("Month", breaks = 1:12)  

GGplot2

ylim.prim <- c(95, 125)   # in this example, precipitation
ylim.sec <- c(15, 30)    # in this example, temperature

b <- diff(ylim.prim)/diff(ylim.sec)
a <- b*(ylim.prim[1] - ylim.sec[1])

ggplot(climate, aes(Month, Precip)) +
  geom_line() +
  geom_line(aes(y = a + Temp*b), color = "red") +
  scale_y_continuous("Precipitation", sec.axis = sec_axis(~ (. - a)/b, name = "Temperature"),) +
  scale_x_continuous("Month", breaks = 1:12)  

GGplot3

Anke
  • 527
  • 1
  • 6
  • 19
  • I think your equation for `a` should be `ylim.prim[1] - b*ylim.sec[1]`. If I use that instead of your definition the remapping between the two scales seems to work and the limits for the two axes match your definitions. – aosmith Nov 08 '19 at 23:44

3 Answers3

6

From what I see in the code, your conversion between the two scales was a little too simple.

In order to get the result that I think your were aiming for, it is necessary to normalize the temperature data (this way you can vary the spread and mean, and make it suit your primary y-scale) and then compute the reverse normalization for the secondary y-axis.

By normalisation I mean: (Temp - mean(TEMP))/sd(TEMP), where TEMP is an array of all values, and Tempis the specific value to be plotted.

EDIT: Since ggplot2 only allows for transformations of the original scale, and has no option to set unique limits for the secondary axis, there is no easy option to set the ylim of the secondary y axis. BUT, there is a way to do it, which is quite hacky, but I will show here.

By solving the normalisation for both boundaries with a simple linear model (or any other solving method) the transformation of the secondary y axis can be made to match unique limits. The linear transformation I use introduces the two variables a and s. s is a scaling factor that the result is multiplied by, which allows varying the spread of the plotted data in respect to the primary y-axis. The variable a shifts the resulting transformation along the y-axis.

The transformation is:

y = a + (Temp - mean(TEMP))/sd(TEMP)) * s

The calculation works as follows:

climate <- tibble(
  Month = 1:12,
  Temp = c(23,23,24,24,24,23,23,23,23,23,23,23),
  Precip = c(101,105,100,101,102, 112, 101, 121, 107, 114, 108, 120)
  )

ylim.prim <- c(95, 125)   # in this example, precipitation
ylim.sec <- c(15, 30)    # in this example, temperature

TEMP <- climate$Temp #needed for coherent normalisation

# This is quite hacky, but it works if you want to set a boundary for the secondary y-axis
fit = lm(b ~ . + 0, 
   tibble::tribble(
     ~a, ~s,  ~b,
      1,  (ylim.sec[1] - mean(TEMP))/sd(TEMP),  ylim.prim[1],
      1,  (ylim.sec[2] - mean(TEMP))/sd(TEMP), ylim.prim[2]))

a <- fit$coefficients['a']
s <- fit$coefficients['s']

To adjust the secondary y-axis scale back to your values, simply do every step in the calculation in reverse.

This way you can have a nice & adjustable overlay of the two time series:

ggplot(climate, aes(Month, Precip)) +
  geom_line() + 
  geom_line(aes(y = (a + ((Temp - mean(TEMP))/sd(TEMP)) * s) ), color = "red") +
  scale_y_continuous("Precipitation", 
                     limits=ylim.prim,
                     sec.axis = sec_axis(~ (. - a) / s * sd(TEMP) + mean(TEMP), name = "Temperature"),) +
  scale_x_continuous("Month", breaks = 1:12) +
  theme(axis.title.y.right = element_text(colour = "red"))

temperature vs precipitation plot with secondary y axis scaled to fit

an outpost
  • 186
  • 9
  • 1
    Where did you applied the ylim.sec in ggplot( ) function ? – Darwin PC Aug 22 '21 at 04:20
  • @DarwinPC Good question, thanks for pointing that out! I modified my answer to use the ylim.sec values, which complicates the steps of the transformation a bit, but it does work like this for linear transformations at least. Needless to say, that this is quite hacky and against ggplot2's philosophy... – an outpost Aug 26 '21 at 15:32
2

How about this:

  ggplot(climate, aes(Month, Precip)) +
    geom_line() +
    geom_line(aes(y = 4.626*Temp), color = "red") +
    scale_y_continuous("Precipitation", sec.axis = sec_axis(~ ./4.626, name = "Temperature"),) +
    scale_x_continuous("Month", breaks = 1:12)   

Let me know if you need further explanation. enter image description here

Zhiqiang Wang
  • 6,206
  • 2
  • 13
  • 27
0

A simple solution showing the direct algebraic transformation. Uses full axis extents for both y-axis variables.

library(ggplot2)
dat <- data.frame(Date = seq.Date(as.Date("2010-01-01"), by = "day", length.out = 61),
                  a = c(50:-10),
                  b = c(-100:-40)
                  )

a.diff <- max(dat$a) - min(dat$a)
b.diff <- max(dat$b) - min(dat$b)
a.min <- min(dat$a)
b.min <- min(dat$b)

ggplot(dat, aes(Date, a)) +
    geom_line(color = "blue") +
    geom_line(aes(y = (b - b.min) / b.diff * a.diff + a.min), color = "red") +
    scale_y_continuous(sec.axis = sec_axis(trans = ~((. -a.min) * b.diff / a.diff) + b.min,
                                           name = "b")) +
    theme(
        axis.title.y.left = element_text(color = "blue"),
        axis.text.y.left = element_text(color = "blue"),
        axis.title.y.right = element_text(color = "red"),
        axis.text.y.right = element_text(color = "red"))
Eric Krantz
  • 1,854
  • 15
  • 25