数据可视化让前端飞Web前端之路

深入浅出d3.js数据可视化之道(7)

2017-11-21  本文已影响119人  金字笙调

今天给大家带来的是比较常见的力导图。力导图的应用场景很多,表示关系连接的很多都可以用力导图来实现,比如企业投资关系,汽车行程路线等等。

为了尽可能模拟工作中使用力导图的场景,我们这里直接使用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属性字段来进行各个节点的连接,也可以把它设置成实际项目的中的字段。

经过转换 nodes 值
和初始值对比,转换过后多了5个属性。

把节点和连接加入到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的技巧也会一并穿插在文章中,敬请期待。
上一篇下一篇

猜你喜欢

热点阅读