当前位置:   article > 正文

【D3.js - v5.x】(5)绘制力导向图 | 附完整代码_d3.js 力导向

d3.js 力导向

力导向图

力导向图(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 } ];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

节点是一些城市名,连线的两端是节点的序号(序号从 0 开始)。

这些数据是不能作图的,因为不知道节点和连线的坐标。

于是,我们想到布局

一个力导向图的布局如下:定义一个力引导仿真器

var simulation = d3.forceSimulation(nodes);
  • 1

文档: 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
  • 2
  • 3
  • 4
  1. d3.forceSimulation().force(name),也就是当force中只有一个参数,这个参数是某个力的名称,那么这段代码返回的是某个具体的力,例如:

d3.forceSimulation().force(“link”),则返回的是d3.forceLink()这个力。

如果没有指定 force 则返回当前仿真的对应 name 的力模型,如果没有对应的 name 则返回 undefined

如果要移除对应的 name 的仿真,可以为其指定 null,比如:

simulation.force("charge", null);
  • 1
  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());
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

如此,数组 nodes 和 edges 的数据都发生了变化。在控制台输出一下,看看发生了什么变化。

console.log(nodes);
console.log(edges);
  • 1
  • 2

在这里插入图片描述

转换后,节点对象里多了一些变量。

2. 绘制

有了转换后的数据,就可以作图了。分别绘制三种图形元素:

  • line,线段,表示连线。

  • circle,圆,表示节点。

  • text,文字,描述节点。

2.1 设置一个颜色比例尺
//设置一个color的颜色比例尺,为了让不同的扇形呈现不同的颜色
var colorScale = d3.scaleOrdinal()
    .domain(d3.range(nodes.length))
    .range(d3.schemeCategory10);
  • 1
  • 2
  • 3
  • 4
2.2 生成节点数据
//生成节点数据
forceSimulation.nodes(nodes)
    .on("tick",ticked);//这个函数很重要,后面给出具体实现和说明
  • 1
  • 2
  • 3

这里出现了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 + ")"; });
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

####2.3 生成边集数据

//生成边数据
forceSimulation.force("link")
   .links(edges)
   .distance(function(d){//每一边的长度
    	return d.value*100;
}) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
2.4 设置图形中心位置
//设置图形的中心位置	
forceSimulation.force("center")
	.x(width/2)
	.y(height/2);
  • 1
  • 2
  • 3
  • 4
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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

应该先绘制边,再绘制顶点,因为在d3中,各元素是有层级关系的,

  • 边上的文字
var linksText = g.append("g")
	.selectAll("text")
	.data(edges)
	.enter()
	.append("text")
	.text(function(d){
		return d.relation;
	})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 先建立用来放在每个节点和对应文字的分组
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)
	);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里出现了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;
   	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 节点和文字
//绘制节点
 	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;
 		})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述

完整代码

<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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130

在这里插入图片描述

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号