避免使用 facet_wrap 从 ggplot 进行情节转换中的图例重复

Posted

技术标签:

【中文标题】避免使用 facet_wrap 从 ggplot 进行情节转换中的图例重复【英文标题】:Avoid legend duplication in plotly conversion from ggplot with facet_wrap 【发布时间】:2021-11-16 05:44:45 【问题描述】:

考虑由以下代表产生的情节。请注意,ggplot 具有合理的图例,而在 plotly 中,图例被大量重复,每次同一类别(“制造商”)出现在每个方面时都有一个条目。如何使情节图例更好地匹配 ggplot2 的图例?

library(plotly)
library(ggplot2)

p <- mpg %>% 
  ggplot(aes(year)) +
  geom_ribbon(aes(ymin=cty, ymax=hwy, fill = manufacturer), alpha=0.2) + 
  geom_line(aes(y = hwy, col=manufacturer))  +
  facet_wrap(~class)
p

plotly::ggplotly(p)

【问题讨论】:

【参考方案1】:

感谢@stefan,您的出色回答既教会了我有关情节对象的知识,又启发了我进一步发展您的概念。

我创建了具有以下功能的函数:

    它将您的逻辑转换为使用 plotly 对象作为输入的函数。 它应用了 purrr 库。 该函数接受允许覆盖图例条目的可选第二个参数 (.new_legend)。

代码肯定比你的代码长,尽管它被函数 assign_leg_grp 拉长了,它支持覆盖,也被我的“展开”样式。

library(plotly)
library(ggplot2)
library(purrr)
library(stringr)

p <- mpg %>% 
  ggplot(aes(year)) +
  geom_ribbon(aes(ymin=cty, ymax=hwy, fill = manufacturer), alpha=0.2) + 
  geom_line(aes(y = hwy, col=manufacturer))  +
  facet_wrap(~class)

gp <- ggplotly(p = p)

clean_pltly_legend <- function(.pltly_obj, .new_legend = c()) 
  # Cleans up a plotly object legend, particularly when ggplot is facetted
  
  assign_leg_grp <- function(.legend_group, .leg_nms) 
    # Assigns a legend group from the list of possible entries
    # Used to modify the legend settings for a plotly object
    
    leg_nms_rem <- .leg_nms
    
    parse_leg_nms <- function(.leg_options) 
      # Assigns a .leg_name, if possible
      # .leg_options is a 2-element list: 1 = original value; 2 = remaining options
      
      if (is.na(.leg_options)) 
        .leg_options
       else if(length(leg_nms_rem) == 0) 
        # No more legend names to assign
        .leg_options
       else 
        # Transfer the first element of the remaining options
        leg_nm_new <- leg_nms_rem[[1]]
        leg_nms_rem <<- leg_nms_rem[-1]
        
        leg_nm_new
      
      
    
    
    .legend_group %>% 
      map(~ parse_leg_nms(.))
    
  
  
  simplify_leg_grps <- function(.legendgroup_vec) 
    # Simplifies legend groups by removing brackets, position numbers and then de-duplicating
    
    leg_grp_cln <-
      map_chr(.legendgroup_vec, ~ str_replace_all(., c("^\\(" = "", ",\\d+\\)$" = "")))
    
    modify_if(leg_grp_cln, duplicated(leg_grp_cln), ~ NA_character_)
    
  
  
  pltly_obj_data <-
    .pltly_obj$x$data
  
  pltly_leg_grp <-
    # pltly_leg_grp is a character vector where each element represents a legend group. Element is NA if legend group not required or doesn't exist
    pltly_obj_data%>% 
    map(~ pluck(., "legendgroup")) %>% 
    map_chr(~ if (is.null(.)) NA_character_ else .) %>%
    # Elements where showlegend = FALSE have legendgroup = NULL. 
    
    simplify_leg_grps() %>% 
    
    assign_leg_grp(.new_legend) 
  
  pltly_obj_data_new <-
    pltly_obj_data %>% 
    map2(pltly_leg_grp, ~ list_modify(.x, legendgroup = .y)) %>%
    map2(pltly_leg_grp, ~ list_modify(.x, name = .y)) %>%
    map2(pltly_leg_grp, ~ list_modify(.x, showlegend = !is.na(.y)))
  # i.e. showlegend set to FALSE when is.na(pltly_leg_grp), TRUE when not is.na(pltly_leg_grp)
  
  .pltly_obj$x$data <- pltly_obj_data_new
  
  .pltly_obj
  


clean_pltly_legend(gp)

【讨论】:

【参考方案2】:

根据您的情况调整我在this 帖子上的回答(利用此answer),一种选择是操纵plotly 对象。

问题在于,对于分面,我们最终会为存在组的每个分面创建一个图例条目,即图例条目中的数字对应于分面或面板的编号。

plotly 中,可以通过legendgroup 参数防止重复的图例条目。使用ggplotly 时获得相同结果的一种选择是手动分配legendgroup,如下所示:

library(plotly)
library(ggplot2)

p <- mpg %>% 
  ggplot(aes(year)) +
  geom_ribbon(aes(ymin=cty, ymax=hwy, fill = manufacturer), alpha=0.2) + 
  geom_line(aes(y = hwy, col=manufacturer))  +
  facet_wrap(~class)

gp <- ggplotly(p = p)

# Get the names of the legend entries
df <- data.frame(id = seq_along(gp$x$data), legend_entries = unlist(lapply(gp$x$data, `[[`, "name")))
# Extract the group identifier
df$legend_group <- gsub("^\\((.*?),\\d+\\)", "\\1", df$legend_entries)
# Add an indicator for the first entry per group
df$is_first <- !duplicated(df$legend_group)

for (i in df$id) 
  # Is the layer the first entry of the group?
  is_first <- df$is_first[[i]]
  # Assign the group identifier to the name and legendgroup arguments
  gp$x$data[[i]]$name <- df$legend_group[[i]]
  gp$x$data[[i]]$legendgroup <- gp$x$data[[i]]$name
  # Show the legend only for the first layer of the group 
  if (!is_first) gp$x$data[[i]]$showlegend <- FALSE

gp

【讨论】:

谢谢!我注意到这不是必需的,例如,如果我放弃geom_line() 呼叫,这对我来说更像是一个错误?我在情节方面并不是特别有经验,所以不想为我自己的错误提交错误报告! 另请注意,反之亦然,注释掉 geom_ribbon 会导致线迹的单个图例项。这似乎与使用多种跟踪类型有关。此外,使用上面的代码(带有重复的图例项),线迹没有图例项。从我的角度来看,值得提出一个问题。

以上是关于避免使用 facet_wrap 从 ggplot 进行情节转换中的图例重复的主要内容,如果未能解决你的问题,请参考以下文章

Facet_Wrap标题与ggplotly中的y轴重叠?

在 ggplot/ggplotly 中使用 facet_wrap 时如何防止 y 轴挤压标签?

如何使用 ggplot 中的 facet_wrap 选项可视化每个组的大小?

如何使用 ggplot2 删除 facet_wrap 图中的额外列?

在“ggplot”和“ggmap”中使用“facet_wrap”来显示多个图形的问题

R语言可视化包ggplot2包使用facet_wrap绘制多面板图(子图)实战