11

I want to explore the directlabels package with ggplot. I am trying to plot labels at the endpoint of a simple line chart; however, the labels are clipped by the plot panel. (I intend to plot about 10 financial time series in one plot and I thought directlabels would be the best solution.)

I would imagine there may be another solution using annotate or some other geoms. But I would like to solve the problem using directlabels. Please see code and image below. Thanks.

library(ggplot2)
library(directlabels)
library(tidyr)

#generate data frame with random data, for illustration and plot:
x <- seq(1:100)
y <- cumsum(rnorm(n = 100, mean = 6, sd = 15))
y2 <- cumsum(rnorm(n = 100, mean = 2, sd = 4))
data <- as.data.frame(cbind(x, y, y2))
names(data) <- c("month", "stocks", "bonds")
tidy_data <- gather(data, month)
names(tidy_data) <- c("month", "asset", "value")
p <- ggplot(tidy_data, aes(x = month, y = value, colour = asset)) + 
geom_line() + 
geom_dl(aes(colour = asset, label = asset), method = "last.points") + 
theme_bw()

line chart with end labels

On data visualization principles, I would like to avoid extending the x-axis to make the labels fit--this would mean having data space with no data. Rather, I would like the labels to extend toward the white space beyond the chart box/panel (if that makes sense).

Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
Juan Mier
  • 155
  • 1
  • 6
  • rearranged text responding to an answer below, which is now slightly obsolete due to the presence of a better answer. – Ben Bolker Sep 17 '19 at 19:59

2 Answers2

13

In my opinion, direct labels is the way to go. Indeed, I would position labels at the beginning and at the end of the lines, creating space for the labels using expand(). Also note that with the labels, there is no need for the legend.

This is similar to answers here and here.

library(ggplot2)
library(directlabels)
library(grid)
library(tidyr)

x <- seq(1:100)
y <- cumsum(rnorm(n = 100, mean = 6, sd = 15))
y2 <- cumsum(rnorm(n = 100, mean = 2, sd = 4))
data <- as.data.frame(cbind(x, y, y2))
names(data) <- c("month", "stocks", "bonds")
tidy_data <- gather(data, month)
names(tidy_data) <- c("month", "asset", "value")

ggplot(tidy_data, aes(x = month, y = value, colour = asset, group = asset)) + 
     geom_line() + 
     scale_colour_discrete(guide = 'none')  + 
     scale_x_continuous(expand = c(0.15, 0)) +
     geom_dl(aes(label = asset), method = list(dl.trans(x = x + .3), "last.bumpup")) +
     geom_dl(aes(label = asset), method = list(dl.trans(x = x - .3), "first.bumpup")) + 
     theme_bw() 

enter image description here

If you prefer to push the labels into the plot margin, direct labels will do that. But because the labels are positioned outside the plot panel, clipping needs to be turned off.

p1 <- ggplot(tidy_data, aes(x = month, y = value, colour = asset, group = asset)) + 
     geom_line() + 
     scale_colour_discrete(guide = 'none')  + 
     scale_x_continuous(expand = c(0, 0)) +
     geom_dl(aes(label = asset), method = list(dl.trans(x = x + .3), "last.bumpup")) +
     theme_bw() +
     theme(plot.margin = unit(c(1,4,1,1), "lines")) 

# Code to turn off clipping
gt1 <- ggplotGrob(p1)
gt1$layout$clip[gt1$layout$name == "panel"] <- "off"
grid.draw(gt1)

enter image description here

This effect can also be achieved using geom_text (and probably also annotate), that is, without the need for direct labels.

p2 = ggplot(tidy_data, aes(x = month, y = value, group = asset, colour = asset)) +
  geom_line() + 
  geom_text(data = subset(tidy_data, month == 100), 
      aes(label = asset, colour = asset, x = Inf, y = value), hjust = -.2) +
  scale_x_continuous(expand = c(0, 0)) +
  scale_colour_discrete(guide = 'none')  +  
  theme_bw() +  
  theme(plot.margin = unit(c(1,3,1,1), "lines"))  

# Code to turn off clipping
gt2 <- ggplotGrob(p2)
gt2$layout$clip[gt2$layout$name == "panel"] <- "off"
grid.draw(gt2)

enter image description here

Community
  • 1
  • 1
Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122
  • Great answer. These are the best solutions. – Nick Becker Jun 30 '16 at 21:14
  • 1
    this can now be done a little bit more easily with `coord_cartesian(clip="off")` ... I tried to figure out how to put a white rectangle behind the text, for cases where the word ran across the plot frame, but wasn't successful (this is better anyway) – Ben Bolker Sep 17 '19 at 19:56
3

Since you didn't provide a reproducible example, it's hard to say what the best solution is. However, I would suggest trying to manually adjust the x-scale. Use a "buffer" increase the plot area.

#generate data frame with random data, for illustration and plot:
p <- ggplot(tidy_data, aes(x = month, y = value, colour = asset)) + 
geom_line() + 
geom_dl(aes(colour = asset, label = asset), method = "last.points") + 
theme_bw() +
xlim(minimum_value, maximum_value + buffer)

Using scale_x_discrete() or scale_x_continuous() would likely also work well here if you want to use the direct labels package. Alternatively, annotate or a simple geom_text would also work well.

Nick Becker
  • 4,059
  • 13
  • 19
  • 1
    Thanks Nick. Your solution with `xlim` works. However, I think it on data visualization principle I would like to avoid extending the x-axis to make the labels fit. Rather, I would like the labels to extend toward the white space beyond the chart box/panel (if that makes sense) – Juan Mier Jun 30 '16 at 00:34
  • BTW, I edited the original question to explain context and add code. – Juan Mier Jun 30 '16 at 00:42
  • @JuanMier I don't disagree at all. Unfortunately, I don't know how to accomplish what you're describing. I know you said you wanted to use directlabels, but I think using annotate and positioning the labels would be a decent work-around solution. – Nick Becker Jun 30 '16 at 01:08