思維導(dǎo)圖的節(jié)點具有層級關(guān)系和隸屬關(guān)系,很像枝葉從樹干伸展開來的形狀。在前面講解布局的時候,提到有五個布局是由層級布局擴展來的,其中的樹狀圖(tree layout)和集群圖(cluster layout)布局制作出來的圖具有“樹形”。因此,可以憑借這兩種布局來制作思維導(dǎo)圖。
樹狀圖布局,將一個具有層級關(guān)系的對象root轉(zhuǎn)換成節(jié)點數(shù)組nodes時,情況如下。有一個root對象:
{ name: "node1", children: [ { name: "node2" }, { name: "node3" } ] }
經(jīng)樹狀圖布局轉(zhuǎn)換后,得到的節(jié)點數(shù)組nodes如下:
[ { name: "node1", children: [ { name: "node2" }, { name: "node3" } ] }, { name: "node2" }, { name: "node3" } ]
下圖是上述節(jié)點數(shù)組的示意圖。由于 node1 具有子節(jié)點,可作為開關(guān)使用,點擊 node1 才會展現(xiàn) node2 和 node3。
問題是:怎樣制作一個“開關(guān)”,使得點擊樹狀圖中的某個節(jié)點時,樹狀圖更新并顯示出被點擊節(jié)點的子節(jié)點。
我們知道,樹狀圖的層級關(guān)系是由每一個對象的children屬性決定的(當(dāng)然,也可以通過tree.children()修改這一點),也就是說,如果某一個節(jié)點的children值為空,則再次用布局計算時,其子節(jié)點就不會進入節(jié)點數(shù)組nodes了。例如,將root改為:
{ name: "node1", children: null }
則得到的節(jié)點數(shù)組nodes里將沒有node2和node3節(jié)點。也就是說,“開關(guān)”只要將被點擊節(jié)點的children設(shè)置為null即可。但是,由于將來可能還要用到children節(jié)點,可設(shè)一臨時變量_children保存此值,例如:
{ name: "node1", children: null _children: /* 臨時變量 */ [ { name: "node2" }, { name: "node3" } ] }
樹狀圖布局不會認為_children是保存子節(jié)點的變量,只把它看做是一般的變量而保存下來,因此節(jié)點數(shù)組nodes里只有一個節(jié)點。根據(jù)上面的思路,寫一個開關(guān)切換函數(shù)如下。
//切換開關(guān),d 為被點擊的節(jié)點 function toggle(d){ if(d.children){ //如果有子節(jié)點 d._children = d.children; //將該子節(jié)點保存到 _children d.children = null; //將子節(jié)點設(shè)置為null }else{ //如果沒有子節(jié)點 d.children = d._children; //從 _children 取回原來的子節(jié)點 d._children = null; //將 _children 設(shè)置為 null } }
每次開關(guān)狀態(tài)切換時,都要重新調(diào)用布局重新計算節(jié)點的位置,也就是說,要有一個重繪函數(shù)能夠處理數(shù)據(jù)發(fā)生更新的情況。這就又要用到【選擇集與數(shù)據(jù) - 第 5 章】的處理模板,重繪函數(shù)的部分代碼如下,尤其要注意開關(guān)函數(shù)是如何被使用的。
//重繪函數(shù) function redraw(source){ //重新計算節(jié)點和連線 var nodes = tree.nodes(root); var links = tree.links(nodes); //獲取節(jié)點的update部分 var nodeUpdate = svg.selectAll(".node") .data(nodes, function(d){ return d.name; }); //獲取節(jié)點的enter部分 var nodeEnter = nodeUpdate.enter(); //在給enter部分添加新的節(jié)點時,添加監(jiān)聽器,應(yīng)用開關(guān)切換函數(shù) nodeEnter.append("g") .on("click", function(d) { toggle(d); redraw(d); }); /*************** 省略 ***************/ }
每一個被新添加的節(jié)點,都會響應(yīng)click事件。當(dāng)某個節(jié)點被點擊時,如果它具有子節(jié)點,則在開關(guān)切換函數(shù)的作用下,root對象被修改了,然后調(diào)用重繪函數(shù)后,新的樹狀圖將被繪制。如此一來,樹狀圖具有開關(guān)功能,也就可以當(dāng)做思維導(dǎo)圖使用了。
首先,要有一個具有層級關(guān)系的 JSON 文件,本文使用:learn.json
{"name":"如何學(xué)習(xí)D3","children":[ { "name":"預(yù)備知識" , "children": [ {"name":"HTML & CSS" }, {"name":"JavaScript" }, {"name":"DOM" }, {"name":"SVG" } ] }, { "name":"安裝" , "children": [ { "name":"記事本軟件", "children": [ {"name":"Notepad++"}, {"name":"EditPlus"}, {"name":"Sublime Text"} ] }, { "name":"服務(wù)器軟件", "children": [ {"name":"Apache Http Server"}, {"name":"Tomcat"} ] }, {"name":"下載D3.js"} ] }, { "name":"入門", "children": [ { "name":"選擇集", "children": [ {"name":"select"}, {"name":"selectAll"} ] }, { "name":"綁定數(shù)據(jù)", "children": [ {"name":"datum"}, {"name":"data"} ] }, {"name":"添加刪除元素"}, { "name":"簡單圖形", "children": [ {"name":"柱形圖"}, {"name":"折線圖"}, {"name":"散點圖"} ] }, {"name":"比例尺"}, {"name":"生成器"}, {"name":"過渡"} ] }, { "name":"進階" , "children": [ { "name":"布局的應(yīng)用", "children": [ {"name":"餅狀圖"}, {"name":"樹狀圖"}, {"name":"矩陣樹圖"} ] }, {"name":"地圖"} ] }]}
其次,依次創(chuàng)建樹狀圖布局、對角線生成器等,用于繪制樹狀圖。
然后,實現(xiàn)最關(guān)鍵的重繪函數(shù),函數(shù)聲明如下:
function redraw(source)
只有一個參數(shù)source,這是被點擊的節(jié)點,如果該節(jié)點原來為閉合狀態(tài),點擊后其子節(jié)點將顯現(xiàn),如果原來為打開狀態(tài),點擊后其子節(jié)點將隱藏。函數(shù)體的實現(xiàn),分為四個步驟:
樹狀圖布局的tree.nodes()返回節(jié)點數(shù)組,tree.links()返回連線數(shù)組。其中,對節(jié)點的y坐標(biāo)重新計算,使其只與節(jié)點的深度有關(guān),由于后期繪制節(jié)點和連線時要將x和y坐標(biāo)對調(diào),因此這里重計算的實際上是水平方向的坐標(biāo)。
//應(yīng)用布局,計算節(jié)點和連線 var nodes = tree.nodes(root); var links = tree.links(nodes); //重新計算節(jié)點的y坐標(biāo) nodes.forEach(function(d) { d.y = d.depth * 180; });
之所以重新計算y坐標(biāo),是為了當(dāng)數(shù)據(jù)更新(用于點擊節(jié)點)時,保證樹狀圖的結(jié)構(gòu)不要發(fā)生太大的變化,如此看起來比較自然。
在svg里選擇當(dāng)前所有的節(jié)點,使其與節(jié)點數(shù)組nodes綁定,綁定時要設(shè)定一個鍵函數(shù)。鍵函數(shù)里直接返回d.name,當(dāng)節(jié)點數(shù)組發(fā)生更新時,新節(jié)點要與舊節(jié)點在名稱上相對應(yīng)。
//獲取節(jié)點的update部分 var nodeUpdate = svg.selectAll(".node") .data(nodes, function(d){ return d.name; }); //獲取節(jié)點的enter部分 var nodeEnter = nodeUpdate.enter(); //獲取節(jié)點的exit部分 var nodeExit = nodeUpdate.exit();
先處理enter部分,即添加節(jié)點。節(jié)點的構(gòu)成為:分組元素里有一個圓表示節(jié)點,還有一個文字元素表示節(jié)點的名稱。元素結(jié)構(gòu)如下:
本例中,每一個新添加的節(jié)點都將緩慢地過渡到自己本身的位置,如此更具有友好性。因此,新節(jié)點的初始位置都設(shè)定在source節(jié)點處,確切的說是重回之前source節(jié)點的位置,該坐標(biāo)是保存在source.x0和source.y0里的。另外,對于每一個新節(jié)點,設(shè)置的半徑為0,設(shè)置為完全透明,接下來在處理update部分的時候會將這些新節(jié)點過渡到正常狀態(tài)的。下圖展示了處理enter部分和update部分時如何節(jié)點的位置時如何確定和過渡的。
處理enter部分的代碼如下。
//1. 節(jié)點的 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部分,將所有節(jié)點(包括在enter部分新添加的節(jié)點)都緩緩過渡到新的位置。由于新的節(jié)點數(shù)組是與節(jié)點選擇集綁定在一起的,因此d.x和d.y里保存的就是新的坐標(biāo)值。
//2. 節(jié)點的 Update 部分的處理辦法 var updateNodes = nodeUpdate.transition() .duration(500) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
最后處理exit部分,需要刪除的節(jié)點的位置緩緩過渡到其父節(jié)點處。
//3. 節(jié)點的 Exit 部分的處理辦法 var exitNodes = nodeExit.transition() .duration(500) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .remove();
在svg中選擇所有的連線,綁定連線數(shù)組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,路徑由對角線生成器獲取,對角線的起點和終點坐標(biāo)都是(source.x0, source.y0)。
對于連線的update部分,將所有的連線的位置(對角線的起點和終點)更新到新的位置,即目前綁定的數(shù)組links里保存的位置。
對于連線的exit部分,令其緩緩過渡到當(dāng)前的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();
當(dāng)用戶點擊節(jié)點后,數(shù)據(jù)發(fā)生更新,即每個節(jié)點的坐標(biāo)要發(fā)生更新。但是,在對節(jié)點和連線進行過渡操作的時候,需要使用到更新前的數(shù)據(jù)(source.x0和source.y0)。因此,每一次調(diào)用重繪函數(shù),都要將當(dāng)前節(jié)點的位置保存下來。
nodes.forEach(function(d) { d.x0 = d.x; d.y0 = d.y; });
x和y坐標(biāo)分別保存在x0和y0中,在調(diào)用redraw(source)時,被點擊的節(jié)點被作為參數(shù)傳到了重繪函數(shù)里,因此source.x0和source.y0里保存的是被點擊之前節(jié)點的坐標(biāo)。
結(jié)果如下圖所示,點擊節(jié)點可以展開子節(jié)點。
源代碼請單擊以下鏈接,郵件查看源代碼: