背景
桑基图(Sankey diagram),即桑基能量分流图,也叫桑基能量平衡图。它是一种特定类型的流程图,右图中延伸的分支的宽度对应数据流量的大小,通常应用于能源、材料成分、金融等数据的可视化分析。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。
桑基图我们上百度一查都能查到很多好看的示例,比如:
我今天要做的可比这些看起来轻松简单许多了
来自于这篇文献中的Fig 2(c)
Spread of airborne antibiotic resistance from animal farms to the environment: Dispersal pattern and exposure risk
https://doi.org/10.1016/j.envint.2021.106927
乍一看很粗糙哈,没有花里胡哨的颜色,没有天花乱坠的连接一切都是那么简单和朴素,文献中关于这部分信息也有了简单的描述,这里使用了networkD3 R包进行绘制,那我们就尝试一下
上官网教程网址 Sankey plot | the R Graph Gallery (r-graph-gallery.com),官网中非常人性的提供了逐步进行绘制的代码,甚至提供了多种绘制模式可供选择
就在我按照文献中给的方法进行复刻的时候,我遇到了难度,我发现无论我如何按照代码的形式进行绘图绘制的结果一定都像官方网址中第一个图所示的一样,所有的连线(灰色的宽带)从流出端都是占满了整个条带,流入端占比加起来也为100%,这就与文献中的图形展示形式不符了,思考人生……
我们还是看看基础的桑基图作图过程是什么样的吧,官网中提供了两种可选择的构建links数据的方式
第一种
# Library
library(networkD3)
library(dplyr)
# A connection data frame is a list of flows with intensity for each flow
links <- data.frame(
source=c("group_A","group_A", "group_B", "group_C", "group_C", "group_E"),
target=c("group_C","group_D", "group_E", "group_F", "group_G", "group_H"),
value=c(2,3, 2, 3, 1, 3)
)
# From these flows we need to create a node data frame: it lists every entities involved in the flow
nodes <- data.frame(
name=c(as.character(links$source),
as.character(links$target)) %>% unique()
)
# With networkD3, connection must be provided using id, not using real name like in the links dataframe.. So we need to reformat it.
links$IDsource <- match(links$source, nodes$name)-1
links$IDtarget <- match(links$target, nodes$name)-1
# Make the Network
p <- sankeyNetwork(Links = links, Nodes = nodes,
Source = "IDsource", Target = "IDtarget",
Value = "value", NodeID = "name",
sinksRight=FALSE)
p
saveNetwork(p,"sankey.html")
# save the widget
# library(htmlwidgets)
# saveWidget(p, file=paste0( getwd(), "/HtmlWidget/sankeyBasic1.html"))
总共由4部分组成,第一部分就是构建links数据框,第二部分获得桑集图中所有nodes节点信息,第三部分获得节点间的连接关系,第四部分就是作图了
第二种
# Library
library(networkD3)
library(dplyr)
# Create an incidence matrix. Usually the flow goes from the row names to the column names.
# Remember that our connection are directed since we are working with a flow.
set.seed(1)
data <- matrix(sample( seq(0,40), 49, replace=T ), 7, 7)
data[data < 35] <- 0
colnames(data) = rownames(data) = c("group_A", "group_B", "group_C", "group_D", "group_E", "group_F", "group_G")
# Transform it to connection data frame with tidyr from the tidyverse:
links <- data %>%
as.data.frame() %>%
rownames_to_column(var="source") %>%
gather(key="target", value="value", -1) %>%
filter(value != 0)
# From these flows we need to create a node data frame: it lists every entities involved in the flow
nodes <- data.frame(
name=c(as.character(links$source), as.character(links$target)) %>%
unique()
)
# With networkD3, connection must be provided using id, not using real name like in the links dataframe.. So we need to reformat it.
links$IDsource <- match(links$source, nodes$name)-1
links$IDtarget <- match(links$target, nodes$name)-1
# Make the Network
p <- sankeyNetwork(Links = links, Nodes = nodes,
Source = "IDsource", Target = "IDtarget",
Value = "value", NodeID = "name",
sinksRight=FALSE)
p
# save the widget
# library(htmlwidgets)
# saveWidget(p, file=paste0( getwd(), "/HtmlWidget/sankeyBasic2.html"))
和第一种差不多,这里的数据框使用sample指令随机生成的
比如我现在要模仿绘制Fig 2(c),我们先来看下他的组成情况
由四部分组成,我们不妨从左到右将他们依次命名为ABCD,可以看到A连着B,B连着C,B连着D,C连着D,通过官网教程的第一种示例,我们可以得知,这一步是进行手动构建links的,value值对应着被连接的部分与连接部分共同的大小
links <- data.frame(
source=c("group_A","group_A", "group_B", "group_C", "group_C", "group_E"),
target=c("group_C","group_D", "group_E", "group_F", "group_G", "group_H"),
value=c(2,3, 2, 3, 1, 3)
)
根据这一原则,我们假设A共有6份,B共有7份,C共有11份,D共有8份(根据figure2c 大致假设),其中AB共有3,BC共有7,DB共有3 CD没有共有
那我么这里的links可以这样写
links <- data.frame(
source=c("A","B", "B"),
target=c("B","C", "D""),
value=c(3,7, 3)
)
执行代码后此时的links是这样的
links
source target value
1 A B 3
2 B C 7
3 B D 3
进行图形绘制后结果变为
可以看到这样的数据输入并不能确定A组包含的份数,而且由于B与C,B与D连接,脚本看来他们就应该放在一列,这这这这这这这这这与文献中差的可有点远了,尝试从其他的R包入手画这张图都没有实现作者的原图,而且作者就是用的networkD3这个包画的,难道中国人骗中国人不成?
所以我这里面临的主要问题就是每一个连接块的数量占比问题,以及如何让D图排到第四列的位置而不是在C的下面
既然第一个连接块无法进行份数设置,而且它这里还延伸一半出来,那有没有可能存在着一条隐形的连接线和连接块呢?
我在这里假设存在了一个连接块E,并且让它与C相连,然后再将C与D相连,这样就解决了CD排列在一列的问题并且可以有效的为A连接块与C连接块流出部分空余的问题,我干嘛假设呀,我直接再连一个E出来,说连就连,这里大致计算了一下连接之间的份数,然后按照以下的数量构建links
links <- data.frame(
source=c("A","A","B","C","C","B"),
target=c("B","C","C","D","E","D"),
value=c(15,15,20,15,20,10)
)
画图
哎~ 差不多了,感觉很近了,现在其实就是多了AC之间的连接,CD之间的连接(你要问我为什么要连CD,不连他俩不能把CD并列放置),CE之间的连接,BE之间的连接
那这个连接该怎么删除呢?
有没有发现第一种方法里面我最后写了saveNetwork(p,"sankey.html") 这句代码,没想到吧,其实这个桑基图在网页上是可以进行拉动的,是交互的,所以我们这里把它保存为网页的格式,接下来呢,接下来就简单了,用网页的F12调试台删除掉多余的部分
上菜
是不是有点那个意思了?唯一的缺点就是每次都得好好算一算比例组成,烦恼