力导向图
力导向图(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());
d3.forceSimulation().force(name)
,也就是当force中只有一个参数,这个参数是某个力的名称,那么这段代码返回的是某个具体的力,例如:
d3.forceSimulation().force(“link”)
,则返回的是d3.forceLink()这个力。
如果没有指定 force 则返回当前仿真的对应 name 的力模型,如果没有对应的 name 则返回 undefined
。
如果要移除对应的 name 的仿真,可以为其指定 null
,比如:
simulation.force("charge", null);
- d3.forceSimulation().nodes()`,输入是一个数组,然后将这个输入的数组进行一定的数据转换。如果指定了 nodes 则将仿真的节点设置为指定的对象数组,并根据需要创建它们的位置和速度,然后 重新初始化 绑定的 力模型,并返回当前仿真。
每个 node 必须是一个对象类型,下面的几个属性将会被仿真系统添加:
index
- 节点在 nodes 数组中的索引x
- 节点当前的 x-坐标y
- 节点当前的 y-坐标vx
- 节点当前的 x-方向速度vy
- 节点当前的 y-方向速度
位置 ⟨x,y⟩ 以及速度 ⟨vx,vy⟩ 随后可能被仿真中的 力模型 修改. 如果 vx 或 vy 为 NaN, 则速度会被初始化为 ⟨0,0⟩. 如果 x 或 y 为 NaN, 则位置会按照 phyllotaxis arrangement 被初始化, 这样初始化布局是为了能使得节点在原点周围均匀分布。
如果想要某个节点固定在一个位置,可以指定以下两个额外的属性:
fx
- 节点的固定 x-位置fy
- 节点的固定 y-位置
d3.forceLink.links()
,这里输入的也是一个数组(边集),然后对输入的边集进行转换simulation.tick()
函数,按指定的迭代次数手动执行仿真,并返回仿真。这个函数对于力导向图来说非常重要,因为力导向图是不断运动的,每一时刻都在发生更新,所以需要不断更新节点和连线的位置。如果没有指定 iterations 则默认为 1,也就是迭代一次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>