【 D3.js 高級系列 — 10.0 】 思維導圖

思維導圖的節點具有層級關係和隸屬關係,很像枝葉從樹幹伸展開來的形狀。在前面講解佈局的時候,提到有五個佈局是由層級佈局擴展來的,其中的樹狀圖(tree layout)和集羣圖(cluster layout)佈局製作出來的圖具有“樹形”。因此,可以憑藉這兩種佈局來製作思維導圖。

1001


1. 構造思路

樹狀圖佈局,將一個具有層級關係的對象root轉換成節點數組nodes時,情況如下。有一個root對象:

{
	name: "node1",
	children: 
[
			{ name: "node2" },
			{ name: "node3" }
	]
}
經樹狀圖佈局轉換後,得到的節點數組nodes如下:
[
{
		name: "node1",
		children: 
		[
			{ name: "node2" },
			{ name: "node3" }
		]
	},
	{ name: "node2" },
	{ name: "node3" }
]

下圖是上述節點數組的示意圖。由於 node1 具有子節點,可作爲開關使用,點擊 node1 纔會展現 node2 和 node3。

1002

問題是:怎樣製作一個“開關”,使得點擊樹狀圖中的某個節點時,樹狀圖更新並顯示出被點擊節點的子節點。

我們知道,樹狀圖的層級關係是由每一個對象的children屬性決定的(當然,也可以通過tree.children()修改這一點),也就是說,如果某一個節點的children值爲空,則再次用佈局計算時,其子節點就不會進入節點數組nodes了。例如,將root改爲:

{
	name: "node1",
	children: null
}

則得到的節點數組nodes裏將沒有node2和node3節點。也就是說,“開關”只要將被點擊節點的children設置爲null即可。但是,由於將來可能還要用到children節點,可設一臨時變量_children保存此值,例如:

{
	name: "node1",
	children: null
       _children: 		/* 臨時變量 */
       [
			{ name: "node2" },
			{ name: "node3" }
	]

}

樹狀圖佈局不會認爲_children是保存子節點的變量,只把它看做是一般的變量而保存下來,因此節點數組nodes裏只有一個節點。根據上面的思路,寫一個開關切換函數如下。

//切換開關,d 爲被點擊的節點
function toggle(d){

if(d.children){
//如果有子節點
    	d._children = d.children; //將該子節點保存到 _children
     	d.children = null;  //將子節點設置爲null
    
}else{
//如果沒有子節點
     	d.children = d._children; //從 _children 取回原來的子節點 
     	d._children = null; //將 _children 設置爲 null
    }
}

每次開關狀態切換時,都要重新調用佈局重新計算節點的位置,也就是說,要有一個重繪函數能夠處理數據發生更新的情況。這就又要用到【選擇集與數據 - 第 5 章】的處理模板,重繪函數的部分代碼如下,尤其要注意開關函數是如何被使用的。

//重繪函數
 function redraw(source){

    //重新計算節點和連線
    var nodes = tree.nodes(root);
    var links = tree.links(nodes);

//獲取節點的update部分
    var nodeUpdate = svg.selectAll(".node")
                        .data(nodes, function(d){ return d.name; });

    //獲取節點的enter部分
    var nodeEnter = nodeUpdate.enter();

//在給enter部分添加新的節點時,添加監聽器,應用開關切換函數
nodeEnter.append("g")
     	   .on("click", function(d) { 
toggle(d); 
redraw(d); 
});

/***************
        省略
***************/
}

每一個被新添加的節點,都會響應click事件。當某個節點被點擊時,如果它具有子節點,則在開關切換函數的作用下,root對象被修改了,然後調用重繪函數後,新的樹狀圖將被繪製。如此一來,樹狀圖具有開關功能,也就可以當做思維導圖使用了。


2. 製作思維導圖

首先,要有一個具有層級關係的 JSON 文件,本文使用:learn.json

其次,依次創建樹狀圖佈局、對角線生成器等,用於繪製樹狀圖。

然後,實現最關鍵的重繪函數,函數聲明如下:

function redraw(source)

只有一個參數source,這是被點擊的節點,如果該節點原來爲閉合狀態,點擊後其子節點將顯現,如果原來爲打開狀態,點擊後其子節點將隱藏。函數體的實現,分爲四個步驟:

2.1 調用佈局,計算節點和連線數組

樹狀圖佈局的tree.nodes()返回節點數組,tree.links()返回連線數組。其中,對節點的y座標重新計算,使其只與節點的深度有關,由於後期繪製節點和連線時要將x和y座標對調,因此這裏重計算的實際上是水平方向的座標。

//應用佈局,計算節點和連線
var nodes = tree.nodes(root);
var links = tree.links(nodes);

//重新計算節點的y座標
nodes.forEach(function(d) { d.y = d.depth * 180; });

之所以重新計算y座標,是爲了當數據更新(用於點擊節點)時,保證樹狀圖的結構不要發生太大的變化,如此看起來比較自然。

2.2 分別處理節點的update、enter、exit三部分

在svg裏選擇當前所有的節點,使其與節點數組nodes綁定,綁定時要設定一個鍵函數。鍵函數裏直接返回d.name,當節點數組發生更新時,新節點要與舊節點在名稱上相對應。

//獲取節點的update部分
var nodeUpdate = svg.selectAll(".node")
               .data(nodes, function(d){ return d.name; });

//獲取節點的enter部分
var nodeEnter = nodeUpdate.enter();

//獲取節點的exit部分
var nodeExit = nodeUpdate.exit();

先處理enter部分,即添加節點。節點的構成爲:分組元素裏有一個圓表示節點,還有一個文字元素表示節點的名稱。元素結構如下:

本例中,每一個新添加的節點都將緩慢地過渡到自己本身的位置,如此更具有友好性。因此,新節點的初始位置都設定在source節點處,確切的說是重回之前source節點的位置,該座標是保存在source.x0和source.y0裏的。另外,對於每一個新節點,設置的半徑爲0,設置爲完全透明,接下來在處理update部分的時候會將這些新節點過渡到正常狀態的。下圖展示了處理enter部分和update部分時如何節點的位置時如何確定和過渡的。

1003

處理enter部分的代碼如下。
//1. 節點的 Enter 部分的處理辦法
var enterNodes = nodeEnter.append("g")
       .attr("class","node")
       .attr("transform", function(d) { 
return "translate(" + source.y0 + "," + source.x0 + ")"; 
})
       .on("click", function(d) { 
toggle(d); 
redraw(d); 
});

//省略添加圓和文字部分

然後處理update部分,將所有節點(包括在enter部分新添加的節點)都緩緩過渡到新的位置。由於新的節點數組是與節點選擇集綁定在一起的,因此d.x和d.y裏保存的就是新的座標值。

//2. 節點的 Update 部分的處理辦法
var updateNodes = nodeUpdate.transition()
               .duration(500)
               .attr("transform", function(d) { 
return "translate(" + d.y + "," + d.x + ")"; 
});
最後處理exit部分,需要刪除的節點的位置緩緩過渡到其父節點處。
//3. 節點的 Exit 部分的處理辦法
var exitNodes = nodeExit.transition()
 .duration(500)
.attr("transform", function(d) { 
return "translate(" + source.y + "," + source.x + ")"; 
})
 .remove();

2.3 分別處理連線的update、enter、exit三部分

在svg中選擇所有的連線,綁定連線數組links,由此可獲得連線的update、enter、exit部分。

//獲取連線的update部分
var linkUpdate = svg.selectAll(".link")
         .data(links, function(d){ return d.target.name; });

//獲取連線的enter部分
var linkEnter = linkUpdate.enter();

//獲取連線的exit部分
var linkExit = linkUpdate.exit();

對於連線的enter部分,是插入路徑元素path,路徑由對角線生成器獲取,對角線的起點和終點座標都是(source.x0, source.y0)。

對於連線的update部分,將所有的連線的位置(對角線的起點和終點)更新到新的位置,即目前綁定的數組links裏保存的位置。

對於連線的exit部分,令其緩緩過渡到當前的source點,再移除。

//1. 連線的 Enter 部分的處理辦法
linkEnter.insert("path",".node")
          .attr("class", "link")
          .attr("d", function(d) {
              var o = {x: source.x0, y: source.y0};
              return diagonal({source: o, target: o});
          })
          .transition()
          .duration(500)
          .attr("d", diagonal);

 //2. 連線的 Update 部分的處理辦法
 linkUpdate.transition()
        .duration(500)
        .attr("d", diagonal);

 //3. 連線的 Exit 部分的處理辦法
 linkExit.transition()
          .duration(500)
          .attr("d", function(d) {
            var o = {x: source.x, y: source.y};
            return diagonal({source: o, target: o});
          })
          .remove();

2.4 保存當前的節點座標

當用戶點擊節點後,數據發生更新,即每個節點的座標要發生更新。但是,在對節點和連線進行過渡操作的時候,需要使用到更新前的數據(source.x0和source.y0)。因此,每一次調用重繪函數,都要將當前節點的位置保存下來。

nodes.forEach(function(d) {
      d.x0 = d.x;
      d.y0 = d.y;
});

x和y座標分別保存在x0和y0中,在調用redraw(source)時,被點擊的節點被作爲參數傳到了重繪函數裏,因此source.x0和source.y0裏保存的是被點擊之前節點的座標。

3. 結果

結果如下圖所示,點擊節點可以展開子節點。

10041005

源代碼請單擊以下鏈接,郵件查看源代碼:

http://www.ourd3js.com/demo/G-10.0/mind.html

謝謝閱讀。

文檔信息

發佈了122 篇原創文章 · 獲贊 380 · 訪問量 109萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章