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

 第一次寫文章,如果有欠缺和不足的地方,歡迎大家指正探討,不盡詳細,感謝閱讀。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM