1、效果图展现及说明javascript
(图一)html
(图二)html5
附注说明:java
1. 图例都是DAG有向无环图的展示效果。两张图的区别为第二张图包含了多个分段关系。放置展现图片效果主要是为了说明该例子支持多段关系的展示(当前也包括单独的节点展示,图例没有展现)node
2.图例中的圆形和曲线均使用的是SVG绘制。以前考虑了三种方式,一种是html5的canvas,一种是原始的html DOM,再有就是SVG。不过canvas对事件的支持不是很好(记得以前看过一篇文章主要是经过计算鼠标定位是否在canvas上的某个区域来触发事件机制,比较不适用工做流节点上的各类事件触发机制),另外原始的 DOM虽然对事件的处理比canvas要方便,可是从编码和绘制dom上则会过分的耗费资源,尤为是曲线的绘制,毕竟过多的dom操做都会影响性能拖慢响应速度,因此综合考虑使用SVG,它提供了绘制圆形,多边形,路径等操做,尤为是path的使用对于咱们这种不会画曲线 的人太方便了。而且svg的dom对事件的支持和处理也很好。jquery
2、有向无环图分析算法
Okay,了解了支持的展示效果和使用的技术,下面开始分析开发workflow的dag有向无环图吧(透露一下,有向无环图最重要的是计算每一个节点的最大步长了,最大步长也就是该节点在这一段关系中,距离根节点的最远距离,网上有一些计算的算法什么的,不过本人不会,搞不通算法)。本例的 核心技术实际上是 递归 。就是用递归就算每一个节点的最大步长。还不了解 递归 是什么的童鞋们先了解一下递归。canvas
1. 梳理dag关系,剥离不存关系的节点和存在关系的节点,并找到每段关系的根节点数组
(图三)dom
附注说明:
上图是一个DAG有向无环图的关系链展现。(不要认为dag有向无环图单单指的是上图的中间部分,咱们彻底能够将上图理解为一个完整的dag关系。由于考虑问题要全面嘛!不是全部的节点只存在一段关系中,也不是全部的节点就必定和其它节点有关系。因此固然会出现上图的展现状况。固然,上图的任何一段单提取出来也是一段完整的dag关系。不过,为了后续的讲解和dag插件通用性,举了一个存在多种状况的dag关系的例子,以后的讲解也会按照这个图片来讲明)。
方法思路:
前提:已知当前dag图中的全部节点和全部关系链。(注意节点间的关系链是有向的)
1. 遍历全部的节点,逐个节点断定当前遍历的节点是否在关系链中,若不存在,则把当前节点做为一个独立的节点存储到一个数组中,咱们就叫它 indiviual。(单独的节点其实跟后续的操做没有过多的关系,咱们 只是考虑到这样特殊的状况,把它们都单独提取出来,展现到页面上便可。)
2. 同第1步同样,不过是取存在在关系链中的全部的节点,把它们push到另外一个数组中,叫它 refNodes。
3. 获取了全部的再关系链中的节点数组 refNodes,遍历refNodes,并对照关系链,查找当前遍历的节点是否有做为输入节点类型对应的输出节点,若是有,表示当前遍历的节点不是根节点,若没有,怎该节点为 根节点,把它们push到一个叫 rootNodes的数组中。(由于是有向无环图,因此关系链是有向的,好比A-->B-->C,A做为第一次遍历的节点,查找是否存在 ?-->A的这种关系链 ,也就是A做为输入点找它上级的输出点,若是遍历完全部关系链都没有发现这种状况,则A是一个根节点。同理,遍历到B的时候,就会找到 A-->B这种状况,因此B不是根节点)。
*注意*:为了保证程序的正确性,数组中不会出现重复的节点,必定要在存储数组前执行如下去重操做。
a). 能够定义一个javascript对象来存储dag中用的数据信息
//工做流对象 var relation={ links:[], //当前工做流中全部的关系链集合 individual:[], //存放全部没有关系的节点 refNodes:[], //存放有关系的节点 rootNodes:[], //存放关系中的根节点 };
b). 查找根节点示例代码,记得数组去重,可使用jquery 的工具函数inArray。(links数组中存储的是全部的关系链对象link.具体的依照我的开发习惯定义,这里只是为了方便读者能够理解部分代码给出我使用的示例)
/**
links中的关系对象存储示例
var relation={
links:[
{
output:{
nodeId:A, //输出节点的id
pointName:A_1 //输出接线点名称
},
input:{
nodeId:B, //输入节点的id
pointName:B_1 //输入接线点的名称
}
}
]
}
**/
//查找根节点 function findRootNodes(){ var len=relation.refNodes.length; for(var i=0;i<len;i++){ var node=relation.refNodes[i]; var isRootNode=true; $.each(relation.links,function(l,link){ var in_node=link.input.nodeId; //当前节点只要有做为输入点就不是根节点 if(node==in_node){ isRootNode=false; } }); if(isRootNode){ if($.inArray(node,relation.rootNodes)==-1){ relation.rootNodes.push(node); } } } }
2. 根据全部的根节点和有关系的节点及全部的关系链找到每一个节点的最大步长
(图四)
循环全部的节点和根节点,每一个节点的步长查找都要从根节点开始计算,如图四所示,以查找C节点的最大步长为例,遍历到C节点上时,查到第一个根节点A开始的关系网,第一次找到A,这时的步长是1,而后逐级向下查找,第二次找到B,步长计数为2,第三次找到C和D,步长为3,第四次找到C和E,步长为4,第五次已经遍历完当前根节点开始的一段关系,因此,上图上的无和步长5实际上是没有的。同理,由于有可能存在多个根节点,因此都要遍历。第二个根节点为F,遍历后找不到C,因此不记录步长。
注意两方面:第一:取节点的最大步长
第二:遍历步长为递归方式,每次从根节点查找(根节点以集合方式存储),取得下一级别的节点集合做为开始,每个级别为一个步长计数,如此反复,直到集合为空为止。
关键代码以下:(节点的步长其实是为了计算节点的横向排列位置用的,因此下面的代码用了一个nodeLevel对象来记录每一个节点的最大步长)
//根据根节点和全部有关系的节点及关系链找到每一个节点的最大步长 function setNodeMaxStep(){ var len=relation.refNodes.length; for(var i=0;i<len;i++){ var search_node=relation.refNodes[i]; //每次须要断定最大步长的节点 //每次从根节点开始查找 for(var k=0;k<relation.rootNodes.length;k++){ var root_node=relation.rootNodes[k]; //获取当前根节点 var node_arr=new Array(); //存放依次遍历的同级节点,首次放入根节点,逐步查找下一级别 node_arr.push(root_node); var stepCount=1; //从根节点级别时步长计数器归零 //设置根节点的级别,根节点的步长为零 nodeLevel[root_node]={}; nodeLevel[root_node].breadth=stepCount; //递归查找search_node的最大步长 recordNodeStep(node_arr,search_node,stepCount); } } } function recordNodeStep(arr,search_node,stepCount){ if(arr!=null && arr!=undefined && arr.length>0){ var temp_node_arr=new Array(); //临时存储下一级别节点的数组 stepCount++; //逐级增长步长,级别的断定就是arr数组的出现频次 for(var n=0;n<arr.length;n++){ var temp_node=arr[n]; //做为输出节点去查找输入点(即查找下一级节点) $.each(relation.links,function(l,link){ var in_node=link.input.nodeId; var out_node=link.output.nodeId; if(temp_node==out_node){ //查找到输入点 if($.inArray(in_node,temp_node_arr)==-1){ temp_node_arr.push(in_node); } //节点做为输出点时找到对应的输入点,若输入点等于须要断定步长的节点怎记录步长信息 if(in_node==search_node){ //找到当前节点则记录当前步长 if(nodeLevel[in_node]==undefined){ nodeLevel[in_node]={}; } //考虑到被断定步长的节点有可能存在多个根节点的关系链中且每次切换根节点计算步长都会将步长计数器归零,所以须要保留最大步长数 if(nodeLevel[in_node].breadth!=undefined && nodeLevel[in_node].breadth!=null){ var last_breadth=nodeLevel[in_node].breadth; if(stepCount>last_breadth){ nodeLevel[in_node].breadth=stepCount; } }else{ nodeLevel[in_node].breadth=stepCount; } } } }); } arr=temp_node_arr; recordNodeStep(arr,search_node,stepCount); } }
3. 根据每一个节点的最大步长计算节点的深度级别,这样最终能够经过坐标的方式定位节点的位置
能够遍历每一个节点的步长Map对象,而后以步长作为key,初始化每一个步长的深度级别为0。而后再次遍历节点的步长Map对象,取得当前步长的深度数,遇到同步长的节点深度+1便可。这样,接线的纵向排列问题便可解决。
代码以下:
function setNodesDeepth(){ var deepthLevel={}; $.each(nodeLevel,function(i,node){ var breadth=node.breadth; if(deepthLevel[breadth]==undefined && deepthLevel[breadth]==null){ deepthLevel[breadth]=0; } }); $.each(nodeLevel,function(i,node){ var breadth=node.breadth; deepthLevel[breadth]+=1; node.deepth=deepthLevel[breadth]; }); }
Okay,Dag最关键的核心步长定位解决了。不过咱们以前还有一个individual的数组用来存放单独的节点,这个就简单啦,彻底能够将它们所有横向展现在svg画布上的顶端。能够直接遍历这个数组,每一个节点的横向步长逐个+1便可,纵向级别可固定为1。而后计算节点的X,Y坐标位置放置到svg画布上便可。
3、关于接线点和绘制和曲线的绘制说明
1. 接线点
由于本例用的是圆形的节点,接线点也是在圆形的边界上,因此仍是以圆形节点为例。计算方式其实就是使用的JavaScript的Math对象的sin和cos函数来肯定接线点的位置的。(不会使用的小伙伴能够上网上搜一下,好多的例子,再也不赘述了。)
2.曲线
曲线的绘制时经过svg的path路径绘制的,看了网上的例子,只要肯定起止点的x和y坐标便可。
例子以下:起始点坐标(354,164) 终止点坐标(762,80),而后结合例子看一下就知道怎么放置位置了吧。
<path d="M354,164C762,164,354,80,762,80" stroke-width="3" fill="none" stroke="#dddddd"></path>
结束语
本文主要介绍了一下在不会算法的状况下,如何使用递归获取有向无环图中各个节点的最大步长。以此来设置各节点的位置信息来实现dag的布局。经过此方法,咱们只须要知道节点和节点的关系便可绘制出一幅dag有向无环图了。若是但愿用户交互和体验更好些,能够实现svg缩放效果和移动效果。可使用svg的scale和translate方法来实现。
第一次写文章,若是有欠缺和不足的地方,欢迎你们指正探讨,不尽详细,感谢阅读。