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

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

为了尽可能模拟工作中使用力导图的场景,我们这里直接使用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个属性。

  • 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的技巧也会一并穿插在文章中,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容