Expressions dynamically positioned on multiple lines in ggplot

Viewed 159

I'm trying to create a plot with four dynamically positioned lines of expressions. It should look something like the plot below (I've had to do this manually by having each expression on a different line of code and manually setting the x and y coordinates but I want to automate it as I have many plots to do):

Target graph

All the plots have different scales so it would be good if I could combine the expressions as one object to position in, for example, the top right corner. The code I'm using is as below:

dat <- data.frame("x" = sample(1:100, 800, replace = T),
                  "y" = sample(1:100, 800, replace = T))
                  
mod <- lm(y ~ poly(x, 1, raw =T), dat)

eq <- linEq(mod, 3)
r2 <- bquote(italic("R")^2~"="~.(round(summary(mod)$adj.r.squared, 2)))
f.value <- round(summary(mod)$fstatistic[[1]], 2)
df1 <- summary(mod)$fstatistic[[2]]
df2 <- summary(mod)$fstatistic[[3]]
f.text <- bquote(italic("F")[.(df1)*","~.(df2)]~"="~.(f.value))
p.value <- bquote(italic("p")~.(ifelse(lmp(mod) < 0.001, "<", "="))~.(ifelse(lmp(mod) < 0.001, "0.001", round(lmp(mod),3))))

p <- ggplot(data=dat, aes(x = x, y = y))+
  geom_point()+
  geom_smooth(method='lm', formula= y ~ x, se = F)+
  xlab("X")+
  ylab("Y")+
  annotate(geom = "text", x = (1*max(dat$x)), y = (1*max(dat$y)),
           label = paste0(eq, r2, f.text, p.value, sep = "\n"),
           parse = T,
           hjust = "inward")+
  theme_classic()

But I get this error:

Error in parse(text = text[[i]]) : :1:18: unexpected symbol 1: y = 51.5 + 0.0278x ^

I know I can use the deparse() function with each singular expression (that's what I did to create the image) but I don't know how to integrate it into my code so that I can position all four lines automatically.

Any ideas would be greatly appreciated!

Many thanks,

Carolina

Some custom functions are needed to make the above code work. They are as below:

linEq <- function(lmObj, dig) {
  paste0(c("","y = "),
         c("",signif(lmObj$coef[1], dig)),
         c("",ifelse(sign(lmObj$coef)[2]==1," + "," - ")),
         c("",signif(abs(lmObj$coef[2]), dig)), c("", "x"),
         collapse="")
}

lmp <- function (modelobject) {
  if (class(modelobject) != "lm") stop("Not an object of class 'lm' ")
  f <- summary(modelobject)$fstatistic
  p <- pf(f[1],f[2],f[3],lower.tail=F)
  attributes(p) <- NULL
  return(p)
}
1 Answers

I don't use math notation that often. That's probably why I always struggle with using it and also with the differing ways to add them to a ggplot. What worked the best for me is to set up the plotmath strings using paste0. Therefore I have rewritten your bquotes using paste0. I also made two small changes to your fun linEq(replaced = by ==, added a * before x).

To put your four labels on the plot I have put them in a vector and also set the y positions as a vector.

However, my approach does not automatically pick the right x and y positions for your labels. And I don't think that there is an easy approach to achieve that and which will work for each and every case. That's why I opted for putting the labels on top of the plot.

Thanks to the comment by @teunbrand: At least for the x-axis position of the labels you could do x=Inf to align the labels on the right of the plot.

library(ggplot2)

set.seed(42)

dat <- data.frame(
  "x" = sample(1:100, 800, replace = T),
  "y" = sample(1:100, 800, replace = T)
)

mod <- lm(y ~ poly(x, 1, raw = T), dat)

linEq <- function(lmObj, dig) {
  paste0(c("", "y == "),
    c("", signif(lmObj$coef[1], dig)),
    c("", ifelse(sign(lmObj$coef)[2] == 1, " + ", " - ")),
    c("", signif(abs(lmObj$coef[2]), dig)), c("", "* x"),
    collapse = ""
  )
}

lmp <- function(modelobject) {
  if (class(modelobject) != "lm") stop("Not an object of class 'lm' ")
  f <- summary(modelobject)$fstatistic
  p <- pf(f[1], f[2], f[3], lower.tail = F)
  attributes(p) <- NULL
  return(p)
}


eq <- linEq(mod, 3)
r2 <- paste0("italic(R)^{2} == ", round(summary(mod)$adj.r.squared, 2))
f.value <- round(summary(mod)$fstatistic[[1]], 2)
df1 <- summary(mod)$fstatistic[[2]]
df2 <- summary(mod)$fstatistic[[3]]
f.text <- paste0("italic(F)['", df1, ",", df2, "'] == ", f.value)
p.value <- paste0("italic(p) ", ifelse(lmp(mod) < 0.001, "<", "=="), ifelse(lmp(mod) < 0.001, "0.001", round(lmp(mod), 3)))

ggplot(data = dat, aes(x = x, y = y)) +
  geom_point() +
  geom_smooth(method = "lm", formula = y ~ x, se = F) +
  xlab("X") +
  ylab("Y") +
  annotate(
    geom = "text", x = (1 * max(dat$x)), y = c(2, 1.8, 1.6, 1.4) * max(dat$y),
    label = c(eq, r2, f.text, p.value),
    parse = TRUE,
    hjust = "inward"
  ) +
  theme_classic()

EDIT Maybe this fits your needs. Not sure whether this works for all of your plots, but ... The approach below first makes on object from the labels via the grid package. Second, making use of the patchwork package you could add them as an inset in the top right corner. One caveat of using patchwork is that is does not work with grid.arrange (at least I got an error), i.e. you have to glue your plots together using patchwork too. As an example I added a second plot by simply scaling x and y by 10:

Note The picture does not show the sub-/superscripts. But they show up in the plot window as well as when saving the plot.

library(patchwork)

make_grob <- function(labels, y, size = unit(8, "pt")) {
  grob_list <- purrr::map2(labels, y, 
              ~ grid::textGrob(scales::parse_format()(.x), hjust = 1, 
                       x = grid::unit(1, "npc"), y = grid::unit(.y, "npc"), 
                       gp = grid::gpar(fontsize = size))
              
              )
  
  grid::grobTree(grob_list[[1]], grob_list[[2]], grob_list[[3]], grob_list[[4]])
}


gt <- make_grob(list(eq, r2, f.text, p.value), y = seq(.2, .8, .2))
gg1 <- ggplot(data = dat, aes(x = x, y = y)) +
  geom_point() +
  geom_smooth(method = "lm", formula = y ~ x, se = F) +
  xlab("X") +
  ylab("Y") +
  theme_classic() +
  patchwork::inset_element(gt, left = .7, bottom = .7, right = 1, top = 1, align_to = "plot", on_top = TRUE)

gg2 <- ggplot(data = dat, aes(x = 10 * x, y = 10 * y)) +
  geom_point() +
  geom_smooth(method = "lm", formula = y ~ x, se = F) +
  xlab("X") +
  ylab("Y") +
  theme_classic() +
  patchwork::inset_element(gt, left = .7, bottom = .7, right = 1, top = 1, align_to = "plot", on_top = TRUE)

gg1 + gg2

enter image description here

Related