JavaScript + SVG實現Web前端WorkFlow工做流DAG有向無環圖

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方法來實現。

 第一次寫文章,若是有欠缺和不足的地方,歡迎你們指正探討,不盡詳細,感謝閱讀。

相關文章
相關標籤/搜索