先上图再解释。
所谓力学图(Force),就是用点和线来表示各点之间的关系。比如上面这张错综复杂的关系,就是由一个json数组,然后利用D3画图来得到的。
之前学D3,一直停留在给他数据,然后用layout布局,再用不同的图表的格式来画图就好了。而数据,不是自己写死的,就是从一个json文件或者csv文件读出来的。这样显然不实用,我们实际用到的数据,多数是从数据库读出来的。于是上周就接到了任务,用动态数据来画D3的Force图。
做之前思考了一下,任务的重点就是构造数据。把数据库中读出来的数据构造为标准的json格式提供给D3的接口。说白了,就是字符串的拼接。
还有一个问题是根据要求,从数据库中查数据。这个就需要一定的mysql基础,这里不详细说了。
下面看一下我纠结了很久的代码吧。
<?php
$con= mysql_connect("localhost","root","root");
if(!$con)
{
die('Could not connect: ' . mysql_error());
}
mysql_select_db("test", $con);
mysql_query('set names utf8');
$des = isset($_REQUEST['Target'])?($_REQUEST['Target']):"";
$sql = "select distinct Target,IPaddress from tknifelog where Target= '{$des}' ";
$query = mysql_query($sql);
while($result= mysql_fetch_assoc($query)){
$data[]=$result;
}
//一层关系
$p=0;
$source = 0;
$source1 = 0;
foreach ($data as $row ){
$Target = $row['Target'];
$IPaddress = $row['IPaddress'];
//nodes0
if($p++==0){
$node[]= '{"name":"'.$Target.'","group":1}';
}
$node[]= '{"name":"'.$IPaddress.'","group":1}';
//links0
$source++;
$link .= '{"source":'.$source.',"target":0,"value":'.rand(1,10).'},';
$target = $source;
$source1= $source;
//二层关系
$sql1 = "select distinct Target,IPaddress from tknifelog where IPaddress = '{$IPaddress}'";
$query1 = mysql_query($sql1);
while ($result1 = mysql_fetch_assoc($query1)) {
$data1[] = $result1;
}
foreach ($data1 as $row1) {
$Target1 = $row1['Target'];
$IPaddress1 = $row1['IPaddress'];
//nodes1
if (!in_array('{"name":"'.$Target1.'","group":2}',$node)) {
$node[]= '{"name":"'.$Target1.'","group":2}';
//links1
$source1++;
$link .= '{"source":'.$source1.',"target":'.$target.',"value":'.rand(1,10).'},';
$source++;
}
}
}
$json_n = json_encode($node);
$nodes = '{"nodes":'. $json_n ;
$json_l= json_encode($link);
$links = '"links":['.$json_l.']}';
$json_f = $nodes.$links;
$new1 = str_replace('\"','"',$json_f);
$new2 = str_replace('"{','{',$new1);
$new3 = str_replace('}"','}',$new2);
$new4 = str_replace('}]"','}],"', $new3);
$json = str_replace('},"]}','}]}', $new4);
file_put_contents("force.json",$json);
// echo "json文件已生成";
?>
<html>
<head>
<meta charset="utf-8">
<title>Force</title>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
<style>
.link {
stroke: #666;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
.container {
margin-top: 30px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-3">
<table class="table">
<thead>
<tr>
<th>Target</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<?php
foreach($data as $row){
?>
<tr>
<td><a href="test.php?Target=<?php echo $row['Target']?>"><?php echo $row['Target']; ?></a></td>
<td><?php echo $row['IPaddress']; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<div class="col-md-9 graph"></div>
</div>
</div>
<script src="js/d3.v3.js" charset="utf-8"></script>
<script>
var width = 800;
var height = 600;
var img_w = 17;
var img_h = 17;
var color = d3.scale.category20();
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var root = d3.select(".graph").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var svg = root.append('svg:g');
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("force.json", function(error, json) {
if (error) throw error;
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); });
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
var drag =force.drag()
.on("dragstart",function(d,i){
d3.event.sourceEvent.cancelBubble = true;
d.fixed = true; //拖拽开始后设定被拖拽对象为固定
});
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; });
//限制结点的边界
json.nodes.forEach(function(d,i){
d.x = d.x - img_w/2 < 0 ? img_w/2 : d.x ;
d.x = d.x + img_w/2 > width ? width - img_w/2 : d.x ;
d.y = d.y - img_h/2 < 0 ? img_h/2 : d.y ;
d.y = d.y + img_h/2 > height ? height - img_h/2 : d.y ;
});
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("dblclick",function(d,i){
d.fixed = false;
})
.call(drag);
});
});
function zoomed () {
svg.attr("transform",
"translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
} ;
</script>
</body>
</html>
首先,我用的数据源,是一些源和目的地址,第一层关系是查数据库中有多少不同的目标地址,列出来。(代码中没有显示这一部分)点击相应的目标地址,可以展示出数据库中和他有关联的源地址,然后以该目标地址为中心,源地址为他的下一层关系连接。第二层关系是查出源地址有关的目标地址(去重)。
画出的图是这样的:
前面的php代码是用来查数据,然后看到查出的数据构造出了nodes和links,这是force图要求的数据格式。构造的过程中可能会遇到很多问题,用字符串替换等函数来变换到需要的格式。
在第二层关系时,要注意links中target和source的关系,和他们与第一层的关系。(脑袋不好想了半天才理清楚啊真是要被自己蠢哭了)
最后把构造好的字符串生成一个json文件备用。
接下来就是重点,用D3画图了。
前面介绍过,引入D3的库就可以用D3的函数画图了!
先定义一些必要的参数,宽高什么的。(这里图片的宽高是这样的,第一版代码的时候节点是一个小爱心的图片,在限制节点边界的时候为了让图整个都在页面内,就要用到图片的宽高。但是为了更清楚的显示节点的层级,后来就用了不同group不同颜色的点点来表示。懒了就没有改图片宽高这里。)
接着定义颜色,用D3中定义好的20中随机的颜色组。
zoom是一个放大平移的函数。
接下来开始画图。
在html代码中有一个class是graph的div,我们的svg就要画在这上面。接着添加属性。重点:要再嵌套一层svg:g,因为在zoom起作用的时候,为了让他对整体放大或者平移而不是只对其中的点或者线作用,就要把点线这些元素放在一个容器中,然后对这个容器调用zoom。
d3.layout.force()是力学图的布局,下面的参数根据需要填写就是了。
接下来导入数据,把php生成的json数据导入。注意:接下来画图的时候就要把下面的内容放在数据的作用域中。
force的nodes和links是json中的nodes和links。接下来就是画边和点,注意到点还调用了drag。
节点用circle表示,填充定义好的颜色。
在节点上添加文字,就是节点中的name。
再然后就是拖拽函数。这里有一句是防止冒泡事件。也就是拖拽这件事节点也有,zoom事件也有,单击时就会有问题。所以用这句代码来防止冒泡事件。
下一句是拖拽结束后固定顶点位置。
下面是更新图的位置。就是在拖拽之后。还要限制节点的边界。最后是双击固定的节点之后可以解除固定。
在数据作用域的外面是zoom函数。
这样就画出了如上的D3图。
对了,展示一下json的格式。
{"nodes":[
{"name":"180.149.153.216:443","group":1},
{"name":"3062634345","group":1},
{"name":"180.149.153.216:443","group":2},
{"name":"3062635806","group":1},
{"name":"3723292305","group":1},
{"name":"1987589676","group":1},
{"name":"1987589733","group":1},
{"name":"1987591229","group":1},
{"name":"3062644032","group":1},
{"name":"1987584314","group":1},
{"name":"1987585124","group":1},
{"name":"3062640123","group":1},
{"name":"1857966460","group":1},
{"name":"1857967040","group":1},
{"name":"1987771852","group":1},
{"name":"3723291694","group":1},
{"name":"2101543622","group":1},
{"name":"180.149.139.248:443","group":2}],
"links":[
{"source":1,"target":0,"value":9},
{"source":2,"target":1,"value":7},
{"source":3,"target":0,"value":8},
{"source":4,"target":0,"value":10},
{"source":5,"target":0,"value":1},
{"source":6,"target":0,"value":2},
{"source":7,"target":0,"value":4},
{"source":8,"target":0,"value":6},
{"source":9,"target":0,"value":1},
{"source":10,"target":0,"value":9},
{"source":11,"target":0,"value":6},
{"source":12,"target":0,"value":3},
{"source":13,"target":0,"value":4},
{"source":14,"target":0,"value":8},
{"source":15,"target":0,"value":7},
{"source":16,"target":0,"value":8},
{"source":17,"target":16,"value":1}]}
嗯,先写到这吧。以后有什么再补充。