JavaScript + SVG实现Web前端WorkFlow工作流DAG有向无环图

一、效果图展示及说明

(图一)

(图二)

附注说明:

1. 图例都是DAG有向无环图的展现效果。两张图的区别为第二张图包含了多个分段关系。放置展示图片效果主要是为了说明该例子支持多段关系的展现(当前也包括单独的节点展现,图例没有展示)

2.图例中的圆形和曲线均使用的是SVG绘制。之前考虑了三种方式,一种是html5的canvas,一种是原始的html DOM,再有就是SVG。不过canvas对事件的支持不是很好(记得之前看过一篇文章主要是通过计算鼠标定位是否在canvas上的某个区域来触发事件机制,比较不适用工作流节点上的各种事件触发机制),另外原始的 DOM虽然对事件的处理比canvas要方便,但是从编码和绘制dom上则会过度的耗费资源,尤其是曲线的绘制,毕竟过多的dom操作都会影响性能拖慢响应速度,所以综合考虑使用SVG,它提供了绘制圆形,多边形,路径等操作,尤其是path的使用对于我们这种不会画曲线 的人太方便了。并且svg的dom对事件的支持和处理也很好。

二、有向无环图分析

Okay,了解了支持的展现效果和使用的技术,下面开始分析开发workflow的dag有向无环图吧(透露一下,有向无环图最重要的是计算每个节点的最大步长了,最大步长也就是该节点在这一段关系中,距离根节点的最远距离,网上有一些计算的算法什么的,不过本人不会,搞不通算法)。本例的 核心技术其实是 递归 。就是用递归就算每个节点的最大步长。还不了解 递归 是什么的童鞋们先了解一下递归。

  1. 理dag关系,剥离不存关系的节点和存在关系的节点,并找到每段关系的根节点

(图三)

附注说明:

上图是一个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画布上即可。

三、关于接线点和绘制和曲线的绘制说明

  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方法来实现。

第一次写文章,如果有欠缺和不足的地方,欢迎大家指正探讨,不尽详细,感谢阅读。

时间: 2024-10-16 06:03:47

JavaScript + SVG实现Web前端WorkFlow工作流DAG有向无环图的相关文章

JavaScript &amp; jQuery交互式Web前端开发

这篇是计算机中网络编程javascript类的优质预售推荐<JavaScript & jQuery交互式Web前端开发>. 内容简介 欢迎选择一种更高效的学习JavaScript和jQuery的方式. 你是一名JavaScript新手?或是您曾经向自己的Web页面上添加过一些脚本,但想以一种更好的方式来实现它们?本书非常适合您.本书不仅向您展示如何阅读和编写JavaScript代码,同时还会以一种简单且视觉化的方式,教您有关计算机编程的基础知识.阅读本书之前,您只需要对HTML和CSS

javascript实现有向无环图中任意两点最短路径的dijistra算法

有向无环图 一个无环的有向图称做有向无环图(directed acycline praph).简称DAG 图.DAG 图是一类较有向树更一般的特殊有向图, dijistra算法 摘自 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 1.定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijk

PGM学习之六 从有向无环图(DAG)到贝叶斯网络(Bayesian Networks)

本文的目的是记录一些在学习贝叶斯网络(Bayesian Networks)过程中遇到的基本问题.主要包括有向无环图(DAG),I-Maps,分解(Factorization),有向分割(d-Separation),最小I-Maps(Minimal I-Maps)等.主要参考Nir Friedman的相关PPT. 1  概率分布(Probability Distributions) 令X1,...,Xn表示随机变量:令P是X1,...,Xn的联合分布(joint distribution).如果每

有向无环图(DAG)拓扑排序的两种方法

如下图的DAG: 第一种: (1)从AOV网中选择一个没有前驱的顶点并且输出它: (2)从AOV网中删除该顶点,并且上去所有该顶点为尾的弧: (3)重复上述两步,直到全部顶点都被输出,或者AOV网中不存在没有前驱的顶点. 第二种: 使用深度优先搜索(DFS),并标记每一个节点的第一次访问(pre)和最后一次访问时间(post),最后post的逆序就是DAG的拓扑排序,其实也是节点在进行DFS搜索时,出栈的逆序就是拓扑排序. 拓扑序列的结果有: (1) c++,高等数学,离散数学,数据结构,概率论

慕课网实战—《用组件方式开发 Web App全站 》笔记七-饼图和环图组件开发

运用HTML5.CSS3.JS流行技术,采用组件式开发模式,开发Web App全站!技术大牛带你统统拿下不同类型的HTML5动态数据报告! <用组件方式开发 Web App全站 > 饼图开发(绘制饼图准备) 饼图实现原理 饼图开发(绘制饼图) 代码 /* 饼图组件对象 */ var H5ComponentPie =function ( name, cfg ) { var component = new H5ComponentBase( name ,cfg ); // 绘制网格线 - 背景层 v

javascript基础-《web前端最佳实践》

先贴代码 1 <form action=""> 2 <p> 3 <label for="x">Number:</label> 4 <input id="x" name="x" type="number" /> 5 </p> 6 <input id="submit" type="submit"

web前端技术课程讲解之关于轮播图水平无缝滚动

制作这个简单的轮播图之前,你必须掌握html.css.以及JavaScript中的节点操作,定时器的使用,条件语句的使用以及JS修改样式和动画原理. 轮播图对于网页来说是个常见也可以说是必备的部分,而轮播图的形式也是各式各样,有简约的也有炫酷的.当然,这里我就只说简约的全屏轮播图水平无缝滚动. 首先是给出一个div标签,并且给这个标签设置宽高(宽高要和图片一致,否则给图片设置居中)以及溢出隐藏(overflow:hidden)并设置定位为相对定位(position:relative),然后用一个

web前端sprite,精灵图,雪碧图

css sprite 俗称:精灵图,雪碧图,指将整个页面不同的图片or图标合并在一张图上:优点:使用CSS Sprite 可以减少网络请求,提高网页加载性能,不会出现网页上端加载完毕下面还在加载中这一问题 缺点:如果后期更改其中某一图标,且其像素大小发生改变,需要重新对所有图标进行定位 精灵图制作: photoshop选择透明图层,将需要的图标和图片放入 测试用图: ??? 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta c

有向无环图(DAG)的最小路径覆盖

DAG的最小路径覆盖 定义:在一个有向图中,找出最少的路径,使得这些路径经过了所有的点. 最小路径覆盖分为最小不相交路径覆盖和最小可相交路径覆盖. 最小不相交路径覆盖:每一条路径经过的顶点各不相同.如图,其最小路径覆盖数为3.即1->3>4,2,5. 最小可相交路径覆盖:每一条路径经过的顶点可以相同.如果其最小路径覆盖数为2.即1->3->4,2->3>5. 特别的,每个点自己也可以称为是路径覆盖,只不过路径的长度是0. DAG的最小不相交路径覆盖 算法:把原图的每个点