本文基于d3.js中的力导向图对关系网络进行可视化。针对实体之间多关系亦即节点之间多条连接线的问题,采用弧形连接线,同时对节点间的多条连接线进行动态编号,并根据编号绘制不同半径的弧线,从而解决多条弧形连接线相互遮挡的问题。同时基于svg中的path标签属性,对弧形方向进行调整,保证多条连接线在节点之间的分布具有对称效果。
最终效果如下(a、b节点之间6条连接线,四条a->b,两条b->a):
整体演示代码如下:
- <!DOCTYPE html>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>test4</title>
- <style>
- path.link {
- fill: none;
- stroke: #666;
- stroke-width: 1.5px;
- }
- marker#licensing {
- fill: green;
- }
- path.link.licensing {
- stroke: green;
- }
- path.link.resolved {
- stroke-dasharray: 0,2 1;
- }
- circle {
- fill: #ccc;
- stroke: #333;
- stroke-width: 1.5px;
- }
- text {
- font: 10px sans-serif;
- pointer-events: none;
- }
- text.shadow {
- stroke: #fff;
- stroke-width: 3px;
- stroke-opacity: .8;
- }
- </style>
- </head>
- <body>
- <div id="chart"></div>
- <script src="http://code.jquery.com/jquery-latest.js"></script>
- <script src="http://d3js.org/d3.v3.min.js"></script>
- <script type="text/javascript">
- var index = 0;
- var width = 1140,
- height = 1000;
- var links = [
- // {source: "Microsoft", target: "Amazon", type: "suit"},
- // {source: "Microsoft", target: "Amazon", type: "suit"},
- // {source: "Microsoft", target: "Amazon", type: "suit"},
- // {source: "Samsung", target: "Apple", type: "suit"},
- // {source: "Samsung", target: "Samsung", type: "suit"},
- {source: "a", target: "b", type: "suit"},
- {source: "a", target: "b", type: "suit"},
- {source: "a", target: "b", type: "suit"},
- {source: "a", target: "b", type: "suit"},
- {source: "a", target: "a", type: "suit"},
- {source: "a", target: "a", type: "suit"},
- {source: "b", target: "a", type: "suit"},
- {source: "b", target: "a", type: "suit"},
- // {source: "Amazon", target: "Microsoft", type: "suit"}
- ];
- //关系分组
- var linkGroup = {};
- //对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
- var linkmap = {}
- for(var i=0; i<links.length; i++){
- var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
- if(!linkmap.hasOwnProperty(key)){
- linkmap[key] = 0;
- }
- linkmap[key]+=1;
- if(!linkGroup.hasOwnProperty(key)){
- linkGroup[key]=[];
- }
- linkGroup[key].push(links[i]);
- }
- //为每一条连接线分配size属性,同时对每一组连接线进行编号
- for(var i=0; i<links.length; i++){
- var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
- links[i].size = linkmap[key];
- //同一组的关系进行编号
- var group = linkGroup[key];
- var keyPair = key.split(':');
- var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体
- if(keyPair[0]==keyPair[1]){
- type = 'self';
- }
- //给节点分配编号
- setLinkNumber(group,type);
- }
- console.log(links);
- var nodes = {};
- // Compute the distinct nodes from the links.
- links.forEach(function(link) {
- link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
- link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
- });
- var force = d3.layout.force()
- .nodes(d3.values(nodes))
- .links(links)
- .size([width, height])
- .linkDistance(600)
- .charge(-300)
- .on("tick", tick)
- .start();
- var svg = d3.select("body").append("svg:svg")
- .attr("width", width)
- .attr("height", height);
- // Per-type markers, as they don't inherit styles.
- svg.append("svg:defs").selectAll("marker")
- .data(["suit", "licensing", "resolved"])
- .enter().append("svg:marker")
- .attr("id", String)
- .attr("viewBox", "0 -5 10 10")
- .attr("refX", 30)
- .attr("refY", 0)
- .attr("markerWidth", 6)
- .attr("markerHeight", 6)
- .attr("orient", "auto")
- .append("svg:path")
- .attr("d", "M0,-5L10,0L0,5");
- var path = svg.append("svg:g").selectAll("path")
- .data(force.links())
- .enter().append("svg:path")
- .attr("class", function(d) { return "link " + d.type; })
- .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
- var circle = svg.append("svg:g").selectAll("circle")
- .data(force.nodes())
- .enter().append("svg:circle")
- .attr("r", 20)
- .call(force.drag);
- var text = svg.append("svg:g").selectAll("g")
- .data(force.nodes())
- .enter().append("svg:g");
- // A copy of the text with a thick white stroke for legibility.
- text.append("svg:text")
- .attr("x", 8)
- .attr("y", ".31em")
- .attr("class", "shadow")
- .text(function(d) { return d.name; });
- text.append("svg:text")
- .attr("x", 8)
- .attr("y", ".31em")
- .text(function(d) { return d.name; });
- // Use elliptical arc path segments to doubly-encode directionality.
- function tick() {
- path.attr("d", function(d) {
- //如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
- if(d.target==d.source){
- dr = 30/d.linknum;
- return"M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y+1);
- }else if(d.size%2!=0 && d.linknum==1){//如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
- return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y;
- }
- //根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
- //注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
- var curve=1.5;
- var homogeneous=1.2;
- var dx = d.target.x - d.source.x,
- dy = d.target.y - d.source.y,
- dr = Math.sqrt(dx*dx+dy*dy)*(d.linknum+homogeneous)/(curve*homogeneous);
- //当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
- if(d.linknum<0){
- dr = Math.sqrt(dx*dx+dy*dy)*(-1*d.linknum+homogeneous)/(curve*homogeneous);
- return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,0 " + d.target.x + "," + d.target.y;
- }
- return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
- });
- circle.attr("transform", function(d) {
- return "translate(" + d.x + "," + d.y + ")";
- });
- text.attr("transform", function(d) {
- return "translate(" + d.x + "," + d.y + ")";
- });
- }
- function setLinkNumber(group,type){
- if(group.length==0) return;
- //对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
- var linksA = [], linksB = [];
- for(var i = 0;i<group.length;i++){
- var link = group[i];
- if(link.source < link.target){
- linksA.push(link);
- }else{
- linksB.push(link);
- }
- }
- //确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
- //特殊情况:当关系都是连接到同一个实体时,不平分
- var maxLinkNumber = 0;
- if(type=='self'){
- maxLinkNumber = group.length;
- }else{
- maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;
- }
- //如果两个方向的关系数量一样多,直接分别设置编号即可
- if(linksA.length==linksB.length){
- var startLinkNumber = 1;
- for(var i=0;i<linksA.length;i++){
- linksA[i].linknum = startLinkNumber++;
- }
- startLinkNumber = 1;
- for(var i=0;i<linksB.length;i++){
- linksB[i].linknum = startLinkNumber++;
- }
- }else{//当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
- //如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
- var biggerLinks,smallerLinks;
- if(linksA.length>linksB.length){
- biggerLinks = linksA;
- smallerLinks = linksB;
- }else{
- biggerLinks = linksB;
- smallerLinks = linksA;
- }
- var startLinkNumber = maxLinkNumber;
- for(var i=0;i<smallerLinks.length;i++){
- smallerLinks[i].linknum = startLinkNumber--;
- }
- var tmpNumber = startLinkNumber;
- startLinkNumber = 1;
- var p = 0;
- while(startLinkNumber<=maxLinkNumber){
- biggerLinks[p++].linknum = startLinkNumber++;
- }
- //开始负编号
- startLinkNumber = 0-tmpNumber;
- for(var i=p;i<biggerLinks.length;i++){
- biggerLinks[i].linknum = startLinkNumber++;
- }
- }
- }
- </script>
- </body>
- </html>
奇数条连接线效果(带有直线):
说明:本文专注于实现多连接线和对称效果,其他可视化效果可自行修改设置。
参考资源:
1、https://stackoverflow.com/questions/37417459/drawing-multiple-links-between-fixed-nodes