R 数据可视化 —— circlize 复杂图形与图例

复杂图像

我们可以使用一些简单的图形对象,将它们组合起来,绘制成各种复杂的图形

1. 圆形柱状图

我们介绍过了各种柱状图/条形图的绘制,如堆积型、并列型,还有径向柱状图,现在我们再介绍一种圆形柱状图的绘制。

圆形柱状图就是将圆形布局中,每一条轨迹当做一个柱子。例如,我们有 9 个样本,要绘制每个样本的肿瘤纯度信息

首先,构造数据

category <- paste0("sample", "_", 1:9)
percent <- sort(sample(40:80, 9))
color <- rev(rainbow(length(percent)))

初始化,以 12 点钟方向作为起始,同时只设置一个扇形,即整个圆就是一个 sector

circos.par("start.degree" = 90, cell.padding = c(0, 0, 0, 0))
circos.initialize("a", xlim = c(0, 100))

添加图形

circos.track(
  ylim = c(0.5, length(percent)+0.5), track.height = 0.8, 
  bg.border = NA,
  panel.fun = function(x, y) {
    xlim = CELL_META$xlim
    # 添加圆形中心线
    circos.segments(rep(xlim[1], 9), 1:9,
                    rep(xlim[2], 9), 1:9,
                    col = "#CCCCCC")
    # 添加 9 个圆形矩形
    circos.rect(rep(0, 9),
                1:9 - 0.45,
                percent,
                1:9 + 0.45,
                col = color,
                border = "white")
    # 添加文本信息
    circos.text(
      rep(xlim[1], 9),
      1:9,
      paste(category, " - ", percent, "%"),
      facing = "downward",
      adj = c(1.05, 0.5),
      cex = 0.8
    )
    # 添加轴信息
    breaks = seq(0, 85, by = 5)
    circos.axis(
      h = "top",
      major.at = breaks,
      labels = paste0(breaks, "%"),
      labels.cex = 0.6
    )
  })

2. 直方图

可以使用 circos.trackHist() 函数在所有单元格中绘制直方图,如果设置 draw.density = TRUE 则会绘制数据分布的密度曲线。

默认情况下,每个单元格的柱子的数量会根据数据自动确定,通过固定 bin.size 的值,可以让所有的单元格绘制相同数量的柱子,有利于单元格之间的分布比较。

例如

x <- rnorm(1600)
sectors <- sample(letters[1:16], 1600, replace = TRUE)
circos.initialize(sectors, x = x)
circos.trackHist(
  sectors, x = x, col = "#a6cee3", 
  border = "#1f78b4"
  )
circos.trackHist(
  sectors, x = x, bin.size = 0.1,  
  col = "#b2df8a", border = "#33a02c"
  )
circos.trackHist(
  sectors, x = x, draw.density = TRUE, 
  col = "#fdbf6f", border = "#ff7f00"
  )
circos.clear()

3. 系统发育树

圆形树状图可以有多方面的应用,比如系统发育树的展示。

R 中有多种树结构类,例如 hclust, dendrogramphylo,它们之间可以进行相互转换,所以我们只使用 dendrogram 类来说明

首先,导入 ape 包提供的鸟类物种信息,并对数据进行层次聚类

library(ape)

data(bird.orders)
hc <- as.hclust(bird.orders)

然后,使用 cutree() 将数进行切割,划分出 6 个物种

# 获取鸟类名称
labels <- hc$labels
# 划分为 6 个物种
ct <- cutree(hc, 6)
# 物种的数量
n <- length(labels) 
# 转换为 dendrogram 结构
dend <- as.dendrogram(hc)

因为树形图在内侧,所以要先绘制物种的标签

circos.par(cell.padding = c(0, 0, 0, 0))
# 只需要一个扇形
circos.initialize("a", xlim = c(0, n))
circos.track(
  ylim = c(0, 1), bg.border = NA, track.height = 0.3, 
  panel.fun = function(x, y) {
    for (i in seq_len(n)) {
      circos.text(
        i - 0.5, 0, labels[i], adj = c(0, 0.5),
        facing = "clockwise", niceFacing = TRUE,
        col = ct[labels[i]], cex = 0.5
      )
    }
  })

最后使用 circos.dendrogram() 函数绘制树形图

library(dendextend)

dend <- color_branches(dend, k = 6, col = 1:6)
dend_height <- attr(dend, "height")
circos.track(
  ylim = c(0, dend_height), bg.border = NA, 
  track.height = 0.4, 
  panel.fun = function(x, y) {
    circos.dendrogram(dend)
    }
  )
circos.clear()

可以使用 dendextend 包对树形图的属性进行设置,树形图默认是朝外的,可以设置 facing = "inside",使其朝向内部

circos.dendrogram(dend, facing = "inside")

注意,要先绘制树形图后绘制标签

4. 手动绘制圆形热图

既然我们可以绘制圆形树状图,那么很容易想到,在最外层使用 circos.rect() 函数添加一圈热图,就变成了圆形热图。

我们要绘制两个独立的热图,首先,构造数据

mat <- matrix(rnorm(100*10), nrow = 100, ncol = 10)
col_fun <- colorRamp2(c(-2, 0, 2), c("#fc8d59", "#ffffbf", "#99d594"))
# 设置两个扇形
sectors <- rep(letters[1:2], times = c(30, 70))
mat_list <- list(
  a = mat[sectors == "a", ],
  b = mat[sectors == "b", ]
  )
# 将聚类结果转换为 dendrogram 类
dend_list <- list(
  a = as.dendrogram(hclust(dist(mat_list[["a"]]))),
  b = as.dendrogram(hclust(dist(mat_list[["b"]])))
)

我们需要从外到内依次绘制,先绘制圆形热图

circos.par(cell.padding = c(0, 0, 0, 0), gap.degree = 5)
circos.initialize(sectors, xlim = cbind(c(0, 0), table(sectors)))
circos.track(
  ylim = c(0, 10), bg.border = NA, 
  panel.fun = function(x, y) {
    sector.index = CELL_META$sector.index
    m = mat_list[[sector.index]]
    dend = dend_list[[sector.index]]
    
    m2 = m[order.dendrogram(dend),]
    col_mat = col_fun(m2)
    nr = nrow(m2)
    nc = ncol(m2)
    for (i in 1:nc) {
      circos.rect(
        1:nr - 1, rep(nc - i, nr), 1:nr,
        rep(nc - i + 1, nr), border = col_mat[, i],
        col = col_mat[, i])
    }
})

因为我们需要绘制了两个树状图,要保证它们的高度一致,可以去两个当中更高的那个来设置 ylim

# 获取最大树高
max_height <- max(sapply(dend_list, function(x) attr(x, "height")))
circos.track(
  ylim = c(0, max_height), bg.border = NA, 
  track.height = 0.3, panel.fun = function(x, y) {
    sector.index = get.cell.meta.data("sector.index")
    dend = dend_list[[sector.index]]
    circos.dendrogram(dend, max_height = max_height)
  }
)
circos.clear()

图例

circlize 为用户提供了完全自由的图形设计,但是缺少了对图例的控制。

例如,对于如下图形

col_fun <- colorRamp2(c(-2, 0, 2), c("green", "blue", "red"))
circlize_plot <- function() {
  set.seed(123)
  sectors = letters[1:10]
  circos.initialize(sectors, xlim = c(0, 1))
  circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
    circos.points(runif(20), runif(20), cex = 0.5, pch = 16, col = 2)
    circos.points(runif(20), runif(20), cex = 0.5, pch = 16, col = 3)
  })
  circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
    circos.lines(sort(runif(20)), runif(20), col = 3)
    circos.lines(sort(runif(20)), runif(20), col = 7)
  })
  
  for(i in 1:10) {
    circos.link(sample(sectors, 1), sort(runif(10))[1:2], 
                sample(sectors, 1), sort(runif(10))[1:2],
                col = add_transparency(col_fun(rnorm(1))))
  }
  circos.clear()
}
circlize_plot()

现在要为这三个轨迹添加图例,我们可以使用 ComplexHeatmapLegend() 函数来自定义图例

library(ComplexHeatmap)
# 点
lgd_points <- Legend(
  at = c("label1", "label2"), type = "points", 
  legend_gp = gpar(col = 2:3), title_position = "topleft", 
  title = "Track1"
)
# 线
lgd_lines <- Legend(
  at = c("label3", "label4"), type = "lines", 
  legend_gp = gpar(col = 4:5, lwd = 2), 
  title_position = "topleft", title = "Track2"
)
# 颜色条
lgd_links <- Legend(
  at = c(-2, -1, 0, 1, 2), col_fun = col_fun, 
  title_position = "topleft", title = "Links"
)

要将这三个图例合并在一起,可以使用 packLegend() 函数,默认按竖直方向添加

lgd_list_vertical <- packLegend(lgd_points, lgd_lines, lgd_links)

可以使用 draw() 函数来绘制图例

draw(
  lgd_list_vertical, x = unit(5, "mm"), 
  y = unit(4, "mm"), just = c("left", "bottom")
)

虽然 circlize 是基于基础图形系统,而 ComplexHeatmap 是基于 grid 绘图系统,但是两种系统可以混合使用。事实上,它们是绘制在同一个图形设备上的不同图层

但是这种方法很容易出现图例与圆形布局重叠的情况,更好的方式是将图例和圆形图分为两部分。

我们可以用 grid 包来对绘图区域进行重排,例如

grid.newpage()
circle_size = unit(1, "snpc")
pushViewport(
  viewport(
    x = 0, y = 0.5, width = circle_size, 
    height = circle_size, just = c("left", "center")
    )
  )
par(omi = c(0, 0, 0.5, 0.5), new = TRUE)
circlize_plot()
upViewport()
draw(lgd_list_vertical, x = circle_size, just = "left")

使用 par(new = TRUE) 防止基础图形绘制到新的图片上,并用 omi 参数来设置边距。

也可以设置水平排列的图例

lgd_points <- Legend(
  at = c("label1", "label2"), type = "points", 
  legend_gp = gpar(col = 2:3), title_position = "topleft",
  title = "Track1", nrow = 1
)

lgd_lines <- Legend(
  at = c("label3", "label4"), type = "lines", 
  legend_gp = gpar(col = 4:5, lwd = 2), title_position = "topleft",
  title = "Track2", nrow = 1
)

lgd_links <- Legend(
  at = c(-2, -1, 0, 1, 2), col_fun = col_fun, 
  title_position = "topleft", title = "Links", direction = "horizontal"
  )

lgd_list_horizontal <- packLegend(
  lgd_points, lgd_lines, lgd_links, 
  direction = "horizontal"
)

plot.new()
pushViewport(viewport(
  x = 0.5, y = 1, width = circle_size, 
  height = circle_size, just = c("center", "top"))
  )
par(omi = c(0, 0, 0, 0), new = TRUE)
circlize_plot()
upViewport()

draw(lgd_list_horizontal, y = unit(1, "npc") - circle_size, just = "top")

这种方式也是要调整绘图区域的大小,来显示图例

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容