tidyverse/ggplot2

guide_legend() fails to render vector shapes when overriding aesthetics

Closed this issue · 1 comments

Hi,

Here is my code:

rm(list=ls())
shape_events=c(EV = 4, CC = 17, WW = 19, ZZ = 19)
library(ggplot2)
library(dplyr)

res<- structure(list(USUBJID = c("D", "D",
                                 "D", "D", "D", "D",
                                 "D", "D", "B", "B",
                                 "B", "B", "B", "B",
                                 "B", "B", "B", "B",
                                 "B", "B", "B", "B",
                                 "B", "B", "E", "E",
                                 "E", "E", "E", "E",
                                 "E", "E", "E", "E",
                                 "E", "E", "E", "E",
                                 "E", "E", "A", "A",
                                 "A", "A", "A", "A",
                                 "A", "A", "C", "C",
                                 "C", "C", "C", "C",
                                 "C", "C", "C", "C"
), TRTSTART = c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                1), TRTEND = c(43, 43, 43, 43, 43, 43, 43, 43, 98, 98, 98, 98,
                               98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 100, 100, 100,
                               100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
                               30, 30, 30, 30, 30, 30, 30, 30, 43, 43, 43, 43, 43, 43, 43, 43,
                               43, 43), FUPEND = c(85, 85, 85, 85, 85, 85, 85, 85, 92, 92, 92,
                                                   92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, NA, NA, NA,
                                                   NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
                                                   NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA),
ARM = structure(c(4L, 2L, 4L, 2L, 4L, 4L, 2L, 2L, 3L, 2L,
                  3L, 2L, 3L, 2L, 3L, 3L, 3L, 3L, 3L, 2L, 2L, 2L, 2L, 2L, 3L,
                  2L, 3L, 2L, 3L, 2L, 3L, 3L, 3L, 3L, 3L, 2L, 2L, 2L, 2L, 2L,
                  1L, 2L, 1L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 2L, 1L,
                  1L, 2L, 2L), levels = c("Y3", "Follow-up", "Y2", "Y1",
                                          "CC", "WW", "WW", "ZZ", "EV"), class = "factor"),
EVENTDUR = c(43, 43, 43, 43, 1, 22, 1, 22, 22, 22, 84, 84,
             98, 98, 43, 64, 85, 22, 1, 43, 64, 85, 22, 1, 46, 46, 85,
             85, 100, 100, 1, 23, 44, 65, 86, 1, 23, 44, 65, 86, 2, 2,
             39, 39, 1, 22, 1, 22, 2, 23, 2, 23, 43, 43, 1, 22, 1, 22),
EVENTCD = structure(c(8L, 8L, 10L, 10L, 9L, 9L, 9L, 9L, 6L,
                      6L, 8L, 8L, 10L, 10L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L,
                      9L, 7L, 7L, 8L, 8L, 10L, 10L, 9L, 9L, 9L, 9L, 9L, 9L, 9L,
                      9L, 9L, 9L, 5L, 5L, 8L, 8L, 9L, 9L, 9L, 9L, 6L, 6L, 6L, 6L,
                      10L, 10L, 9L, 9L, 9L, 9L), levels = c("Y3", "Follow-up",
                                                            "Y2", "Y1", "CC", "WW", "WW", "ZZ", "EV",
                                                            "EOT"), class = "factor"), ORDER = structure(c(1L, 1L, 1L,  1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L,    3L, 3L, 3L, 3L, 3L, 3L, 3L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L), 
levels = c("1", "2", "3", "4", "5"), class = "factor"), 
Legend_Group = structure(c(10L,                                                                                                                                                          10L, 1L, 2L, 5L, 5L, 5L, 5L, 11L, 11L, 10L, 10L, 3L, 2L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 9L, 9L, 10L, 10L, 3L, 2L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 6L, 6L, 10L,10L, 5L, 5L, 5L, 5L, 11L, 11L, 11L, 11L, 4L, 2L, 5L, 5L,                                                                               5L, 5L), levels = c("Y1", "Follow-up", "Y2", "Y3", "EV",                                                                                                                                                                "CC", "CC beyond CC-period", "CC", "WW", "ZZ", "WW" ), class = "factor")), row.names = c("47", "471", "44", "441",
                                                                                                                                                                                                                                                                       "45", "46", "451", "461", "11", "111", "17", "171", "10", "101",
                                                                                                                                                                                                                                                                       "12", "13", "14", "15", "16", "121", "131", "141", "151", "161",
                                                                                                                                                                                                                                                                       "70", "701", "69", "691", "63", "631", "64", "65", "66", "67",
                                                                                                                                                                                                                                                                       "68", "641", "651", "661", "671", "681", "6", "610", "9", "91",
                                                                                                                                                                                                                                                                       "7", "8", "710", "83", "40", "41", "401", "411", "39", "391",
                                                                                                                                                                                                                                                                       "42", "43", "421", "431"), class = "data.frame")




cols_segment=NULL
cols_events=c("EV" = "black","CC" = "#a114a8","WW" = "#4634eb","ZZ" = "#E61A33")



  ### Graphical parameters #####
  arms<-unique(res$ARM)
  yores<-arms[arms!="Follow-up"]

  # colors
  cols_yores<- c(Y1 = "#CCCCFF", Y2 = "#33CCFF", Y3 = "#6666FF")
  cols_segment<-c(cols_yores,"Follow-up" = "grey")

  cols<-c(cols_segment,cols_events)

  # shapes
  shape_yores<-rep(NA,length(arms)) %>% setNames(arms)
  shape_override <- c(shape_yores, shape_events)

  # segments
  shape_segments<-rep(1,length(arms)) %>% setNames(arms)
  segment_events<-rep(NA,length(shape_events)) %>% setNames(names(shape_events))
  segment_override<-c(shape_segments,segment_events)

  # stroke
  stroke_segments<-rep(NA,length(arms)) %>% setNames(arms)
  stroke_events<-rep(2,length(shape_events)) %>% setNames(names(shape_events))
  stroke_override<-c(stroke_segments,stroke_events)

  # size
  size_segments<-rep(2,length(arms)) %>% setNames(arms)
  size_events<-rep(5,length(shape_events)) %>% setNames(names(shape_events))
  size_override<-c(size_segments,size_events)


  # Ordonner pour que les infusions soient à la fin (donc CCoix devant les ronds et carrés)
  res$EVENTCD<-droplevels(res$EVENTCD)
  levels<-levels(res$EVENTCD) %>% .[.!="EV"] %>% c(.,"EV")
  res$EVENTCD<- factor(res$EVENTCD,levels=levels)
  res<-res[order(res$EVENTCD),]

  geometrics<-unique(c(as.character(res$ARM), levels))
  cols<-cols[geometrics] %>% .[!is.na(names(.))]
  shape_override<-shape_override[geometrics]  %>% .[!is.na(names(.))]
  segment_override<-segment_override[geometrics]  %>% .[!is.na(names(.))]
  stroke_override<-segment_override[geometrics]  %>% .[!is.na(names(.))]
  size_override<-size_override[geometrics]  %>% .[!is.na(names(.))]

  # Formatting ######
  # je suis obligée de faire ça pour avoir une légende "fusionnée"
  # ajouter EV et ZZ dans les "levels" de ARM
  res$ARM<-factor(res$ARM, levels=names(cols))
  res$EVENTCD<-factor(res$EVENTCD, levels=c(names(cols),"EOT"))
  res <- res[order(res$ORDER),]

  # Separating follow up and non follow up data
  # for bars
  res_bar<-res[(res$USUBJID %in% res$USUBJID[res$EVENTCD == "EOT"]),] %>%
    .[.$ARM != "Follow-up",]
  # for arrows
  res_arrow<-res[(!res$USUBJID %in% res$USUBJID[res$EVENTCD == "EOT"]),]%>%
    .[.$ARM != "Follow-up",]

  # max days
  maxDays<- max(res$TRTEND + res$FUPEND, na.rm = T) + 10
  if (!is.finite(maxDays)) {
    maxDays<-max(res$TRTEND, na.rm=TRUE) + 10

  }

  p<-ggplot(res, aes(y = forcats::fct_inorder(ORDER),
                     group = forcats::fct_inorder(ORDER))) +
    geom_segment(data=res[res$ARM!="Follow-up",],
                 aes(x = TRTSTART, xend = TRTEND,   y = forcats::fct_inorder(ORDER), color = ARM),
                 linewidth = 2) +
    geom_segment(data=res[res$ARM=="Follow-up",],
                 aes(x = TRTEND, xend = TRTEND + FUPEND,   y = forcats::fct_inorder(ORDER), color = ARM),
                 linewidth = 2) +
    geom_segment(data = res_arrow,
                 aes(x = TRTEND, xend = TRTEND + 5, y = forcats::fct_inorder(ORDER), color = ARM),
                 arrow = arrow(type = "closed", length = unit(0.2, "inches")),
                 linewidth = 2, show.legend = FALSE) +  # Exclude arrow from legend
    geom_segment(data =res_bar,
                 aes(x = TRTEND, xend = TRTEND, y = forcats::fct_inorder(ORDER), color = ARM),
                 arrow = arrow(type = "open", length = unit(0.1, "inches"), angle = 90),
                 linewidth = 3, show.legend = FALSE) +  # Exclude arrow from legend
    geom_point(data = res[res$ARM!="Follow-up"&res$EVENTCD != "EOT"&!is.na(res$EVENTCD),],
               aes(x = EVENTDUR, y = forcats::fct_inorder(ORDER),
                   color = EVENTCD, shape = EVENTCD),
               size = 5, stroke = 2) +
    # Colors
    scale_color_manual(name = "Trial periods and events",
                       values = cols) +
    # Shape
    scale_shape_manual(name = "Trial periods and events",
                       values = shape_override)+
    # One legend for shape and colors
    guides(color = guide_legend(override.aes = list(stroke = stroke_override,
                                                    shape = shape_override,
                                                    linetype = segment_override,
                                                    size = size_override)),
           shape = "none") +
    # y-axis
    scale_y_discrete(name = "",
                     labels = res$USUBJID) +  # Display usubjid
    # X-axis
    scale_x_continuous(name = "Time (Days)") +  # x-axis by cycle (21 days)
    # Minimal theme
    theme_minimal() +
    # Personnalisation des axes et de la légende
    theme(title = element_text(size = 14, face = "bold"),
          axis.text.y = element_text(size = 12, face = "bold"),  # Taille de l'axe Y
          legend.position = "right",  # Placer la légende à droite
          legend.box = "vertical",  # Légende empilée verticalement
          plot.caption = element_text(margin = margin(t = 15),
                                      face = "italic",
                                      size = 8,
                                      hjust = 0),
          plot.caption.position = "plot")

p

which renders as
Image

As you can see, the cross doesn't appear in the legend... :/

Now, if I replace only the second line of my code and that I give a solid shape to "EV":

shape_events=c(EV = 15, CC = 17, WW = 19, ZZ = 19)

I get that:

Image It appears in the legend.

It doesn't make sense to me, so I guess it's a bug.

Here is my SessionInfo :


R version 4.5.1 (2025-06-13 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=French_France.utf8  LC_CTYPE=French_France.utf8    LC_MONETARY=French_France.utf8
[4] LC_NUMERIC=C                   LC_TIME=French_France.utf8    

time zone: Europe/Paris
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] dplyr_1.1.4   ggplot2_4.0.0

loaded via a namespace (and not attached):
 [1] labeling_0.4.3     RColorBrewer_1.1-3 R6_2.6.1           tidyselect_1.2.1   farver_2.1.2      
 [6] magrittr_2.0.3     gtable_0.3.6       glue_1.8.0         tibble_3.3.0       pkgconfig_2.0.3   
[11] generics_0.1.4     lifecycle_1.0.4    cli_3.6.5          S7_0.2.0           scales_1.4.0      
[16] grid_4.5.1         vctrs_0.6.5        withr_3.0.2        compiler_4.5.1     forcats_1.0.0     
[21] rstudioapi_0.17.1  tools_4.5.1        pillar_1.11.0      rlang_1.1.6  

Thank you in advance for your help and correction!

EDIT : No problem with ggplot2 version 3.5.0

Image

You're setting a non-solid shape to have stroke = NA for the EV level through stroke_override. This seems like a user error and not a bug on ggplot2's part. I'd also like to mention that it is easier to diagnose issues with a minimal example.