1. 和弦图:不只是好看,更是关系的“翻译官”

如果你做过数据分析,肯定遇到过这种头疼事:手里有一堆数据,想看看不同组别之间是怎么“勾勾搭搭”的,关系有多强,谁和谁最“铁”。用表格?密密麻麻的数字看得人眼花。用普通的条形图或散点图?又很难直观地表达出这种多对多的复杂网络关系。

这时候,和弦图就该登场了。我第一次看到这种图的时候,就觉得它简直是个天才设计——它用一个圆环,把所有的“参与者”(我们叫它“扇区”)摆成一圈,然后用一条条像丝带一样的“连接”,把有关系的两者连起来。丝带的宽度代表关系的强弱,颜色还能再表达一层含义,比如关系的类型。每个扇区本身的大小,也能代表这个“参与者”自身的重要性或总量。

想象一下,你要分析一个公司内部不同部门之间的协作强度,或者研究社交媒体上不同话题之间的关联度,甚至看看不同基因之间的调控关系。用和弦图来呈现,谁跟谁关系紧密,谁是个“孤岛”,一眼就能看明白,信息密度极高。

在R语言里,想要画出专业又好看的和弦图,circlize 包绝对是你的不二之选。它功能强大,定制性极高,但刚上手时,那一堆参数可能会让人有点发怵。别担心,我用了这么多年,踩过的坑都帮你填平了。这篇文章,我们就抛开那些基础的“Hello World”式绘图,直接深入 chordDiagram() 函数的核心,用实战案例带你玩转高级技巧,让你的和弦图从“能看”升级到“惊艳”。

2. 数据准备:从“原材料”到“可下锅”

画图第一步,永远是准备数据。chordDiagram() 非常贴心,它接受两种最常用的数据格式:邻接矩阵和邻接列表。选哪种,取决于你的数据来源和个人习惯。

2.1 邻接矩阵:方方正正,一目了然

邻接矩阵就像一张关系强度表。行和列的名字代表不同的对象,矩阵中间的值就代表这两个对象之间的关系强度。数值越大,关系越强。这种格式特别适合从相关矩阵、流量矩阵等计算直接得到的数据。

# 创建一个3行6列的随机矩阵,模拟3个源(S)和6个目标(E)之间的关系
set.seed(123) # 设置随机种子,保证结果可复现
mat <- matrix(sample(18, 18), 3, 6)
rownames(mat) <- paste0("Source_", 1:3) # 行名,代表关系的“出发点”
colnames(mat) <- paste0("Target_", 1:6) # 列名,代表关系的“到达点”
print(mat)

运行后,你会得到一个有明确行列名的矩阵。这种格式的优点是直观,整个关系网络全在一张表里。后续设置颜色、边框等图形参数时,也常常需要构造对应的矩阵,逻辑一致。

2.2 邻接列表:灵活轻便,处理复杂关系

邻接列表则是一个长长的数据框,通常至少包含三列:第一列是源(from),第二列是目标(to),第三列是关系值(value)。这种格式在数据库查询结果或整理后的数据中非常常见,尤其适合关系数量远少于所有可能组合的情况。

# 将上面的矩阵转换为邻接列表
library(tidyr)
library(tibble)

df <- mat %>%
  as.data.frame() %>%
  rownames_to_column(var = "from") %>%
  pivot_longer(cols = -from, names_to = "to", values_to = "value")
head(df)

你会发现,这个数据框把矩阵中的每一个非零(即使为零,但位置存在)关系都变成了一行记录。这种格式的灵活性极高,比如你可以轻松地添加第四列、第五列,用来存放关系的类型、颜色、边框粗细等额外属性,这在后续高级定制时会非常方便。

提示:我个人的经验是,如果你的数据天生就是矩阵形式(比如相关系数矩阵),那就直接用矩阵。如果你的数据是从数据库里一条条抽出来的“流水账”,或者需要处理一对多、多对多的复杂属性,那么从一开始就整理成邻接列表格式会更省心。两种格式用 tidyr 包里的 pivot_longer 和 pivot_wider 函数可以轻松转换,不必纠结。

3. 视觉定制的核心:颜色与连接的精细化控制

一张和弦图是否专业、是否易于解读,颜色和连接的细节处理是关键。chordDiagram() 提供了极其精细的控制参数,我们一起来攻克它们。

3.1 扇区颜色:奠定整体基调

扇区颜色通过 grid.col 参数设置。强烈建议使用命名向量的方式来指定,这样可以避免因扇区顺序变动而导致的颜色错配。通常,我们会将源(矩阵的行或数据框的第一列)和目标(矩阵的列或数据框的第二列)分成两大组,用不同的色系区分。

# 定义扇区颜色,源用暖色,目标用冷色
grid.col <- c(
  Source_1 = "#FF6B6B", # 珊瑚红
  Source_2 = "#4ECDC4", # 青绿色
  Source_3 = "#FFD166", # 琥珀黄
  Target_1 = "#118AB2", # 钢蓝色
  Target_2 = "#073B4C", # 深蓝
  Target_3 = "#06D6A0", # 碧绿
  Target_4 = "#EF476F", # 粉红
  Target_5 = "#7209B7", # 紫罗兰
  Target_6 = "#F15BB5"  # 亮粉
)

# 基础绘图
library(circlize)
chordDiagram(mat, grid.col = grid.col)
circos.clear() # 切记!画完用circos.clear()重置画布,否则会影响后续绘图

默认情况下,连接的颜色继承自其源扇区的颜色。所以当你设置了扇区颜色后,连接的颜色也就自动赋予了意义。如果你希望连接颜色继承目标扇区颜色,一个简单的技巧是对输入矩阵进行转置 chordDiagram(t(mat), grid.col = grid.col),这样行列关系就互换了。

3.2 连接颜色:映射第三维度信息

连接的颜色可以不仅仅是继承,它本身可以作为一个强大的视觉通道,来展示关系的另一个属性,比如关系类型、正负性、统计显著性等。

对于矩阵输入,你可以直接提供一个颜色矩阵:

# 生成一个与数据矩阵同尺寸的随机颜色矩阵,并设置透明度
col_mat <- rand_color(length(mat), transparency = 0.3)
dim(col_mat) <- dim(mat) # 将颜色向量重塑为矩阵
rownames(col_mat) <- rownames(mat)
colnames(col_mat) <- colnames(mat)

chordDiagram(mat, grid.col = grid.col, col = col_mat)
circos.clear()

更强大的方法是使用颜色映射函数。当你的关系值是连续型数值时(比如相关系数从-1到1),这招特别有用。我们可以用 colorRamp2() 函数创建一个映射函数。

library(circlize)
# 假设我们的矩阵值范围是 min(mat) 到 max(mat)
# 我们想用“蓝-白-红”的渐变色来表示值的大小
col_fun <- colorRamp2(c(min(mat), mean(range(mat)), max(mat)), 
                        c("#4575B4", "#FFFFBF", "#D73027"),
                        transparency = 0.2)

chordDiagram(mat, grid.col = grid.col, col = col_fun)
circos.clear()

现在,连接的颜色深浅就直观地反映了关系值的强弱。对于邻接列表 df,用法更直接:chordDiagram(df, grid.col = grid.col, col = col_fun(df$value))。

3.3 连接边框与样式:突出关键连接

有时候,我们需要特别强调某些重要的连接,比如超过阈值的关系。这时,可以给它们加上醒目的边框。

link.lwd(边框宽度)、link.lty(线型)和 link.border(边框颜色)这三个参数可以协同工作。它们可以是单个值(应用于所有连接),也可以是矩阵或数据框(用于精细控制)。

# 1. 全局设置:所有连接都用2磅宽的红色虚线边框
chordDiagram(mat, grid.col = grid.col, 
             link.lwd = 2, 
             link.lty = 2, 
             link.border = "red")
circos.clear()

# 2. 矩阵控制:只对关系值大于12的连接加粗加红色边框
lwd_mat <- matrix(1, nrow = nrow(mat), ncol = ncol(mat)) # 默认宽度为1
border_mat <- matrix(NA, nrow = nrow(mat), ncol = ncol(mat)) # 默认无边框

# 找到需要强调的位置
highlight_pos <- which(mat > 12, arr.ind = TRUE)
lwd_mat[highlight_pos] <- 3
border_mat[highlight_pos] <- "#E63946" # 一种更鲜艳的红色

chordDiagram(mat, grid.col = grid.col,
             link.lwd = lwd_mat,
             link.border = border_mat)
circos.clear()

对于邻接列表,控制起来简直是一种享受。你只需要在数据框里增加几列,分别命名为 link.lwd、link.border 等,绘图时这些列会自动被识别并应用。这是我非常推荐使用邻接列表格式的原因之一,管理和修改属性非常直观。

4. 方向、排序与布局:让故事线更清晰

和弦图不是把数据堆上去就完事了,合理的布局能引导观众的视线,讲好数据背后的故事。

4.1 控制连接方向:让数据流动起来

很多关系是有方向性的,比如资金的流向、信息的传递。directional 参数就是为此而生。

  • directional = 1:默认方向,对于矩阵是行指向列,对于列表是第一列指向第二列。
  • directional = -1:方向反转。
  • directional = 2:绘制双向箭头。

仅仅设置方向,视觉差异可能不明显。通常我们会结合 diffHeight 参数,让起始端比终止端“矮”一截,模拟“流出”的感觉。

par(mfrow = c(1, 2)) # 并排显示两幅图
chordDiagram(mat, grid.col = grid.col, directional = 1,
             title("仅设置方向"))
chordDiagram(mat, grid.col = grid.col, directional = 1,
             diffHeight = mm_h(4), # 设置高度差,mm_h是circlize的单位函数
             title("方向 + 高度差"))
circos.clear()

想要更明确的方向指示?可以加上箭头。direction.type = c("diffHeight", "arrows") 可以同时启用两种效果。你甚至可以用 link.arr.type = "big.arrow" 来绘制那种贯穿整个连接带的超大箭头,特别适合连接数量不多但需要强烈视觉引导的图。

4.2 连接排序与绘制顺序:解决视觉混乱

当某个扇区连接很多时,所有连接带会堆叠在一起,可能会显得杂乱。link.sort 和 link.decreasing 参数可以帮我们按连接宽度进行排序,让图形看起来更有秩序。

par(mfrow = c(1, 2))
chordDiagram(mat, grid.col = grid.col, 
             link.sort = TRUE, 
             link.decreasing = TRUE)
title("从宽到窄排列 (decreasing=TRUE)")
chordDiagram(mat, grid.col = grid.col,
             link.sort = TRUE,
             link.decreasing = FALSE)
title("从窄到宽排列 (decreasing=FALSE)")
circos.clear()

另一个隐藏的细节是绘制顺序(Z-index)。默认按数据顺序绘制,可能导致宽连接被窄连接盖住。link.zindex 参数可以控制谁画在上面。通常我们让宽连接在上层更清晰:link.zindex = rank(mat)。

4.3 扇区顺序与间隔:自定义你的环形舞台

扇区默认按字母顺序排列。你可以用 order 参数手动指定一个顺序向量,把重要的、有关联的扇区放在一起,方便比较。

圆环上扇区之间的间隔用 gap.after 控制,单位是角度。你可以给每个扇区后面指定不同的间隔,从而把不同组别的扇区“集群”分开。更简单的是用 big.gap 参数,它直接设置源组和目标组两大集群之间的间隔,我常用 big.gap = 30,能让图形结构立刻清晰起来。

# 使用 big.gap 清晰分隔源和目标
chordDiagram(mat, grid.col = grid.col, big.gap = 30)
circos.clear()

5. 高级实战案例:从模仿到创造

掌握了这些零件,我们就可以像搭积木一样,创作出解决实际问题的复杂和弦图了。下面我分享两个实战中常用的案例。

5.1 案例一:带正负流向的贸易关系图

假设我们有一个国际贸易数据,国家之间既有出口(正值)也有进口(负值)。我们想用一张图同时展示两种流向,并用颜色区分。

# 模拟数据:3个国家,双向贸易值(有正负)
set.seed(456)
countries <- c("Country_A", "Country_B", "Country_C")
trade_df <- expand.grid(from = countries, to = countries)
trade_df <- trade_df[trade_df$from != trade_df$to, ] # 移除自贸易
trade_df$net_export <- sample(c(-10:-1, 1:10), nrow(trade_df), replace = TRUE) # 净出口值

# 准备颜色:正值为红色系(出口),负值为蓝色系(进口)
library(scales)
trade_df$link_color <- ifelse(trade_df$net_export > 0,
                               alpha("#D73027", 0.7), # 红色,带透明度
                               alpha("#4575B4", 0.7)) # 蓝色,带透明度

# 扇区颜色
country_col <- setNames(brewer.pal(3, "Set2"), countries)

# 绘制:关键是将宽度绝对值化,颜色代表正负
chordDiagram(trade_df[, c("from", "to", "net_export")],
             grid.col = country_col,
             col = trade_df$link_color,
             link.lwd = 2,
             link.border = trade_df$link_color,
             directional = 1,
             diffHeight = mm_h(3),
             direction.type = "arrows",
             link.arr.length = 0.1,
             annotationTrack = c("name", "grid"), # 显示扇区名和网格
             preAllocateTracks = list(track.height = 0.1)) # 为标注预留轨道
circos.clear()

这张图一眼就能看出哪个国家是净出口国(发出的红色连接多且宽),哪个是净进口国(收到的蓝色连接多且宽),贸易关系一目了然。

5.2 案例二:基因调控网络的高亮与筛选

在生物信息学中,我们常处理成千上万的基因互作关系。全画出来是一团乱麻,我们需要高亮核心网络或满足特定条件(如p值<0.01)的连接。

# 模拟基因表达相关性矩阵(对称矩阵)
set.seed(789)
genes <- paste0("Gene_", 1:20)
cor_mat <- matrix(runif(400, -1, 1), 20, 20)
diag(cor_mat) <- 1
rownames(cor_mat) <- colnames(cor_mat) <- genes

# 我们只关心强相关(绝对值>0.7)且显著(这里用随机p值模拟)的关系
pval_mat <- matrix(runif(400, 0, 0.1), 20, 20) # 模拟p值矩阵
sig_mask <- pval_mat < 0.05 & abs(cor_mat) > 0.7

# 创建颜色映射函数:红色正相关,蓝色负相关
col_fun <- colorRamp2(c(-1, 0, 1), c("#313695", "#FFFFE0", "#A50026"), transparency = 0.6)

# 技巧:将不显著或弱相关的值设为NA,chordDiagram会自动忽略它们
plot_mat <- cor_mat
plot_mat[!sig_mask] <- NA

# 绘制高亮网络
circos.par(gap.after = 5) # 设置小间隔,让圆环更紧凑
chordDiagram(plot_mat,
             col = col_fun,
             symmetric = TRUE, # 对称矩阵只画一半,避免重复
             grid.col = setNames(rand_color(20), genes),
             transparency = 0.3,
             link.sort = TRUE,
             link.decreasing = TRUE,
             annotationTrack = "grid", # 只画网格,不写名字,避免拥挤
             preAllocateTracks = list(track.height = 0.05))

# 可以添加一层外圈,标记我们感兴趣的某个基因模块
highlight_genes <- c("Gene_3", "Gene_8", "Gene_15")
circos.track(ylim = c(0, 1), track.index = 2, bg.border = NA,
             panel.fun = function(x, y) {
               sector.index = CELL_META$sector.index
               if(sector.index %in% highlight_genes) {
                 circos.rect(CELL_META$cell.xlim[1], 0,
                             CELL_META$cell.xlim[2], 1,
                             col = alpha("gold", 0.3), border = NA)
               }
             })
circos.clear()

这张图通过数据筛选和视觉高亮,从海量关系中抽丝剥茧,让核心的基因调控网络清晰地浮现出来。这种“聚焦”思维,在处理复杂网络数据时非常实用。

6. 性能优化与避坑指南

画小图很轻松,但当扇区和连接数成百上千时,你可能会遇到绘图慢、字体重叠、图例臃肿等问题。这里分享几个我积累的实战经验。

首先,关于性能。chordDiagram() 在绘制数百个连接时效率尚可,但如果超过一千,渲染速度会明显下降。对于超大规模网络,更好的策略是先进行网络筛选或聚合。比如,只保留关系权重前10%的连接,或者将相似节点聚类后再绘制。不要试图在一张图上呈现所有信息,那只会得到一张无法解读的“毛球图”。

其次,标签重叠是常见问题。当扇区很多很窄时,标签肯定会挤在一起。有几个解决办法:

  1. 使用 annotationTrack 参数:设置为 c("name", "grid") 是默认显示标签和网格。你可以设置为 "grid" 只显示网格,然后用 circos.text() 手动在少数重要扇区外添加标签,这样更清爽。
  2. 调整标签方向:在 panel.fun 中自定义标签时,可以用 circos.text() 的 facing 参数控制标签朝向(如 "clockwise", "reverse.clockwise", "inside")。
  3. 终极方案:如果扇区实在太多,考虑用分组条形图或热图作为替代,和弦图并非万能。

最后,别忘了 circos.clear()。这是 circlize 包绘图的一个关键习惯。每次调用 chordDiagram() 或其他 circlize 绘图函数后,必须执行 circos.clear() 来重置环形布局参数。否则,所有后续的绘图都会叠加在同一个圆环上,导致一片混乱。我早期就因为这个疏忽,调试了半天找不到原因。

画图是一个不断迭代和审美的过程。我的建议是,先从简单的数据和默认参数开始,画出第一张图。然后,像装修房子一样,一步步添加颜色、调整布局、高亮重点。多尝试不同的配色方案(RColorBrewer、viridis 等包提供了很多科学配色),多思考你想通过这张图向观众传达的最核心的一两个信息是什么,所有的视觉元素都应该为这个核心信息服务。

circlize 的和弦图功能就像一把精密的瑞士军刀,刚开始可能觉得工具太多无从下手,但一旦熟悉了每项功能,你就能游刃有余地将复杂的数据关系转化为清晰、有力、甚至具有艺术美感的视觉故事。希望这些实战技巧能帮你少走弯路,直接画出让人眼前一亮的高分图表。

更多推荐