Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set ggplot gridlines with endpoints / ggplot's break calculation

Tags:

r

ggplot2

I have a ggplot issue where I'm trying to make line plots with a very minimal look. I've gotten rid of the legend in favor of text labels at the right side of each line. It might not be so noticeable if the labels weren't so long, but I'd like it better if the gridlines stopped at the maximum x-value (in this case, at the year 2015).

library(tidyverse)

df <- structure(list(industry = c("Accommodation & Food Services", "Construction", "Health Care & Social Asst.", "Retail Trade", 
        "Accommodation & Food Services", "Construction", "Health Care & Social Asst.", "Retail Trade"), 
    year = c(2002L, 2002L, 2002L, 2002L, 2015L, 2015L, 2015L, 2015L), 
    value = c(6.977, 5.264, 17.065, 14.528, 8.032, 4.648, 20.547, 12.568)), 
    class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, -8L), 
    .Names = c("industry", "year", "value"))

ggplot(df, aes(x = year, y = value, color = industry)) +
  geom_line() +
  geom_text(aes(label = industry), data = . %>% filter(year == max(year)), size = 3, hjust = 0, nudge_x = 0.1) +
  scale_x_continuous(expand = expand_scale(mult = c(0.05, 0.85)), breaks = c(min(df$year), max(df$year)), name = NULL) +
  scale_y_continuous(limits = c(0, NA), name = "Value (thousands)") +
  theme_minimal() +
  theme(legend.position = "none", 
        panel.grid.major.x = element_blank(), 
        panel.grid.minor = element_blank())

I'd like to avoid the grob-hacking done in this answer because I have several plots and want to put them together quickly.

The solution I've come up with so far is to create pseudo-gridlines by making a geom_segment with dummy data at regular y-values that starts at the earliest year and ends at the latest one. This works okay, because I have an idea of what y-breaks I want and can set scale_y_continuous to match geom_segment.

break_df <- tibble(x1 = 2002, x2 = 2015, y = seq(0, 20, by = 5))

ggplot(df, aes(x = year, y = value, color = industry)) +
  geom_segment(aes(x = x1, xend = x2, y = y, yend = y), data = break_df, inherit.aes = F, size = 0.4, color = "gray85") +
  geom_line() +
  geom_text(aes(label = industry), data = . %>% filter(year == max(year)), size = 3, hjust = 0, nudge_x = 0.1) +
  scale_x_continuous(expand = expand_scale(mult = c(0.05, 0.85)), breaks = c(min(df$year), max(df$year)), name = NULL) +
  scale_y_continuous(limits = c(0, NA), name = "Value (thousands)", breaks = break_df$y) +
  theme_minimal() +
  theme(legend.position = "none", 
        panel.grid = element_blank())

Created on 2018-05-24 by the reprex package (v0.2.0).

What I'm wondering is how ggplot calculates breaks, and if there's a function it uses that I could harness for this? Like if there's some internal calculation for the breaks that I could feed into both geom_segment and scale_y_continuous, so they'd match without me setting up a separate data frame of break information.

Alternatively, maybe this is a silly method and there's a better way to get the gridlines I want. I'm thinking there also might be a way to do this with a trans function, like building a trans_new(), but I don't know how to go about that.

Thanks in advance!

like image 614
camille Avatar asked May 25 '18 03:05

camille


1 Answers

I would like to propose an alternative approach using annotation_custom(), which achieves a similar look, at least in this case:

ggplot(df, aes(x = year, y = value, color = industry)) +
  geom_line() + 
  annotation_custom(
    grob = grid::rectGrob(gp = grid::gpar(col = NA, fill = "white")),
    xmin = max(df$year)
  ) +
  geom_text(aes(label = industry), data = . %>% filter(year == max(year)), 
            size = 3, hjust = 0, nudge_x = 0.1) +

  # note: my version of ggplot2 doesn't have the expand_scale function, but I suppose
  # it isn't critical to the question here
  scale_x_continuous(expand = c(0.2, 0), breaks = c(min(df$year), max(df$year)), 
                     name = NULL) +
  scale_y_continuous(limits = c(0, NA), name = "Value (thousands)") +
  theme_minimal() +
  theme(legend.position = "none", 
        panel.grid.major.x = element_blank(), 
        panel.grid.minor = element_blank())

plot

annotation_custom's default location parameters are Inf / -Inf, stretching the displayed grob to all four edges of the plot panel, so specifying xmin = max(df$year) is sufficient to have it cover grid lines for everything to the right of max(df$year).

This trick works best with ggplot themes that do not have a visible panel.border component, such as theme_minimal, theme_grey, or theme_dark. If you are using theme_bw / theme_linedraw / theme_light, the illusion becomes less effective. (As for theme_classic / theme_void, they have no grid lines, so it doesn't matter either way.)

like image 121
Z.Lin Avatar answered Nov 15 '22 07:11

Z.Lin