【D3.js - v5.x】(5)繪製力導向圖 | 附完整代碼

力導向圖

力導向圖(Force-Directed Graph),是繪圖的一種算法

在二維或三維空間裏配置節點,節點之間用線連接,稱爲連線。各連線的長度幾乎相等,且儘可能不相交。

節點和連線都被施加了力的作用,力是根據節點和連線的相對位置計算的。

根據力的作用,來計算節點和連線的運動軌跡,並不斷降低它們的能量,最終達到一種能量很低的安定狀態。

力導向圖能表示節點之間的多對多的關係。

初始數據如下:

var nodes = [ { name: "桂林" }, { name: "廣州" },
              { name: "廈門" }, { name: "杭州" },
              { name: "上海" }, { name: "青島" },
              { name: "天津" } ];

 var edges = [ { source : 0 , target: 1 } , { source : 0 , target: 2 } ,
               { source : 0 , target: 3 } , { source : 1 , target: 4 } ,
               { source : 1 , target: 5 } , { source : 1 , target: 6 } ];

節點是一些城市名,連線的兩端是節點的序號(序號從 0 開始)。

這些數據是不能作圖的,因爲不知道節點和連線的座標。

於是,我們想到佈局

一個力導向圖的佈局如下:定義一個力引導仿真器

var simulation = d3.forceSimulation(nodes);

文檔: https://www.d3js.org.cn/document/d3-force/#installing

  • d3.forceSimulation([nodes]) ,新建一個力導向圖,使用指定的 nodes 創建一個新的沒有任何 forces(力模型) 的仿真。如果沒有指定 nodes 則默認爲空數組。仿真會自動 starts(啓動)
  • `d3.forceSimulation().force(name[, force]),添加或者移除一個力
var simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("link", d3.forceLink(links))
    .force("center", d3.forceCenter());
  1. d3.forceSimulation().force(name),也就是當force中只有一個參數,這個參數是某個力的名稱,那麼這段代碼返回的是某個具體的力,例如:

d3.forceSimulation().force(“link”),則返回的是d3.forceLink()這個力。

如果沒有指定 force 則返回當前仿真的對應 name 的力模型,如果沒有對應的 name 則返回 undefined

如果要移除對應的 name 的仿真,可以爲其指定 null,比如:

simulation.force("charge", null);
  1. d3.forceSimulation().nodes()`,輸入是一個數組,然後將這個輸入的數組進行一定的數據轉換。如果指定了 nodes 則將仿真的節點設置爲指定的對象數組,並根據需要創建它們的位置和速度,然後 重新初始化 綁定的 力模型,並返回當前仿真。

每個 node 必須是一個對象類型,下面的幾個屬性將會被仿真系統添加:

  • index - 節點在 nodes 數組中的索引
  • x - 節點當前的 x-座標
  • y - 節點當前的 y-座標
  • vx - 節點當前的 x-方向速度
  • vy - 節點當前的 y-方向速度

位置 ⟨x,y⟩ 以及速度 ⟨vx,vy⟩ 隨後可能被仿真中的 力模型 修改. 如果 vxvy 爲 NaN, 則速度會被初始化爲 ⟨0,0⟩. 如果 xy 爲 NaN, 則位置會按照 phyllotaxis arrangement 被初始化, 這樣初始化佈局是爲了能使得節點在原點周圍均勻分佈。

如果想要某個節點固定在一個位置,可以指定以下兩個額外的屬性:

  • fx - 節點的固定 x-位置
  • fy - 節點的固定 y-位置
  1. d3.forceLink.links(),這裏輸入的也是一個數組(邊集),然後對輸入的邊集進行轉換

  2. simulation.tick()函數,按指定的迭代次數手動執行仿真,並返回仿真。這個函數對於力導向圖來說非常重要,因爲力導向圖是不斷運動的,每一時刻都在發生更新,所以需要不斷更新節點和連線的位置。如果沒有指定 iterations 則默認爲 1,也就是迭代一次

  3. d3.drag(),是力導向圖可以被拖動

繪製

1. 數據準備

	var marge = {top:60,bottom:60,left:60,right:60}
    	var svg = d3.select("svg")
    	var width = svg.attr("width")
    	var height = svg.attr("height")
    	var g = svg.append("g")    .attr("transform","translate("+marge.top+","+marge.left+")");
	//準備數據
	var nodes = [ { name: "桂林" }, { name: "廣州" },
              { name: "廈門" }, { name: "杭州" },
              { name: "上海" }, { name: "青島" },
              { name: "天津" } ];

    var edges = [ { source : 0 , target: 1 } , { source : 0 , target: 2 } ,
               { source : 0 , target: 3 } , { source : 1 , target: 4 } ,
               { source : 1 , target: 5 } , { source : 1 , target: 6 } ];
//新建一個力導向圖
    var forceSimulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("link", d3.forceLink(links))
    .force("center", d3.forceCenter());
              

如此,數組 nodes 和 edges 的數據都發生了變化。在控制檯輸出一下,看看發生了什麼變化。

console.log(nodes);
console.log(edges);

在這裏插入圖片描述

轉換後,節點對象裏多了一些變量。

2. 繪製

有了轉換後的數據,就可以作圖了。分別繪製三種圖形元素:

  • line,線段,表示連線。

  • circle,圓,表示節點。

  • text,文字,描述節點。

2.1 設置一個顏色比例尺

//設置一個color的顏色比例尺,爲了讓不同的扇形呈現不同的顏色
var colorScale = d3.scaleOrdinal()
    .domain(d3.range(nodes.length))
    .range(d3.schemeCategory10);

2.2 生成節點數據

//生成節點數據
forceSimulation.nodes(nodes)
    .on("tick",ticked);//這個函數很重要,後面給出具體實現和說明

這裏出現了tick函數,我把它的實現寫到了一個有名函數ticked:

function ticked(){
    links
	.attr("x1",function(d){return d.source.x;})
	.attr("y1",function(d){return d.source.y;})
	.attr("x2",function(d){return d.target.x;})
	.attr("y2",function(d){return d.target.y;});
    			
	linksText
	.attr("x",function(d){
    	return (d.source.x+d.target.x)/2;
    })
    .attr("y",function(d){
    	return (d.source.y+d.target.y)/2;
    });
    			
    gs
    .attr("transform",function(d) { return "translate(" + d.x + "," + d.y + ")"; });
   }

####2.3 生成邊集數據

//生成邊數據
forceSimulation.force("link")
   .links(edges)
   .distance(function(d){//每一邊的長度
    	return d.value*100;
}) 

2.4 設置圖形中心位置

//設置圖形的中心位置	
forceSimulation.force("center")
	.x(width/2)
	.y(height/2);

2.5 繪製邊

//繪製邊
var links = g.append("g")
	.selectAll("line")
	.data(edges)
	.enter()
	.append("line")
	.attr("stroke",function(d,i){
		return colorScale(i);
	})
	.attr("stroke-width",1);

應該先繪製邊,再繪製頂點,因爲在d3中,各元素是有層級關係的,

  • 邊上的文字
var linksText = g.append("g")
	.selectAll("text")
	.data(edges)
	.enter()
	.append("text")
	.text(function(d){
		return d.relation;
	})
  • 先建立用來放在每個節點和對應文字的分組
var gs = g.selectAll(".circleText")
	.data(nodes)
	.enter()
	.append("g")
	.attr("transform",function(d,i){
		var cirX = d.x;
		var cirY = d.y;
		return "translate("+cirX+","+cirY+")";
	})
	.call(d3.drag()
		.on("start",started)
		.on("drag",dragged)
		.on("end",ended)
	);

這裏出現了start、drag、end函數:

function started(d){
	if(!d3.event.active){
  			forceSimulation.alphaTarget(0.8).restart();//設置衰減係數,對節點位置移動過程的模擬,數值越高移動越快,數值範圍[0,1]
  		}
  		d.fx = d.x;
  		d.fy = d.y;
  	}
   	function dragged(d){
   		d.fx = d3.event.x;
   		d.fy = d3.event.y;
   	}
   	function ended(d){
   		if(!d3.event.active){
   			forceSimulation.alphaTarget(0);
   		}
   		d.fx = null;
   		d.fy = null;
   	}
  • 節點和文字
//繪製節點
 	gs.append("circle")
 		.attr("r",10)
 		.attr("fill",function(d,i){
 			return colorScale(i);
 		})
 	//文字
 	gs.append("text")
 		.attr("x",-10)
 		.attr("y",-20)
 		.attr("dy",10)
 		.text(function(d){
 			return d.name;
 		})

完整代碼

<body>
        <svg width="500" height="500"></svg>
        <script>
            var marge = {top:60,bottom:60,left:60,right:60}
    	var svg = d3.select("svg")
    	var width = svg.attr("width")
    	var height = svg.attr("height")
    	var g = svg.append("g")    .attr("transform","translate("+marge.top+","+marge.left+")");
//	準備數據
  var nodes = [ { name: "桂林" }, { name: "廣州" },
              { name: "廈門" }, { name: "杭州" },
              { name: "上海" }, { name: "青島" },
              { name: "天津" } ];

 var edges = [ { source : 0 , target: 1,relation:"舍友",value:1 } , { source : 0 , target: 2,relation:"籍貫",value:1.3 } ,
               { source : 0 , target: 3,relation:"舍友",value:1 } , { source : 1 , target: 4,relation:"舍友",value:1 } ,
               { source : 1 , target: 5,relation:"籍貫",value:0.9 } , { source : 1 , target: 6,relation:"同學",value:1.6 } ];

   //新建一個力導向圖
 var forceSimulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("link", d3.forceLink(edges))
    .force("center", d3.forceCenter());
    //設置一個color的顏色比例尺,爲了讓不同的扇形呈現不同的顏色
    var colorScale = d3.scaleOrdinal()
    		.domain(d3.range(nodes.length))
    		.range(d3.schemeCategory10);
//生成節點數據
	forceSimulation.nodes(nodes)
        .on("tick",ticked);//這個函數很重要,後面給出具體實現和說明
        //生成邊數據
    	forceSimulation.force("link")
    		.links(edges)
    		.distance(function(d){//每一邊的長度
    			return d.value*100;
        }) 
        //設置圖形的中心位置	
    	forceSimulation.force("center")
    		.x(width/2)
        .y(height/2);
        
        //繪製邊
    	var links = g.append("g")
    		.selectAll("line")
    		.data(edges)
    		.enter()
    		.append("line")
    		.attr("stroke",function(d,i){
    			return colorScale(i);
    		})
        .attr("stroke-width",1);
        
        var linksText = g.append("g")
    		.selectAll("text")
    		.data(edges)
    		.enter()
    		.append("text")
    		.text(function(d){
    			return d.relation;
        })
        
        var gs = g.selectAll(".circleText")
    		.data(nodes)
    		.enter()
    		.append("g")
    		.attr("transform",function(d,i){
    			var cirX = d.x;
    			var cirY = d.y;
    			return "translate("+cirX+","+cirY+")";
    		})
    		.call(d3.drag()
    			.on("start",started)
    			.on("drag",dragged)
    			.on("end",ended)
        );
        
        
      
      //繪製節點
    	gs.append("circle")
    		.attr("r",10)
    		.attr("fill",function(d,i){
    			return colorScale(i);
    		})
    	//文字
    	gs.append("text")
    		.attr("x",-10)
    		.attr("y",-20)
    		.attr("dy",10)
    		.text(function(d){
    			return d.name;
        })
        function ticked(){
    		links
    			.attr("x1",function(d){return d.source.x;})
    			.attr("y1",function(d){return d.source.y;})
    			.attr("x2",function(d){return d.target.x;})
    			.attr("y2",function(d){return d.target.y;});
    			
    		linksText
    			.attr("x",function(d){
    			return (d.source.x+d.target.x)/2;
    		})
    		.attr("y",function(d){
    			return (d.source.y+d.target.y)/2;
    		});
    			
    		gs
    			.attr("transform",function(d) { return "translate(" + d.x + "," + d.y + ")"; });
    	}
        function started(d){
    		if(!d3.event.active){
    			forceSimulation.alphaTarget(0.8).restart();//設置衰減係數,對節點位置移動過程的模擬,數值越高移動越快,數值範圍[0,1]
    		}
    		d.fx = d.x;
    		d.fy = d.y;
    	}
    	function dragged(d){
    		d.fx = d3.event.x;
    		d.fy = d3.event.y;
    	}
    	function ended(d){
    		if(!d3.event.active){
    			forceSimulation.alphaTarget(0);
    		}
    		d.fx = null;
    		d.fy = null;
      }
   </script>
 </body>

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章