Search code examples
rggplot2facetfacet-wrap

How to make a facet plot with some lines repeated in each facet from a single dataframe?


I'm trying to create a facet plot using a data.table. Ideally I would like to add the maximum and minimum acceptable values in each facet so it is easier to compare. They are all in the same data.table. They also should not have their own facet, since they are reference values.

After looking around I've seen this proposed for adding the lines to each facet but I can't seem to make it work. The limits have their own facets and do not appear on the rest of facets. The complete opposite of what I intended!

library(ggplot2)

x_values <- rep(1:20, 5)
y_values <- runif(1:100)
class_labels <- rep(c("A", "B", "C", "UpperLimit", "LowerLimit"), times = c(20,20,20, 20,20 ))

# Combine into a data frame
dt <- data.table(X = x_values, Y = y_values, Class = class_labels)
dt[class_labels == "UpperLimit", Y := Y * 2]
dt[class_labels == "LowerLimits", Y := Y *-1]

# Print the resulting datatable
p <- ggplot(data = dt[class_labels != c("UpperLimits", "LowerLimits") ], aes(X,Y)) + 
  geom_line() + facet_wrap(~(class_labels)) + 
  geom_line(data = dt[class_labels == "UpperLimits"]) + aes(x = X, y = Y) +
  geom_line(data = dt[class_labels == "LowerLimits"]) + aes(x = X, y = Y)

This is what i get: enter image description here


Solution

  • First, some things we should clear up before answering the initial question.

    1. The use of class_labels (not in the frame) as both a filtering vector and faceting is not good practice, especially since you have Class in the data. While there are exceptions, I almost always put variables I need in plotting in the data itself. In this case, replace all instances of class_labels in your plot code with Class.

    2. c("UpperLimits", "LowerLimits") do not exist, they should be c("UpperLimit", "LowerLimit").

    3. In dt[class_labels != c("UpperLimits", "LowerLimits"),], both sides of the != (and == and < and others) must be a clean multiple of each other, but in reality they should be length 1 or the same length as the other. There are clever tricks out there using mis-matching vector lengths, some of them useful, but in most cases (in my experience) when comparing two different-length vectors, if one is shorter than the other and not length-1, then it is a mistake. If you're fortunate, it will trigger a warning/error; if you're not (as in this case), R will silently and sloppily recycle your length-2 vector. In this case, you are effectively comparing

      class_labels != c("UpperLimits", "LowerLimits", "UpperLimits", "LowerLimits", "UpperLimits", "LowerLimits", "UpperLimits", "LowerLimits", ...)
      ### out to length 100, which is length(class_labels)
      

      I think the unintentional effect of recycling should be clearer there.

      This should be !Class %in% c(..). See What is the difference between `%in%` and `==`?, Difference between the == and %in% operators in R, Filter multiple values on a string column in dplyr.

    All of that said, to get the two limits super-imposed on all facets, there are a couple of options.

    1. Use annotate:

      ggplot(data = dt[!Class %in% c("UpperLimit", "LowerLimit") ], aes(X,Y)) + 
        geom_line() + 
        facet_wrap(~ Class) + 
        annotate(geom = "line", x = dt[Class == "UpperLimit", X], y = dt[Class == "UpperLimit", Y], color = "red", linetype = "dashed") + 
        annotate(geom = "line", x = dt[Class == "LowerLimit", X], y = dt[Class == "LowerLimit", Y], color = "green", linetype = "dashed")
      

      three facet plot with red/green lines in all facets showing upper and lower limits

    2. Impose the upper/lower limits in all three Classes. (A little more work, but shows how you could duplicate the limits into some of the facets, if needed.) I'll preserve the original Class so that we can aesthetic it to match the previous plot (where I assigned color= and linetype= manually).

      ggplot(data = dt[!Class %in% c("UpperLimit", "LowerLimit") ], aes(X,Y)) + 
        geom_line() + 
        facet_wrap(~ Class) + 
        geom_line(data = dt_limits, aes(color = FakeClass), linetype = "dashed") + 
        scale_color_manual(values = c("UpperLimit" = "red", "LowerLimit" = "green"))
      

      same upper/lower limit faceted plot, now with a legend to indicate limit

      I kept the legend in there, you can remove it if desired with + guides(color = "none"). (I don't think there's an easy way to get the legend on the first plot made using annotate(.), in case that's important to you.)