Search code examples
rggplot2geomaestheticsgeom-tile

ggplot legend with geom_tile and geom_line


I'm trying to produce a ggplot chart with two vertical axes where the user can decide which geom to use at each side. The options are lines, points and bars. I'm having troubles to have the correct shape in the legend when adding bars at the left and lines at the right.


MRE

library(ggplot2)
library(dplyr)
library(plotly)

# prepare raw data
df1 <- data.frame(ID = c("A", "A", "A", "A", "B", "B", "B", "B"), 
                  Date = structure(c(19078, 19085, 19092, 19099, 19078, 19085, 19092, 19099), class = "Date"),
                  Val = c(236, 221, 187, 136, 77, 100, 128, 180))

df2 <- data.frame(ID = c("J", "J", "J", "J", "K", "K", "K", "K"), 
                  Date = structure(c(19078, 19085, 19092, 19099, 19078, 19085, 19092, 19099), class = "Date"),
                  Val = c(478, 500, 549, 479, 73, 5, 15, 74))

# prepare y2 scaled data
ylim1 <- range(df1$Val)
ylim2 <- range(df2$Val)
scale_y2.1 <- function(y, ylim1, ylim2) {
  ylim1[1] + (ylim1[2] - ylim1[1]) *(y - ylim2[1])/(ylim2[2] - ylim2[1])
}
dfAll <- full_join(df1, df2, by = c("ID", "Date"), suffix = c("1", "2"))
y2.scl <- scale_y2.1(dfAll$Val2, ylim1, ylim2)
dfAll <- dfAll %>% mutate(Val2_scl = y2.scl)

# prepare y2 ticks and scaled breaks
labs2 <- pretty(ylim2)
brks2 <- scale_y2.1(labs2, ylim1, ylim2)

With lines and points it works fine

# prepare legend guides
legguides <- guides(color = guide_legend(title.position = "top",
                                         override.aes = list(shape = c(NA, NA, 20, 20),
                                                             linetype = c(1, 1, 0, 0))))

# prepare geoms
gglines <- geom_line(aes(x = Date, y = Val1, color = ID, group = ID), na.rm = TRUE)
ggpoints <- geom_point(aes(x = Date, y = Val2_scl, color = ID, group = ID), na.rm = TRUE)

# generate ggplot
ggplot(dfAll) + gglines + ggpoints +
  scale_y_continuous(sec.axis = dup_axis(breaks = rev(brks2), labels = rev(labs2), name = "Val2")) +
  coord_cartesian(ylim = ylim1) + ylab("Val1") +
  legguides

Lines and points

With bars and points it works fine too

# prepare legend guides
legguides <- guides(color = guide_legend(title.position = "top",
                                         override.aes = list(shape = c(15, 15, 20, 20),
                                                             linetype = c(0, 0, 0, 0))))

# prepare geoms
ggtiles <- geom_tile(aes(x = Date, y = Val1/2, height = Val1, fill = ID, group = ID),
                     na.rm = TRUE, stat = "identity", position = position_dodge(preserve = "single"),
                     show.legend = FALSE)

# generate ggplot
ggplot(dfAll) + ggtiles + ggpoints +
  scale_y_continuous(sec.axis = dup_axis(breaks = rev(brks2), labels = rev(labs2), name = "Val2")) +
  coord_cartesian(ylim = ylim1) + ylab("Val1") +
  legguides

Tiles and points

With bars and lines it fails to show any shape for the bars in the legend

# prepare legend guides
legguides <- guides(color = guide_legend(title.position = "top",
                                         override.aes = list(shape = c(15, 15, NA, NA),
                                                             linetype = c(0, 0, 1, 1))))

# prepare geoms
gglines <- geom_line(aes(x = Date, y = Val2_scl, color = ID, group = ID), na.rm = TRUE)

# generate ggplot
ggplot(dfAll) + ggtiles + gglines +
  scale_y_continuous(sec.axis = dup_axis(breaks = rev(brks2), labels = rev(labs2), name = "Val2")) +
  coord_cartesian(ylim = ylim1) + ylab("Val1") +
  legguides

Tiles and points

I've tried with different shape values in the guide_legend() but the plot just seems to ignore that property. I think it has something to do with the fact that the two first examples use a geom_point() call, while the last one doesn't. The shape is one of the properties of a geom_point() object, but not of a geom_line() one.

I know I've read in a SO answer to a question about a legend that someone suggested to use a dummy geom_something() call just to have a shape added to the legend. Maybe that could work here using geom_point(), but I cannot get to find that thread or documentation on how to add an empty/dummy geom.

In case someone thinks of a solution using geom_bar() instead of geom_tile() note that I'm using the later here based on this SO answer which helped me get the bars to be plotted inverted when one of the axes is reversed.


Solution

  • I don't know whether there is a solution for this problem which does not in fact is a workaround, but we can at least create the legend using a dummy geom_point() as already mentioned by you.

    Therefore, we can use the definition of ggpoints from the above examples and set its transparency to alpha = 0.0 such that the points are not visible any more. Note that we then additionally have to overwrite alpha in overwrite.aes() in order to make the bars visible in the legend.

    enter image description here

    Edited code parts:

    # prepare geoms
    ggpoints <-
        geom_point(aes(
            x = Date,
            y = Val2_scl,
            color = ID,
            group = ID
        ),
        na.rm = TRUE,
        alpha = 0.0)
    
    # prepare legend guides
    legguides <- guides(color = guide_legend(
        title.position = "top",
        override.aes = list(
            shape = c(15, 15, NA, NA),
            linetype = c(0, 0, 1, 1),
            alpha = 1.0
        )
    ))
    
    # generate ggplot
    ggplot(dfAll) + ggtiles + gglines + ggpoints +
        scale_y_continuous(sec.axis = dup_axis(
            breaks = rev(brks2),
            labels = rev(labs2),
            name = "Val2"
        )) +
        coord_cartesian(ylim = ylim1) + ylab("Val1") +
        legguides