d3.js 绘制力导向图

简介

D3.js是一个JavaScript库,它可以通过数据来操作文档。D3可以通过使用HTML、SVG和CSS把数据鲜活形象地展现出来。D3严格遵循Web标准,因而可以让你的程序轻松兼容现代主流浏览器并避免对特定框架的依赖。同时,它提供了强大的可视化组件,可以让使用者以数据驱动的方式去操作DOM。

使用

在 HTML 文件中引入:

<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

确定初始数据

初始数据为节点数据 nodes 和 连线数组 links,如下:

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
var nodes = [{'label': 'Website', 'title': 'ebay'}, {'label': 'Sort', 'title': 'Antiques'}, {
'label': 'Sort',
'title': 'Baby'
}, {'label': 'Sort', 'title': 'Books'}, {'label': 'Sort', 'title': 'Business & Industrial'}, {
'label': 'Sort',
'title': 'Cameras & Photo'
}, {'label': 'Sort', 'title': 'Cell Phones & Accessories'}, {
'label': 'Sort',
'title': 'Clothing, Shoes & Accessories'
}, {'label': 'Sort', 'title': 'Coins & Paper Money'}, {'label': 'Sort', 'title': 'Collectibles'}, {
'label': 'Sort',
'title': 'Computers Tablets & Networking'
}, {'label': 'Sort', 'title': 'Consumer Electronics'}, {'label': 'Sort', 'title': 'Crafts'}, {
'label': 'Sort',
'title': 'eBay Motors'
}, {'label': 'Sort', 'title': 'Everything Else'}, {'label': 'Sort', 'title': 'Health & Beauty'}, {
'label': 'Sort',
'title': 'Home & Garden'
}, {'label': 'Sort', 'title': 'Jewelry & Watches'}, {
'label': 'Sort',
'title': 'Musical Instruments & Gear'
}, {'label': 'Sort', 'title': 'Sporting Goods'}];
var links = [{'source': 1, 'target': 0}, {'source': 2, 'target': 0}, {'source': 3, 'target': 0}, {
'source': 4,
'target': 0
}, {'source': 5, 'target': 0}, {'source': 6, 'target': 0}, {'source': 7, 'target': 0}, {
'source': 8,
'target': 0
}, {'source': 9, 'target': 0}, {'source': 10, 'target': 0}, {'source': 11, 'target': 0}, {
'source': 12,
'target': 0
}, {'source': 13, 'target': 0}, {'source': 14, 'target': 0}, {'source': 15, 'target': 0}, {
'source': 16,
'target': 0
}, {'source': 17, 'target': 0}, {'source': 18, 'target': 0}, {'source': 19, 'target': 0}];

创建布局

1
2
var force = d3.layout.force()
.charge(-300).linkDistance(130).size([width, height]);
  • d3.layout.force() 创建一个力导向图布局。
  • charge() 设定节点的电荷数,负数则排斥,正数则吸引。
  • linkDistance() 设置连线的距离。
  • size([x, y]) 设置力导向图的作用范围,用于指定重力中心为(x/2,y/2),所有节点的初始位置限定为 [0,x] 和 [0,y] 之间。

创建 SVG

1
2
var svg = d3.select("#graph").append("svg")
.attr("width", "100%").attr("height", "380px");
  • append() 用于添加元素
  • 设置 svg 的长宽

转换数据和绘制

  • 设定节点数组和连线数组之后开启布局计算

    1
    force.nodes(nodes).links(links).start();

  • 绘制 绑定数组 nodes 和 links,分别添加节点的元素 <circle> 和连线的元素<line>。另外,还要添加文字元素 <text>。各元素的 CSS 样式分别为:node,link,nodeText。代码如下:

    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
    var link = svg.selectAll(".link")
    .data(links);
    link.enter()
    .insert("line", ".link")
    .attr("class", "link");
    var node = svg.selectAll(".node")
    .data(nodes);
    node.enter()
    .append("circle")
    .attr("class", function (d) {
    return "node " + d.label
    })
    .attr("r", 25)
    .call(force.drag);
    var text = svg.selectAll(".nodeText")
    .data(nodes);
    text.enter()
    .append("text")
    .attr("class", "nodeText")
    .attr("x", function (d) {
    return d.x;
    })
    .attr("font-size", "10px")
    .attr("text-anchor", "middle")
    .attr("dy", ".3em")
    .attr("y", function (d) {
    return d.y;
    }).text(function (d) {
    var act_title;
    if (d.title.length > 8) {
    act_title = d.title.substring(0, 8) + "...";
    return act_title;
    }
    else {
    return d.title;
    }
    });
    • 节点的选择集调用了 call(force.drag),可以让节点支持鼠标拖拽。
    • 连线的选择集调用了 insert("line", ".link"),可以确保力导向图进行更新(移除节点后增加节点)之后连线不会覆盖在节点上。
  • 在拖动节点之后,图形元素的坐标会发生变化,所以需要设置一个监听器进行更新,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    force.on("tick", function () {
    link.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;
    });

    node.attr("transform", function (d) {
    return "translate(" + d.x + "," + d.y + ")";
    });
    text.attr("x", function (d) {
    return d.x;
    })
    .attr("y", function (d) {
    return d.y;
    });
    });

     * `force.on()` 可为三种事件设定监听器,start,tick,end。其中,start 是刚开始运动,end 是运动停止,tick 是表示运动的每一步。
     * 选择集 node,link,text 上都绑定了数据,当每一次 tick 事件发生时,被绑定的数据被更新,`function(d)` 中的 d 也都更新了。

    增加样式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     <style type="text/css">
    .node {
    stroke: #222;
    stroke-width: 1.5px;
    }

    .link {
    stroke: #707071;
    stroke-opacity: .6;
    stroke-width: 1px;
    }

    .node.Website {
    fill: #ff756e;
    }

    .node.Sort {
    fill: #de9bf9;
    }

    .nodeText {
    fill: #ffffff
    }
    </style>

    效果
    效果

绘制箭头和添加文字

  • 绘制箭头需要用到 SVG 中的标记(marker)。标记 写在 中,defs 用于定义可重复利用的元素。定义箭头代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var defs = svg.append("defs");
    var arrowMarker = defs.append("marker")
    .attr("id", "arrow")
    .attr("markerUnits", "strokeWidth")
    .attr("markerWidth", "10")
    .attr("markerHeight", "10")
    .attr("viewBox", "-0 -5 10 10")
    .attr("refX", "35")
    .attr("refY", "0")
    .attr("orient", "auto");

    var arrow_path = 'M 0,-5 L 10 ,0 L 0,5 L5,0 L0,-5';

    arrowMarker.append("path")
    .attr("d", arrow_path)
    .attr("fill", "#a6a6a6");
    • refX, refY 指的是图形元素和 marker 连接的位置坐标,这里圆的半径设置为 25,箭头的顶点到底部垂直距离为 10,所以设置为(35,0)则箭头的顶端刚好在圆上。
    • markerUnits 标记大小的基准,有两个值,即 strokeWidth(线的宽度)和 userSpaceOnUse(线前端的大小)。
    • markerWidth,markerHeight 标识的大小。
    • orient 绘制方向,可设定为 auto(自动确认方向和角度值)。
    • id 标识的 id 号。
    • 粉红色框是 viewBox 范围,黑色为 arrow_path 的轨迹: arro
    • link.attr('marker-end', 'url(#arrow)');可以为连接线添加箭头。
  • 为连接线增加文字,首先需要定义 <path> 确定文字放在连接线路径上,再定义 <text>,之后在 <text> 添加 <textPath> 引用路径,代码如下:

    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
    //定义文字路径
    var linkpaths = svg.selectAll(".linkpath")
    .data(links_data);
    linkpaths.enter()
    .append('path')
    .attr('d', function (d) {
    return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
    })
    .attr('class', 'linkpath')
    .attr('fill-opacity', 0)
    .attr('stroke-opacity', 0)
    .attr('fill', '#ffffff')
    .attr('stroke', 'red')
    .attr('id', function (d, i) {
    return 'linkpath' + i;
    })
    .style("pointer-events", "none");

    //定义 <text>
    var linklabels = svg.selectAll(".linklabel")
    .data(links_data);
    linklabels.enter()
    .append('text')
    .style("pointer-events", "none")
    .attr('class', 'linklabel')
    .attr('id', function (d, i) {
    return 'linklabel' + i;
    })
    .attr('dx', 50)
    .attr('dy', 0)
    .attr('font-size', 5)
    .attr('fill', '#aaa');
    //增加 <textPath> 引用 path
    linklabels.append('textPath')
    .attr('xlink:href', function (d, i) {
    return '#linkpath' + i
    })
    .style("pointer-events", "none")
    .text(function (d, i) {
    return 'BELONG';
    });
    • pointer-events 设置为 none 则不在接收鼠标事件,设置为 all 则在指针在元素中或边缘时接收鼠标事件。
    • 上述代码只是确保了初始时文字在连接线路径上,所以还需要设置对上述元素的坐标更新。在 force.on() 中增加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    linkpaths.attr('d', function (d) {
    var path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
    return path
    });

    linklabels.attr('transform', function (d, i) {
    if (d.target.x < d.source.x) {
    bbox = this.getBBox();
    //确保文字不会倒置
    rx = bbox.x + bbox.width / 2;
    ry = bbox.y + bbox.height / 2;
    return 'rotate(180 ' + rx + ' ' + ry + ')';
    }
    else {
    return 'rotate(0)';
    }
    });
    ### 缩放和拖动 要同时支持鼠标缩放以及拖动,必须处理这两者的事件冲突,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", function zoomed() {
d3.select(this).attr("transform",
"translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
});
svg.call(d3.behavior.zoom().on("zoom", redraw))
.append('g');
force.drag()
.on("dragstart", function (d) {
//在拖动节点的时候阻止事件冒泡
d3.event.sourceEvent.stopPropagation();
});
// 缩放之后重绘
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}

function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
  • g 元素 是用来分组用的,它能把多个元素放在一组里,对 标记实施的样式和渲染会作用到这个分组内的所有元素上。组内的所有元素都会继承 标记上的所有属性。
  • fixed 设置为 true 时,顶点固定不动。

更新数据

有时候需要更新数据,更新后力导向图也会跟着变化。由于上述的选择集通过 data() 将数据与元素进行绑定,而绑定的情况分为以下三种: * update:数组长度 = 元素数量 * enter:数组长度 > 元素数量 * exit:数组长度 < 元素数量 所以在更新数据的时候需要分别对这三种情况进行处理,对于 enter 这种没有足够的元素的处理办法是添加元素,对于 exit 这种存在多余元素的处理办法是删除元素,而对于 update 则是进行内容修改。 以节点的更新数据为例:

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
var node = svg.selectAll(".node")
.data(nodes_data);
//节点数据内容修改
node.attr("class", function (d) {
return "node " + d.label
})
.on("dblclick", dblclick)
.attr("r", 25)
.call(force.drag)
.on("click", function (d) {
keys = d3.keys(d);
delete_key = ["x", "y", "index", "weight", "px", "py", "fixed"];
info = [];
for (var i = 0; i < delete_key.length; i++) {
keys.removeByValue(delete_key[i]);
}
for (var j = 0; j < keys.length; j++) {
info.push(d[keys[j]])
}
showinfo(d.label, keys, info);
});
//增加不足的元素
node.enter()
.append("circle")
.attr("class", function (d) {
return "node " + d.label
})
.on("dblclick", dblclick)
.attr("r", 25)
.call(force.drag)
.on("click", function (d) {
keys = d3.keys(d);
delete_key = ["x", "y", "index", "weight", "px", "py", "fixed"];
info = [];
for (var i = 0; i < delete_key.length; i++) {
keys.removeByValue(delete_key[i]);
}
for (var j = 0; j < keys.length; j++) {
info.push(d[keys[j]])
}
showinfo(d.label, keys, info);
});
//删除多余元素
node.exit().remove();

参考资料

理解 update, enter, exit 的使用