You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决ggplot2绘图中图例在图表下方不对齐的问题

解决ggplot2+patchwork组合图底部图例错位问题

问题背景

这是此前技术问题的后续,该问题已由社区给出优雅解答。本次使用与之前相同的数据集,因set.seed和自动定义点形状的部分存在问题,对代码进行了微调,但运行后生成的图表底部图例出现错位,需要实现图例与图表下方的对齐。

数据集

topclones <- structure(list(CTaa_beta = c("CASSEGTSGGASTQYF", "CASSEGTSGGASTQYF", 
"CSVEDPSSGSYEQYF", "CASSVAGPNTEAFF", "CSARDPETYEQYF", "CASSVAGPNTEAFF", 
"CASSLGGLGTSTDTQYF", "CASSLRQGPSYEQYF", "CASSLGEYYGYTF", "CASSFGYTGELFF", 
"CASSVGNRGGTDTQYF", "CASSELAGGQETQYF", "CSVPAGYTDTQYF", "CASSPGTAVQGYTF", 
"CASSNWGGRGHTDTQYF", "CASRDSDGLAYQPQHF", "CSVPGTSTSGEQFF", "CASKPGTTSNQPQHF", 
"CASSYPTSGANVLTF", "CSASTGADYNEQFF", "CSARVPTSGDYNEQFF", "CASSYPTSGANVLTF", 
"CASRPEQGGPYEQYF", "CSARGGKENSPLHF", "CSVAGTGVYNEQFF", "CSVVPGGQGGYEQYF", 
"CASSLEGRERYEQFF", "CASSVGLFSTDTQYF", "CASSLRGGPYNEQFF", "CASSLLAGGNNEQFF", 
"CASSPLQGPSQPQHF", "CATSGRGDEVGELFF", "CSARAGESGRAMEQFF", "CASSLRQGPSYEQYF", 
"CASTPAVRDGNYEQYF", "CASSPSTGYNEQFF", "CASSSGGLDEQYF", "CASSQDRGTGANVLTF", 
"CASSFGTENTGELFF", "CATSGRGDEVGELFF", "CAWSVQSGGHEQYF", "CASSPGTAVQGYTF", 
"CSARGGKENSPLHF", "CASSDSGGAYNEQFF", "CASSQDSGSGANVLTF", "CASSAGLAGGYEQYF", 
"CASSSPGTTNEKLFF", "CASSLRGGPYNEQFF", "CASSQAKGGGETQYF"), Group = structure(c(6L, 
7L, 3L, 4L, 1L, 3L, 1L, 4L, 1L, 1L, 4L, 2L, 1L, 2L, 2L, 6L, 2L, 
4L, 7L, 2L, 7L, 6L, 7L, 7L, 4L, 4L, 4L, 7L, 6L, 4L, 7L, 6L, 1L, 
3L, 4L, 2L, 4L, 1L, 7L, 7L, 2L, 3L, 6L, 2L, 6L, 4L, 4L, 7L, 7L
), levels = c("HC PBMC", "axSpA PBMC", "axSpA SFMC", "InEx", 
"PD-1+ TIGIT+", "ReA PBMC", "ReA SFMC"), class = "factor"), n = c(441L, 
292L, 345L, 303L, 268L, 264L, 242L, 200L, 218L, 211L, 163L, 242L, 
166L, 225L, 223L, 59L, 209L, 125L, 53L, 177L, 48L, 44L, 46L, 
45L, 99L, 96L, 94L, 41L, 39L, 89L, 40L, 38L, 97L, 97L, 85L, 128L, 
80L, 88L, 36L, 35L, 117L, 84L, 30L, 106L, 29L, 67L, 66L, 29L, 
29L), Total_n_per_group = c(2770L, 2899L, 7226L, 6400L, 7081L, 
7226L, 7081L, 6400L, 7081L, 7081L, 6400L, 9999L, 7081L, 9999L, 
9999L, 2770L, 9999L, 6400L, 2899L, 9999L, 2899L, 2770L, 2899L, 
2899L, 6400L, 6400L, 6400L, 2899L, 2770L, 6400L, 2899L, 2770L, 
7081L, 7226L, 6400L, 9999L, 6400L, 7081L, 2899L, 2899L, 9999L, 
7226L, 2770L, 9999L, 2770L, 6400L, 6400L, 2899L, 2899L), Percent = c(15.92, 
10.07, 4.77, 4.73, 3.78, 3.65, 3.42, 3.12, 3.08, 2.98, 2.55, 
2.42, 2.34, 2.25, 2.23, 2.13, 2.09, 1.95, 1.83, 1.77, 1.66, 1.59, 
1.59, 1.55, 1.55, 1.5, 1.47, 1.41, 1.41, 1.39, 1.38, 1.37, 1.37, 
1.34, 1.33, 1.28, 1.25, 1.24, 1.24, 1.21, 1.17, 1.16, 1.08, 1.06, 
1.05, 1.05, 1.03, 1, 1)), row.names = c(NA, -49L), class = c("tbl_df", 
"tbl", "data.frame"))

调整后的代码

library(dplyr)
library(scales)
library(colorspace)
library(ggplot2)
library(patchwork)

# Remove unused factor level (PD1+TIGIT+ in Group)
topclones$Group <- droplevels(topclones$Group)

# Create tibble defining unique point shapes for each duplicated value in CTaa_beta
styles <- topclones %>%
  filter(n() > 1, .by = CTaa_beta) %>%
  select(CTaa_beta, Group) %>%
  mutate(point_shape = setNames(sample(0:20, length(unique(CTaa_beta))), unique(CTaa_beta))[CTaa_beta])

# Sort topclones, join styles to topclones while matching Group to Group and CTaa_beta to CTaa_beta
topclones <- topclones |>
  arrange(Group, CTaa_beta) |>
  left_join(styles, by = join_by(Group, CTaa_beta))

# Generate min and max colour hues for each Group based on hue_pal()
colour1 <- darken(hue_pal()(length(unique(topclones$Group))), 0.5)
colour2 <- lighten(hue_pal()(length(unique(topclones$Group))), 0.5)

# Initialise colours column, loop through groups, assign colourramps
topclones$colours <- NA
ct <- 0
for (i in unique(topclones$Group)) {
  
  ct <- ct + 1
  n <- length(unique(topclones$CTaa_beta[topclones$Group == i]))
  groups <- unique(topclones$CTaa_beta[topclones$Group == i])
  group_colours <- setNames(colorRampPalette(c(colour1[ct], colour2[ct]))(n), groups)
  topclones$colours[topclones$Group == i] <- group_colours[topclones$CTaa_beta[topclones$Group == i]]
  
}

# Generate list of plots for each Group
p_tmp <- lapply(unique(topclones$Group), \(x) {
  
  tmp <- topclones |> filter(Group == x)
  
  ggplot(tmp) +
    geom_col(aes(CTaa_beta, Percent, fill = CTaa_beta),
             width = 1) +
    geom_point(aes(CTaa_beta, 0.25, shape = CTaa_beta),
               size = 2,
               stroke = 1,
               colour = "white") +
    labs(x = x) +
    scale_fill_manual(name = "", values = tmp$colours) +
    scale_shape_manual(name = "", values = tmp$point_shape)
  
})

# Get rounded up max summed Percent for y-axis (in case of > 1 CTaa_beta value per Group)
max_y <- ceiling(max(aggregate(Percent ~ Group + CTaa_beta,
                               data = topclones,
                               FUN = sum)$Percent))

# Get vector of Group lengths for plot widths
pw <- topclones |>
  summarise(n = n_distinct(CTaa_beta), .by = Group) |>
  pull(n)

# Plot
p <- wrap_plots(p_tmp,
                nrow = 1,
                axes = "collect_y") +
  plot_layout(widths = unit(pw, rep("cm", length(pw))),
              heights = unit(rep(16, length(pw)), rep("cm", length(pw)))) &
  geom_text(aes(CTaa_beta, Percent, label = Percent),
            vjust = -.5,
            position = position_dodge(1)) &
  scale_y_continuous(limits = c(0, max_y),
                     breaks = seq(0, max_y, length.out = 5)) &
  guides(fill = guide_legend(nrow = 10),
         shape = guide_legend(override.aes = list(size = 1, stroke = 0.75))) &
  ylab("Frequency of CTaa_beta (% total cells per group)") &
  coord_cartesian(expand = FALSE,
                  clip = "off") &
  theme_classic() &
  theme(axis.text = element_text(size = 18),
        axis.title = element_text(size = 18),
        axis.title.x = element_text(vjust = -1),
        axis.ticks.x = element_blank(),
        legend.text = element_text(size = 10),
        legend.position = "bottom",
        legend.justification = c(0.5, 0.5),
        legend.key.spacing.y = unit(0, "mm"),
        axis.text.x = element_blank())

suppressWarnings(
  ggsave("/Users/zoyaqaiyum/Downloads/gradient_colours_by_group_no_thatching.jpg",
         p,
         width = 30, 
         height = 12,
         dpi = 150)
  )

解决方案

图例错位的核心原因是patchwork默认的图例收集逻辑未与主图宽度对齐,通过以下两处修改可解决问题:

1. 子图隐藏独立图例,由patchwork统一收集

修改p_tmp生成代码,给每个子图添加theme(legend.position = "none"),避免子图单独渲染图例:

# Generate list of plots for each Group
p_tmp <- lapply(unique(topclones$Group), \(x) {
  
  tmp <- topclones |> filter(Group == x)
  
  ggplot(tmp) +
    geom_col(aes(CTaa_beta, Percent, fill = CTaa_beta),
             width = 1) +
    geom_point(aes(CTaa_beta, 0.25, shape = CTaa_beta),
               size = 2,
               stroke = 1,
               colour = "white") +
    labs(x = x) +
    scale_fill_manual(name = "", values = tmp$colours) +
    scale_shape_manual(name = "", values = tmp$point_shape) +
    theme(legend.position = "none")  # 子图不显示图例
  
})

2. 优化全局图例布局与对齐设置

修改组合图部分的wrap_plots和theme参数,统一收集图例并设置居中对齐:

# 修改后的Plot部分
p <- wrap_plots(p_tmp,
                nrow = 1,
                axes = "collect_y",
                guides = "collect") +  # 统一收集所有子图图例
  plot_layout(widths = unit(pw, rep("cm", length(pw))),
              heights = unit(rep(16, length(pw)), rep("cm", length(pw)))) &
  geom_text(aes(CTaa_beta, Percent, label = Percent),
            vjust = -.5,
            position = position_dodge(1)) &
  scale_y_continuous(limits = c(0, max_y),
                     breaks = seq(0, max_y, length.out = 5)) &
  guides(
    fill = guide_legend(nrow = 10, byrow = TRUE),  # 按行排列图例项
    shape = guide_legend(override.aes = list(size = 1, stroke = 0.75), nrow = 10, byrow = TRUE),
    merge = TRUE  # 合并fill与shape图例
  ) &
  ylab("Frequency of CTaa_beta (% total cells per group)") &
  coord_cartesian(expand = FALSE,
                  clip = "off") &
  theme_classic() &
  theme(axis.text = element_text(size = 18),
        axis.title = element_text(size = 18),
        axis.title.x = element_text(vjust = -1),
        axis.ticks.x = element_blank(),
        legend.text = element_text(size = 10),
        legend.position = "bottom",
        legend.justification = "center",  # 图例内容居中
        legend.box.just = "center",  # 图例框整体居中
        legend.key.spacing.y = unit(0, "mm"),
        legend.margin = margin(t = 10, b = 0, unit = "mm"),  # 调整图例上下边距
        plot.margin = margin(b = 30, unit = "mm"),  # 主图底部留足图例空间
        axis.text.x = element_blank())

修改后重新运行代码,图例将自动居中对齐在图表正下方,解决错位问题。

内容的提问来源于stack exchange,提问作者Zoya Qaiyum

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 07:59:50