手机版

d3.js力导布局绘制资源拓扑图示例教程

时间:2021-08-27 来源:互联网 编辑:宝哥软件园 浏览:

前言

最近公司的业务服务总有bug,各行各业的老板都盯着链接图找问题。有一天老板丢了一张图片过来,“我们做个资源拓扑图,方便大家找bug”。

这是照片。它应该属于马爸爸的房子

好吧,让我们仔细看看这个需求。一圈资源在中心包围着一个应用,中间用曲线连接,应用和资源之间的调用信息记录在曲线的中间。Emmm,这看起来像是女神在遛一群舔狗.哦不,是d3.js力导向图!

D3.js力导向图

D3.js是著名的数据可视化基础工具,提供了将数据映射到网页元素的基本能力,并封装了大量实用的数据操作功能和图形算法。Force-Directed Graph是d3.js提供的一种非常经典的绘图算法,通过在二维空间中排列节点和连线,在各种力的作用下,节点相互碰撞移动,在这个过程中能量不断减少,最终达到能量非常低的稳定状态,形成稳定的Force-Directed Graph。

在d3.js力导引图中,默认提供五种力(以最新的5.x为准):

定心力(定心)

中心力作用在所有节点上,而不是一些单独的节点上,可以将所有节点的中心一致地移动到指定的位置,这种移动不会改变速度或影响节点之间的相对位置。

碰撞力(碰撞)

碰撞力将每个节点视为一个具有一定半径的圆。这个力会防止代表节点的圆相互重叠,即两个节点相互碰撞。您可以通过设置强度来设置该碰撞力的强度。

弹簧力(连杆)

当两个节点通过设置连杆连接在一起时,可以设置一个弹簧力,它会根据两个节点之间的距离将两个节点拉得更近或更远。力的大小与距离成正比,就像弹簧一样。

多体电荷力(多体)

通过设置强度来模拟所有节点之间的相互作用力。如果是正的,节点会相互吸引,可以用来模拟电荷吸引力。如果是负的,节点会互相排斥。力还与节点之间的距离有关。

定位力(定位)

这个力可以将节点沿着指定的维度推到指定的位置。比如通过设置forceX和forceY,可以在x轴和y轴方向推或拉所有节点,forceRadial可以组成一个环,将所有节点推到环上对应的位置。

回到这个要求,其实应用、所有资源、调用信息都可以看作节点,资源与调用信息之间是通过一个弱弹簧力连接的。同时,如果应用程序和资源之间的调用来来去去,那么在这两个调用信息之间会添加一个强大的弹簧力。

好吧,就这么做。

//所有代码均基于typescript,部分代码省略,type inode=D3 . simulationnodedata { id : stringlabel 3360 string;isAppNode?布尔型;};类型ILink=d3。SimulationLinkDatumINode { strength : }数字;};const node 3360 INode[]=[.];const links: ILink[]=[.];const container=D3 . select(' container ');const svg=container.select('svg ')。attr('width ',width)。attr('高度',高度);const html=container . append(' div ')。attr('class ',样式。html container);//创建一个弹簧力,根据链接的强度值确定强度。const link force=D3 . forcelinkinode,ilink(链接)。id(节点=节点。id)//资源节点和信息节点之间的强度较小,而信息节点之间的强度较大。const simulation=D3 . force simulation inode,ilink(节点)。力(' link ',link force)//在y轴方向施加一个力,使整个图形稍微变平。力(' yt ',D3。Forcey()。强度(()=0.025))。力(' Yb ',D3。力量(高度)。强度(()=0.025))//节点间互斥的电磁力。力(电荷),D3。forcemanybodyinode()。强度(-400))//避免节点互相覆盖。力('碰撞',d3。forcecollide()。半径(d=4))。力('中心',d3。forcecenter (width/2,height/2))//手动调用tick使布局稳定为(让I=0,n=math . ceil(math . log(simulation . alpha min())/math . log(1-simulation . alpha deck())));I n;I){ simulation . tick();} const NodeElements=SVG . append(' g ')。选择全部(“圆”)。数据(节点)。输入()。追加('圆圈')。attr('r ',10)。attr('fill ',GetNodeColor);const labelements=SVG . append(' g ')。选择全部(“文本”)。数据(节点)。输入()。追加('文本')。文本(节点=node.label)。attr('font-size ',15);const PathElements=SVG . append(' g ')。选择全部(“行”)。数据(链接)。输入()。追加('行')。attr('笔画宽度',1)。attr('stroke ',' # E5E 5 ');const render=()={ nodelements。attr('cx ',node=node.x!) .attr('cy ',node=node.y!);标签元素。attr('x ',node=node.x!) .attr('y ',node=node.y!);pathElements。attr('x1 ',link=link.source.x)。attr('y1 ',link=link.source.y)。attr('x2 ',link=link.target.x)。attr('y2 ',link=link . target . y);} render();效果如下:

Ok已经基本实现了,就这样。后台学生实现界面后可以上线。日均紫外线两位数的产品需要什么样的自行车,有的还不错(手动两哈)。

当然不是。有一个城市传说,中国和台湾的产品是否好用,跟周转率有关。需要打开资源拓扑图是一件很棒的事情。再看到这样体验差的产品,感觉几分钟就要走了。为了促成我公司年营业额2万亿元的长远目标,我们来看看还有哪些需要改进的地方。

至少给我中间的话

请注意,我们的单词位于左下角节点的中心,因为我们使用了svg的文本元素。默认情况下,为文本元素设置的X和Y表示文本元素基线的起始位置。当然,我们可以直接设置dx和dy,并设置一个偏移量来完成对中问题。但是考虑到svg元素与普通html元素相比还是有局限性的,不方便以后扩展,所以我们简单的把所有的点和字都改成html元素。

.const NodeElements=html . append(' div ')。选择全部(“div”)。data(nodes . filter(node=node . isappnode))。输入()。追加(' div') //css模块。attr('class ',样式。NodeItem)。html((node : INode)={ return ` p $ { node . id }/p `;});const labelements=html . append(' div ')。选择全部(“div”)。data(nodes.filter(node=!node.isAppNode))。输入()。追加(' div') //css模块。attr('class ',样式。标签诉讼)。html(node=` p $ { node . label }/p Pavada Kedavra!/p `);const render=()={ nodelements。attr('style ',(node)={ return ` transform : translate 3d(calc($ { node . x } px-50%),calc(${node.y}px - 50%),0);`;});标签元素。attr('style ',(node)={ return ` transform : translate 3d(calc($ { node . x } px-50%),calc(${node.y}px - 50%),0);`;});}效果如下:

文字居中!

为什么这条线像激光?这一点也不像遛狗和舔狗

我们再来看一下这条线。一开始我们把所有代表弹簧力的线段都当成直线画出来,但是看起来比较生硬,效果不佳。其实我们需要的是一条连接资源节点和应用节点同时经过信息节点的自然曲线,那么问题就变成了如何通过三点画出一条曲线。

绘制曲线,自然要用到svg的路径元素及其D绘制指令。这里有非常详细的关于如何用path和MDN绘制曲线的教程。在实际工程应用中,一般来说,很难控制贝塞尔曲线并获得更好的效果,所以我们使用A命令来绘制这条弧。

要使用A命令绘制圆弧,需要知道以下元素:X轴半径、Y轴半径、圆弧旋转角度、角度大小标志、圆弧方向标志和圆弧终点。三点坐标已知的情况下如何找到这些元素?是时候复习一波三角函数了。

给定a,b,c (xaya,xbyb,xcyc)的坐标,可以得到a,b,c (Math.sqrt((x1-x2)2-(y1-y2)2))的长度,然后根据余弦定理得到C,再根据正弦定理得到r。有关详细信息,请参见代码:

键入IVisualLink={ id: stringstart : number[];middle:号[];end:数字[];arcPath:字符串;hasReverseVisualLink:布尔值;};const visuallinks 3360 ivisualink[]=[.];函数dist(a:数[],b:数[]){ return math . sqrt(math . pow(a[0]-b[0],2) Math.pow(a[1] - b[1],2));}.const PathElements=SVG . append(' g ')。选择全部(“路径”)。数据(可视化链接)。输入()。追加('路径')。attr('填充','无')。attr('笔画宽度',1)。attr('stroke ',' # E5E 5 ');const render=()={ 0.nodes //过滤掉所有信息节点。筛选器(节点=!foreach((节点)={ 0.//根据信息节点的信息,得到对应的visualLink对象index const idx=find visualLink index(节点)visualLink[idx]。start=[source.x!来源. y!];visualLinks[idx]。middle=[node.x!node.y!];visualLinks[idx]。end=[target.x!target.y!];const A=visualLinks[idx]。开始;const B=visualLinks[idx]。结束;const C=visualLinks[idx]。中间;常数a=距离(B,C);常数b=距离(C,A);const c=dist(A,B);//用余弦定理求c const angle=math . acos((a * ab * B- c * c)/(2 * a * b));//外接圆的半径为const r=_。圆(c/数学。sin(角度)/2,4);//角度大小标志,因为我们要的是一个圆弧而不是一个破圆,所以它是常量为0 const laf=0//圆弧方向标志,根据AB的斜率,判断AB线c是哪边,然后确定圆弧方向const saf=((B[0]-A[0])*(c[1]-A[1])-(B[1]-A[1])*(c[0]-A[const arcPath=[' M ',A,' A ',r,r,0,laf,SAF,B]。join(' ');visualLinks[idx]。arcPath=arcPath});pathElements。attr('d ',(link)={ return link . arcpath;});}效果如下:

这些线没有一对A,所以你分不清利弊

应用程序和资源之间的关系是有方向性的。在大多数情况下,应用程序调用资源,也有双向调用的情况。除了意想不到的词,我们还需要添加箭头来显示谁在呼叫谁。这个箭头怎么加?svg的Path元素有一个标记结束属性。通过设置该属性,可以在路径元素的最后一个向量上绘制svg元素。

//添加标记元素svg标记id=' arrow' viewbox='-10-10 20 20 '标记宽度=' 20 '标记高度=' 20 '方向=' auto '路径d=' m-6.75,-6.75l0,0 L -6.75,6.75 '填充='none '描边=' # e5e 5 '/标记/svg.const PathElements=SVG . append(' g ')。选择全部(“路径”)。数据(可视化链接)。输入()。追加('路径')。attr('fill ',' none') //设置标记结束属性。attr('标记结束',' URL (#箭头')。attr ('id ',link=link.id)。attr('笔画宽度',1).但是如果直接写,效果会很差。为什么呢?因为我们的path元素的起点和终点是节点的中心点,所以箭头都在节点的正上方,如图所示:

看到中间那朵菊花了吗

因此,我们不能通过添加这个属性来直接添加箭头。我们需要对路径做一些处理,去掉路径线段的头尾。那怎么做呢?幸运的是,一个巨人家伙实现了一个算法来计算两个路径元素之间的交点,所以我们可以在计算完原始arcPath之后,计算出这个弧和节点外更大的圆之间的交点,然后将原始arcPath的起点和终点移动到这两个点。

从“路径-交集”导入交集;const render=()={ 0.nodes //过滤掉所有信息节点。筛选器(节点=!foreach((节点)={ 0.//根据信息节点的信息,得到对应的visualLink对象index const idx=find visualLink index(节点)visualLink[idx]。start=[source.x!来源. y!];visualLinks[idx]。middle=[node.x!节点. y!];visualLinks[idx]。end=[target.x!target.y!];const A=visualLinks[idx]。开始;const B=visualLinks[idx]。结束;const C=visualLinks[idx]。中间;常数a=距离(B,C);常数b=距离(C,A);const c=dist(A,B);//用余弦定理求c const angle=math . acos((a * ab * B- c * c)/(2 * a * b));//外接圆的半径为const r=_。圆(c/数学。sin(角度)/2,4);//角度大小标志,因为我们要的是一个圆弧而不是一个破圆,所以它是常量为0 const laf=0//圆弧方向标志,根据AB的斜率,判断AB线c是哪边,然后确定圆弧方向const saf=((B[0]-A[0])*(c[1]-A[1])-(B[1]-A[1])*(c[0]-A[const origarpath=[' M ',A,' A ',r,r,0,laf,SAF,B]。join(' ');const raidus=NODE _ RADIUSconst startCirclePath=[ 'M ',A,' M ',[-raidus,0],' A ',raidus,raidus,0,1,0,[raidus * 2,0],' A ',raidus,raidus,0,1,0,[-raidus * 2,0],]。join(' ');const endCirclePath=[ 'M ',B,' M ',[-raidus,0],' a ',raidus,raidus,0,1,0,[raidus * 2,0],' a ',raidus,raidus,0,1,0,[-raidus * 2,0],]。join(' ');const start intersection=intersect(origarpath,startcircle path)[0];const endIntersection=intersect(origarpath,endCirclePath)[0];const arcPath=[ 'M ',[startIntersection.x,startIntersection.y],' A ',r,r,0,laf,saf,[endIntersection.x,endIntersection.y],]。join(' ');visualLinks[idx]。arcPath=arcPath});pathElements。attr('d ',(link)={ return link . arcpath;});}

效果已经很接近了!

字叠在一起,臣妾看不清楚

到了这一步,整体效果差不多了,但是怎么才能停止追求完美呢?仔细看看这个图,因为调用信息是一个方盒子,不是原型节点。如果应用程序和资源来来去去,这些词可以很容易地堆叠在一起。您可以尝试调整碰撞力和链接,使它们不重叠,但通过调整这两个系数很容易使整个画面变得混乱。那我们该怎么办?我们就此打住吗?换个说法吧。如果应用程序和资源之间有起伏,这个连接信息不会放在中间点,而是放在第一个三分之一处。

那很好。我怎么知道前三分之一在哪里?

幸运的是,我们的前辈帮助我们探索了这种“复杂”的数学问题。svg标准定义了两种方法,SVG几何分层。gettotallength和svggeometriyelement。getpointatlength。通过这两种方法,可以得到路径的全长和某个长度点的位置。但是这两个方法都是附加在DOM元素上的,直接调用有点麻烦。幸运的是,有PureJS的实现:

从“svg-path-properties”导入{ svgPathProperties };render=()={ 0.标签元素。attr('style ',(link)={ const properties=svgPathProperties(link . arcpath);const TotaLength=properties . getTotalLength();const point=properties . getpointatlength(link . hasreviewsualink?合计长度/3 :合计长度/2,返回` transform : translate 3d(calc($ { point . x } px-50%),calc(${point.y}px - 50%),0);`;});}最终效果:

我还是有点矮

效果几乎一样,但还是有一些不完善的地方

当数据不同时,各种力的系数不能通用,必须根据不同的数据试出一个相对通用的系数函数。不可能保证所有节点都在盒子里并且不重叠。这两个问题可以看作是力诱导布局的固有缺陷。也许该图的实现与力诱导布局毫无关系。但是使用力导布局可以达到很好的效果,这个边缘情况可以慢慢解决。

摘要

以上就是本文的全部内容。希望本文的内容对大家的学习或工作有一定的参考价值。有问题可以留言交流。谢谢你的支持。

版权声明:d3.js力导布局绘制资源拓扑图示例教程是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。