d3重力布局——节点间多关系处理
相关的知识:
path相关的知识
<path>标签是d3里面功能最为丰富的标签,很多图形用该标签可以制作出来。它通过一系列的坐标点来绘制。在d3的图形绘制中。经常会用到。
用法:给出一个坐标点,在坐标点前面添加一个英文字母,表示如何运动到此坐标点。
英文字母的按照功能可分为5类:
- 移动类
- M = moveto:将画笔移动到指定坐标
- 直线类
- L = lineto:画直线到指定坐标。
- H = horizontal lineto:画水平线到指定坐标。
- V = vertical lineto:画垂直线到指定坐标。
- 曲线类
- C = curveto:画三次贝塞尔曲线经两个指定控制点到达最终坐标。
- S = shorthand/smooth curveto:与前一条三次贝塞尔曲线,第一个控制点为前一条曲线第二个控制点的对称点,只需要输入第二个控制点和终点,即可绘制一个三次贝塞尔曲线
- Q = quadratic Bezier curveto:画一个二次贝塞尔曲线经过一个指定控制点到达终点坐标
- T = Shorthand/smooth quadratic Bezier curveto:与前一条贝塞尔曲线相连,控制点为前一条二次贝塞尔曲线控制点的对称点。只需输入终点,即可绘制一个二次贝塞尔曲线。
- 弧线类
- A = elliptical arc:画椭圆曲线到指定坐标
- 闭合类
- Z = closepath:绘制一条直线,连接终点和起点,用来封闭图形。
到这里,我们知道了绘制一个<path>所要知道的基本的英文字母的基本意思
问题解析
然后就开始处理我们的核心问题:节点间的多关系
在实际应用场景中,人与人之间的关系不仅仅是单向,更多的是多项的,例如:小明 — 朋友 — 小红 ,小明 — 同学 — 小红 ;或者 小红— 朋友 — 小明 ,小红 — 同学 — 小明。
如果在d3的重力导向布局中如果不对这种节点间的关系做处理,很多关系会叠加载一块,影响美观与观感。

在现有的数据可视化框架中也有相关案例,例如echarts。你只需要设置lineStyle的属性curveness的值就可以使得线条呈现一个弧度。但是由于项目的特殊性,很多情况下需要我们用d3来完成自由度更高,并且一些个定制化的服务。

因此,在d3关于节点间的多关系处理中,曲线连接是一种比较好的方案。因此解决问题的核心在于如何绘制曲线,并且保证两点之间的多条线不会覆盖?在多条线弯曲下,如何平均半圆弧弯曲避免全跑到某半圆弧上?定义曲线弧方向?
首先统计下两点之间的线条数,再将这些连接线分配到一个 map 里,两个节点的 name 字段进行拼接做成 key,这样计算得到两点之间的连接线总数。
然后在遍历时同 map 的线根据方向分成正向、反向两组,正向组遍历给每条线追加设置一个 linknum 编号,同理,反向组遍历追加一个 -linknum 编号值。这个正向、反向判断方法很多,根据节点 source.name、target.name 进行比较,这里其实是比较 ASCII 码。而我们设定的 linknum 值就是来确定该条弧线的弯曲度和弯曲方向的,这里搭配下面代码讲解比较好理解:
关系处理
//在vue的methods的方法中
dealRealtions(links) {
var linkGroup = {}; //用来分组,将两点之间的连线进行归类
var linkMap = {}; //对连接线的计数
for (var i = 0; i < links.length; i++) {
var key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
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.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
links[i].size = linkMap[key];
//同一组的关系进行编号
var group = linkGroup[key];
var keyPair = key.split(':');
var type = 'noself';
if (keyPair[0] == keyPair[1]) { //指向两个不同实体还是同一个实体
type = 'self';
}
this.setLinkNumber(group, type); //给关系编号
}
},
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.name < link.target.name){
linksA.push(link);
}else{
linksB.push(link);
}
}
console.log(linksA,linksB)
//确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
//特殊情况:当关系都是连接到同一个实体时,不平分
var maxLinkNumber = 1;
if(type=='self'){
maxLinkNumber = group.length;
}else{
maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;
}
console.log(maxLinkNumber)
//如果两个方向的关系数量一样多,直接分别设置编号即可
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++;
}
}
},
然后在d3的tick方法中进行样式配置
function tick () {
path.attr('d', function(d) { //连接线
var linePadding = 0; //给连线到节点间的距离
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist;
var sourceX = d.source.x + (linePadding * normX),
sourceY = d.source.y + (linePadding * normY),
targetX = d.target.x - (linePadding * normX),
targetY = d.target.y - (linePadding * normY);
if (d.target == d.source) {
dr = 38/d.linknum;
return"M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 1,1 " + targetX + "," + (targetY+1);
}else if (d.size%2!=0&&d.linknum==1) {
return 'M'+ sourceX +' '+sourceY+' L '+ targetX +' '+targetY;
}
var curve =1.5;
var homogeneous=1.2;
var dr = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * (d.linknum + homogeneous) / (curve * homogeneous);
//当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
if(d.linknum<0){
dr = Math.sqrt(deltaX*deltaX+deltaY*deltaY)*(-1*d.linknum+homogeneous)/(curve*homogeneous);
return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,0 " + targetX + "," + targetY;
}
return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
});
....
}
实现效果如下

附加
在tick方法中我们设置linePadding,用来配置path到节点的距离,设置的参数大于节点半径的时候,连线的末端会显示在节点外边,但是会出现path的箭头拥堵在一块。如果只是展示单一关系的图,则不需要调整。
否则:需要将参数设置为0,显示在节点底下,通过设置箭头的refX和refY的参数来调整箭头在path的显示位置,一般>=节点的半径。这样就可以简单的将箭头不至于挤在一块,但是这也仅仅只是在节点间的关系比较少的时候能够显得好看点,两个节点关系如果很多,则还是会出现拥挤。
注:参考如下
D3.js 力导向图的显示优化
【D3.js数据可视化系列教程】(三十二)-- 力导向图之弧形箭头连线
d3.js多重力导向图多条关系线,mouseover只显示相关节点
本文介绍了如何在d3.js的重力布局中处理节点间多关系,确保多条线不叠加并保持美观。通过统计两点间的线条数并分配到map中,根据节点名设置方向和编号,利用tick方法配置线条样式,实现曲线连接。同时讨论了linePadding参数对箭头布局的影响,提供了解决箭头拥挤的方法。
9618

被折叠的 条评论
为什么被折叠?



