1

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.

djbetancourt
  • 347
  • 3
  • 11

1 Answers1

1

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
Jan
  • 2,245
  • 1
  • 2
  • 16
  • Thank you, seems to work great! Just a question. Would the `alpha` specification in `override.aes` affect the coloring of the data or just the legend? I'm asking because another functionality I need to implement is opacity based on one additional variable. Would I still be able to set custom opacities to the points, bars or lines even if I use `alpha = 1.0` in `override.aes`? – djbetancourt Jul 09 '23 at 03:41
  • `override.aes` only changes the legend appearance, the rest of the plot remains unchanged. Hence it should be possible that you set custom opacities to other `geom` objects. – Jan Jul 09 '23 at 09:01
  • Awesome, thank you! – djbetancourt Jul 09 '23 at 17:32