今天给大家带来的是比较常见的力导图。力导图的应用场景很多,表示关系连接的很多都可以用力导图来实现,比如企业投资关系,汽车行程路线等等。
为了尽可能模拟工作中使用力导图的场景,我们这里直接使用json 数据来完成今天力导图的制作。
模拟数据 和 初始化画布
const width = 500,
height = 500;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var nodes = [
{ name: "XiaMen" },
{ name: "BeiJing"},
{ name: "XiAn" },
{ name: "HangZhou"},
{ name: "ShangHai"},
{ name: "QingDao"},
{ name: "NanJing"},
{ name: "QueShan"}
];
var links = [
{ source : 'BeiJing' , target: "XiaMen" } ,
{ source : 'BeiJing' , target: "XiAn" } ,
{ source : 'BeiJing' , target: "XiaMen" } ,
{ source : 'BeiJing' , target: "HangZhou" } ,
{ source : 'BeiJing' , target: "ShangHai" } ,
{ source : 'BeiJing' , target: "QingDao" } ,
{ source : 'BeiJing' , target: "NanJing" },
{ source : 'QueShan' , target: "XiaMen" } ,
{ source : 'QueShan' , target: "XiAn" } ,
{ source : 'QueShan' , target: "XiaMen" } ,
{ source : 'QueShan' , target: "HangZhou" } ,
{ source : 'QueShan' , target: "ShangHai" } ,
{ source : 'QueShan' , target: "QingDao" } ,
{ source : 'QueShan' , target: "NanJing" }
];
通过布局将原始数据转化为我们便于绘制力导图的格式。
通过内置的函数,我们将数据进行转化,d3 v3到v4,由之前的d3.layout.force()变成了现在的d3.forceSimulation().
var simulation = d3.forceSimulation(nodes) // 根据指定的节点数组创建一个没有作用力的仿真
.force("link", d3.forceLink(links).distance(100).strength(1).id((d)=>d.name)) // 连线作用力
.force("charge",d3.forceManyBody()) // 节点间的作用力
.force("center",d3.forceCenter(width / 2, height / 2)); //重力,布局的参考位置,力导向图的中心点
具体这几个api 可以查看这里,在这里着重提一个 id(d) => d.name,这里使用name属性字段来进行各个节点的连接,也可以把它设置成实际项目的中的字段。
和初始值对比,转换过后多了5个属性。
- index 节点的索引
- x 节点当前x坐标
- y 节点当前y坐标
- vx 节点当前x速度
- vy 节点当前y速度
把节点和连接加入到dom中
var color = d3.scaleOrdinal(d3.schemeCategory20);
/ 绘制连线
var svg_links = svg.selectAll("line")
.data(links)
.enter()
.append("line")
.style("stroke","#ccc")
.style("stroke-width",1)
// 绘制节点
var svg_nodes = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r",10)
.style("fill",function(d,i){
return color(i);
})
.attr("cx",function(d){return d.x;})
.attr("cy",function(d){return d.y;})
// 绘制文字
var svg_text = svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.style("fill","#000")
.attr('font-size', '12px')
.attr("dx",0)
.attr("dy",20)
.attr('text-anchor', 'middle')
.text(function(d){return d.name;});
在进行到这一步时,当前画布上的所有节点和线和文字都堆在左上角,这是由于此时采用了初始化布局计算的位置,接下来,添加tick方法,将其计算并调整到合适的布局。
function draw(){
svg_nodes
.attr("cx",function(d){return d.x;})
.attr("cy",function(d){return d.y;});
svg_text
.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y; });
svg_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; });
}
simulation.on("tick",draw);
力导向图布局的形成是一个异步的过程,而且需要一定时间。而tick会在节点增加减少,事件操作时都会触发,实时计算。
添加拖动
接下来,给力导向图添加拖拽事件。
var svg_nodes = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r",10)
.style("fill",function(d,i){
return color(i);
})
.attr("cx",function(d){return d.x;})
.attr("cy",function(d){return d.y;})
.call(d3.drag().on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.8).restart(); // 在交互时重新启动仿真,比如拖拽了某个节点或使用simulation.stop暂停仿真之后进行重新调整布局
d.fx = d.x;
d.fy = d.y;
};
function dragged(d) {
d.fx = d3.event.x; // 设置当前位置
d.fy = d3.event.y;
};
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0); // 阻止仿真继续计算位置
d.fx = null;
d.fy = null;
};
d3.event.active代表的是除去当前事件,当前正在发生的拖动事件的个数。在dragStart的时候,如果没有其他的拖拽事件,那么d3.event.active的将会是 0,仿真模拟计算将会被启动,各个点的位置将依次被计算;同样的道理,如果在dragended的时候,d3.event.active的如果是 0,说明计算的是最后一个点,此时可以关闭仿真模拟,不再计算。
事实上,如果不在每次拖拽过后手动关闭仿真模拟,那么计算将会一直持续下去,再也不能完成第二次拖拽。而在拖拽开始时不判断开启仿真模拟,那么你一次拖动也不能完成。
接下来,我们可以给他们添加一些箭头
//添加defs标签
var defs = svg.append("defs");
//添加marker标签及其属性
var arrowMarker = defs.append("marker")
.attr("id","arrow")
.attr("markerUnits","strokeWidth")
.attr("markerWidth",12)
.attr("markerHeight",12)
.attr("viewBox","0 0 12 12")
.attr("refX",20)
.attr("refY",6)
.attr("orient","auto")
//绘制直线箭头
var arrow_path = "M2,2 L10,6 L2,10 L6,6 L2,2";
arrowMarker.append("path")
.attr("d",arrow_path)
.attr("fill","red")
源码地址在这里,目前正在学习webgl,希望有时间能够和大家分享一下。接下来将会继续更新d3以及svg相关的知识,同时在之前一段时间内掌握到的操纵svg的技巧也会一并穿插在文章中,敬请期待。