如何用 ggplot2 排列多列热图并保留多个独立图例?
可以继续采用“每列一个 ggplot,再用 patchwork 拼接”的方式。把各列的行顺序和坐标范围统一,单独划出图例区域,再根据需要决定是否保留 coord_fixed()。
由于没有实际效果图和目标图,下面的代码先处理可以明确的问题:列间空白、行对齐、列标题和多个独立图例的位置。
为什么只把 margin 设为 0 还不够
plot.margin 只控制整张子图的外边距,图例、坐标轴和固定纵横比仍会影响空间分配。
每张子图都设置了 coord_fixed(ratio = 1),绘图区的纵横比因此受到约束。当导出尺寸与布局不匹配时,即使外边距为 0,也可能出现空白。panel.spacing 主要控制分面之间的间距,不能消除这些独立 ggplot 之间的所有空白。
guides = "collect" 可以集中摆放图例,但不会让不同变量共享同一个色标;内容相同的图例可能被去重。不同单位的数据仍应分别设置色标,并给图例加上明确的标题。
统一数据与行顺序
先生成所有列共用的数据,再固定 uniqueID 的顺序。下面计算均值时使用了 na.rm = TRUE,与原代码有所不同:原代码中的 mean() 遇到缺失值会返回 NA,随后 drop_na() 可能删除整个汇总行。
library(tidyverse)
library(patchwork)
library(palmerpenguins)
penguins_formatted <- penguins %>%
drop_na(species, sex) %>%
group_by(species, sex) %>%
summarise(
MEAN_BILL_LENGTH = mean(bill_length_mm, na.rm = TRUE),
MEAN_BILL_DEPTH = mean(bill_depth_mm, na.rm = TRUE),
MEAN_FLIPPER_LENGTH = mean(flipper_length_mm, na.rm = TRUE),
MEAN_MASS = mean(body_mass_g, na.rm = TRUE),
.groups = "drop"
) %>%
rownames_to_column(var = "uniqueID")
row_ids <- penguins_formatted$uniqueID
penguins_formatted <- penguins_formatted %>%
mutate(uniqueID = factor(uniqueID, levels = row_ids))
这段代码先排除了 species 或 sex 缺失的记录。如果实际数据需要展示这些缺失类别,应保留它们并显式设置显示方式。如果某组的数值全部缺失,mean(..., na.rm = TRUE) 仍可能得到 NaN,需要按业务含义处理。
用一个函数生成所有热图列
把共用的坐标设置和主题放进函数,将变量、列标题和色标作为参数传入。增加到十几列时,就不必反复复制整段 theme()。
make_strip <- function(column, label, fill_scale) {
ggplot(
penguins_formatted,
aes(x = 1, y = uniqueID, fill = .data[[column]])
) +
geom_tile(
width = 1,
height = 1,
colour = "black",
linewidth = 0.25
) +
fill_scale +
scale_x_continuous(
limits = c(0.5, 1.5),
breaks = 1,
labels = label,
position = "top",
expand = expansion(mult = 0, add = 0)
) +
scale_y_discrete(
limits = rev(row_ids),
drop = FALSE,
expand = expansion(mult = 0, add = 0.5)
) +
theme_void() +
theme(
axis.text.x.top = element_text(
angle = 90,
hjust = 0,
vjust = 0.5,
size = 9,
margin = margin(b = 4)
),
panel.border = element_rect(
colour = "black",
fill = NA,
linewidth = 0.4
),
panel.background = element_rect(
colour = NA,
fill = "transparent"
),
plot.background = element_rect(
colour = NA,
fill = "transparent"
),
plot.margin = margin(0, 0, 0, 0),
legend.title = element_text(size = 9),
legend.text = element_text(size = 8)
)
}
plots <- list(
make_strip(
"species", "Species",
scale_fill_discrete(name = "Species")
),
make_strip(
"sex", "Sex",
scale_fill_manual(
name = "Sex",
values = c(female = "white", male = "black")
)
),
make_strip(
"MEAN_BILL_LENGTH", "Bill length",
scale_fill_fermenter(
name = "Bill length (mm)",
type = "seq",
palette = 2,
direction = 1
)
),
make_strip(
"MEAN_FLIPPER_LENGTH", "Flipper length",
scale_fill_fermenter(
name = "Flipper length (mm)",
type = "seq",
palette = 3,
direction = 1
)
)
)
代码中的几处设置:
x = 1是数值位置,因此使用scale_x_continuous(),并明确给出一个单元格的横向范围。- 所有列使用相同的
scale_y_discrete(),即使某列存在缺失值,也会保留完整的行位置。 rev(row_ids)让数据中的第一行显示在最上方。- 列名通过顶部刻度显示。原代码同时使用了
labs(x = ...)和axis.title = element_blank(),因此轴标题会被隐藏。 - 暂时去掉
coord_fixed(),让各列填满分配到的绘图区。单元格可以是矩形,整体布局也更容易调整。
scale_fill_fermenter() 保留了原代码的分段色标。如果需要连续渐变,可以将相应色标替换为 scale_fill_distiller(),并按数据范围选择合适的配色。
为所有图例预留一个区域
把 guide_area() 作为最后一个布局单元,让收集到的图例集中放在右侧。
combined <- wrap_plots(
c(plots, list(guide_area())),
nrow = 1,
widths = c(rep(1, length(plots)), 2.5),
guides = "collect"
) &
theme(
legend.position = "right",
legend.box = "vertical"
)
combined
前四列使用相同的相对宽度,最后的 2.5 为图例区域预留空间。这个值需要根据图例文字的长度和数量调整,不表示固定的厘米数。
guide_area() 与热图列放在同一层 patchwork 布局中,便于接收收集后的图例。使用 & theme(...) 可以统一设置图例位置。
如果图例太多,右侧区域可能放不下。可以增加导出高度,或者重新安排图例区域;压缩热图列宽无法解决图例本身过高的问题。
按最终用途设置导出尺寸
先确定输出尺寸,再调整图例宽度和文字大小。下面的尺寸只是一个起点:
ggsave(
"penguin_example.png",
plot = combined,
width = 12,
height = 6,
units = "in",
dpi = 300,
bg = "white"
)
# SVG export requires an available SVG device.
# ggsave(
# "penguin_example.svg",
# plot = combined,
# width = 12,
# height = 6,
# units = "in",
# device = "svg",
# bg = "transparent"
# )
扩展到十几列时,在 plots 中继续添加 make_strip() 调用,同时增加输出宽度,避免列标题和热图格子被挤得过窄。
如果必须使用正方形单元格,可以在函数中加回 coord_fixed(ratio = 1),再根据行数、列数和图例占用调整导出尺寸。固定比例会重新限制面板形状,可能再次产生空白,需要结合整体尺寸检查。
不同单位的列应保留各自的数值范围和图例。只有确实希望比较标准化后的相对大小时,才适合先进行标准化,再统一使用同一个色标。
备注:内容仅供参考。
