d3.js多重力导向图多条关系线,mouseover只显示相关节点
直接上效果图
鼠标移至节点不相连节点变暗
代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<style>
.nodetext {
font-size: 12px ;
font-family: SimSun;
fill:#000000;
}
.linetext {
font-size: 12px ;
font-family: SimSun;
fill:#1f77b4;
fill-opacity:0.0;
}
.circleImg {
stroke: #ff7f0e;
stroke-width: 1.5px;
}
.tooltip{
height: auto;
font-family: simsun;
font-size: 14px;
text-align: center;
}
select#lineFontType.form-control , select#nodesFontType.form-control{
width:184px;
}
.layout-split-east {
border-left: 5px solid #E6EEF8;
}
</style>
</head>
<body>
<!-- 画图容器svg -->
<svg width="800" height="500"></svg>
<!-- 右侧属性栏 -->
<div class="panel layout-panel layout-panel-east layout-split-east" style="width: 300px; left: 847px; top: 0px;">
<div data-options="region:'east',split:true" style="width: 300px;float: right;">
<table class="table" border="0" width="100%">
<tr>
<td width="100px" align="right"><font size="2">背景颜色:</font></td>
<td width="*">
<div class="input-group">
<input onblur="showChart()" type="text" class="form-control" id="backgroundColor" value="#eee">
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">节点字体:</font></td>
<td width="*">
<div class="input-group">
<select onblur="showChart()" class="form-control" id="nodesFontType">
<option value="SimSun">宋体</option>
<option value="SimHei">黑体</option>
<option value="Microsoft Yahei">微软雅黑</option>
<option value="Microsoft JhengHei">微软正黑体</option>
<option value="KaiTi">楷体</option>
<option value="NSimSun">新宋体</option>
<option value="FangSong">仿宋</option>
</select>
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">节点字号:</font></td>
<td width="*">
<div class="input-group">
<input onblur="showChart()" type="text" class="form-control" id="nodesFontSize" value="20">
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">关系线字体:</font></td>
<td width="*">
<div class="input-group">
<select onblur="showChart()" class="form-control" id="lineFontType">
<option value="SimSun">宋体</option>
<option value="SimHei">黑体</option>
<option value="Microsoft Yahei">微软雅黑</option>
<option value="Microsoft JhengHei">微软正黑体</option>
<option value="KaiTi">楷体</option>
<option value="NSimSun">新宋体</option>
<option value="FangSong">仿宋</option>
</select>
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">关系线字号:</font></td>
<td width="*">
<div class="input-group">
<input onblur="showChart()" type="text" class="form-control" id="lineFontSize" value="15">
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">关系线颜色:</font></td>
<td width="*">
<div class="input-group">
<input onblur="showChart()" type="text" class="form-control" value="#1f77b4" id="lineColor">
</div>
</td>
</tr>
<tr>
<td width="100px" align="right"><font size="2">显示数据:</font></td>
<td width="*">
<div class="input-group">
<textarea onblur="showChart()" id="chartSeries" class="form-control" rows="3">
{
"links":[
{ "source": "陆洋","target":"建银国际产业基金管理有限公司","relation" : "副董事长" ,"sourceId":"","targetId":"","sourceImg":"http://mail.tom.com/error/i2.gif","targetImg":"","sourceColor":"#00FF00","targetColor":"","sourceRadius":"30","targetRadius":"45"},
{ "source": "汪红辉" , "target" : "乾行文化投资公司" , "relation" : "总经理" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"#00FF00","sourceRadius":"30","targetRadius":"40"},
{ "source": "汪红辉" , "target" : "天津裕丰股权投资管理有限公司" , "relation" : "董事" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"42"},
{ "source": "汪红辉" , "target" : "乾行文化投资公司" , "relation" : "法人" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"40"},
{ "source": "汪红辉" , "target" : "乾行文化投资公司" , "relation" : "董事长" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"40"},
{ "source": "汪红辉" , "target" : "建银国际产业基金管理有限公司" , "relation" : "董事" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"45"},
{ "source": "胡章宏" , "target" : "建银国际产业基金管理有限公司" , "relation" : "董事长" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"45"},
{ "source": "胡章宏" , "target" : "建银国际产业基金管理有限公司" , "relation" : "法人" ,"sourceId":"","targetId":"","sourceImg":"","targetImg":"","sourceColor":"","targetColor":"","sourceRadius":"30","targetRadius":"45"}
]
}
</textarea>
</div>
</td>
</tr>
</table>
</div>
</div>
</body>
<script>
$(document).ready(function(){
showChart();
})
function showChart(){
var backgroundColor = $("#backgroundColor").val();
var nodesFontType = $("#nodesFontType").val();
var nodesFontSize = $("#nodesFontSize").val();
var lineFontType = $("#lineFontType").val();
var lineFontSize = $("#lineFontSize").val();
var lineColor = $("#lineColor").val();
var data = $("#chartSeries").val();
data = JSON.parse(data);
drawChart(backgroundColor,nodesFontType,nodesFontSize,lineFontType,
lineFontSize,lineColor,data);
}
function drawChart(backgroundColor,nodesFontType,nodesFontSize,lineFontType,
lineFontSize,lineColor,data){
var radius = 40; //圆形半径
var width = 800;
var height = 500;
var svgChart = d3.select("svg");
svgChart.remove();
var links = data.links;
//关系分组
var linkGroup = {};
//对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
var linkmap = {};
for(var i=0; i<links.length; i++){
var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
if(!linkmap.hasOwnProperty(key)){
linkmap[key] = 0;
}
linkmap[key]+=1;
if(!linkGroup.hasOwnProperty(key)){
linkGroup[key]=[];
}
linkGroup[key].push(links[i]);
}
//为每一条连接线分配size属性,同时对每一组连接线进行编号
for(var i=0; i<links.length; i++){
var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
links[i].size = linkmap[key];
//同一组的关系进行编号
var group = linkGroup[key];
var keyPair = key.split(':');
var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体
if(keyPair[0]==keyPair[1]){
type = 'self';
}
//给节点分配编号
setLinkNumber(group,type);
}
var nodes = {};
for(var i = 0; i < links.length; i++){
links[i].source = nodes[links[i].source] || (nodes[links[i].source] = {name: links[i].source,color:links[i].sourceColor,image:links[i].sourceImg,radius:links[i].sourceRadius});
links[i].target = nodes[links[i].target] || (nodes[links[i].target] = {name: links[i].target,color:links[i].targetColor,image:links[i].targetImg,radius:links[i].targetRadius});
}
nodes = d3.values(nodes);
//绑定相连节点
for(var i = 0; i < nodes.length; i++){
for(var j = 0; j < links.length; j++){
if(nodes[i].name == links[j].source.name){
nodes[i][links[j].target.name] = {name: links[j].target.name};
}
if(nodes[i].name == links[j].target.name){
nodes[i][links[j].source.name] = {name: links[j].source.name};
}
}
}
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height)
.attr("style","background-color:" + backgroundColor);
//D3力导向布局
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width,height])
.linkDistance(200)
.charge(-1500)
.start();
//箭头
/* var arrowMarker = svg.append("marker")
.attr("id","arrow")
.attr("markerUnits","strokeWidth")
.attr("markerWidth","16")
.attr("markerHeight","15")
.attr("viewBox","0 0 12 12")
.attr("refX","30")
.attr("refY","6")
.attr("orient","auto");
.append("path")
.attr("d","M2,2 L10,6 L2,10 L6,6 L2,2")
.attr("fill",lineColor); */
/* var arrowMarker = svg.append("marker")
.attr("id","arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 55)
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 16)
.attr("markerUnits","userSpaceOnUse")
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.attr('fill',lineColor); */
//边
var edges_path = svg.selectAll(".edgepath")
.data(links)
.enter()
.append("path")
.attr("marker-end",function(d,i){
var arrowMarker = svg.append("marker")
.attr("id","arrow"+i)
.attr("markerUnits","userSpaceOnUse")
.attr("markerWidth","16")
.attr("markerHeight","15")
.attr("viewBox","0 0 12 12")
.attr("refX",d.targetRadius)
.attr("refY","6")
.attr("orient","auto")
.append("svg:path")
.attr("d","M2,2 L10,6 L2,10 L6,6 L2,2")
.attr("fill",lineColor);
return "url(#arrow" + i + ")";
})
.style("stroke",lineColor)
.style("stroke-width",1.5)
.style("fill","none");
//边上的文字(人物之间的关系)
var edges_text = svg.selectAll(".linetext")
.data(links)
.enter()
.append("text")
.attr("fill-opacity",1)
.style("font-size",(lineFontSize+"px"))
.style("font-family",lineFontType)
.style("fill",lineColor)
.text(function(d){
return d.relation;
});
// 圆形图片节点(人物头像)
var circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", "circleImg")
.attr("r", function(d){
return d.radius;
})
.attr("fill", function(d,i){
//节点图片不为空是添加背景色
if(d.image == ""){
if(d.color == ""){
return "#eee";
}
return d.color;
}else{
//创建圆形图片
var defs = svg.append("defs").attr("id", "imgdefs")
var catpattern = defs.append("pattern")
.attr("id", "catpattern" + i)
.attr("height", 1)
.attr("width", 1)
catpattern.append("image")
/* .attr("x", - (img_w / 2 - radius))
.attr("y", - (img_h / 2 - radius)) */
.attr("width", radius*2)
.attr("height", radius*2)
.attr("xlink:href", d.image)
return "url(#catpattern" + i + ")";
}
})
.on("mouseover",function(d,i){
//影藏其它连线上文字
edges_text.style("fill-opacity",function(edge){
if( edge.source === d || edge.target === d ){
return 1;
}
if( edge.source !== d && edge.target !== d ){
return 0;
}
})
//其它节点亮度调低
circle.style("opacity",function(edge){
var v = d.name;
if( edge.name == v || (edge[v] != undefined && edge[v].name == v)){
return 1;
}else{
return 0.2;
}
})
//其他连先亮度调低
edges_path.style("opacity",function(edge){
if( edge.source === d || edge.target === d ){
return 1;
}
if( edge.source !== d && edge.target !== d ){
return 0.2;
}
})
//其他节点文字亮度调低
nodes_text.style("opacity",function(edge){
var v = d.name;
if( edge.name == v || (edge[v] != undefined && edge[v].name == v)){
return 1;
}else{
return 0.2;
}
})
})
.on("mouseout",function(d,i){
//显示连线上的文字
edges_text.style("fill-opacity",1);
edges_path.style("opacity",1);
circle.style("opacity",1);
nodes_text.style("opacity",1);
tooltip.style("opacity",0.0);
})
.call(force.drag);
var tooltip = d3.select("body").append("div")
.attr("class","tooltip")
.attr("opacity",0.0);
var nodes_text = svg.selectAll(".nodetext")
.data(nodes)
.enter()
.append("text")
.style("font-size",(nodesFontSize+"px"))
.style("font-family",nodesFontType)
.attr('x',function(d){
var name = d.name;
//如果小于四个字符,不换行
if(name.length < 4){
d3.select(this).append('tspan')
.attr("dx",-nodesFontSize*(name.length/2))
.text(function(){return name;});
}else if(name.length >= 4 && name.length <= 6) {
var top=d.name.substring(0,3);
var bot=d.name.substring(3,name.length);
d3.select(this).append('tspan')
.attr("dx",-nodesFontSize*1.5)
.attr("dy",-nodesFontSize*0.5)
.text(function(){return top;});
d3.select(this).append('tspan')
.attr("dx",-(nodesFontSize*name.length/2))
.attr("dy",nodesFontSize)
.text(function(){return bot;});
}else if(name.length > 7){
var top=d.name.substring(0,3);
var mid=d.name.substring(3,6);
var bot=d.name.substring(6,name.length);
d3.select(this).append('tspan')
.attr("dx",-nodesFontSize*1.5)
.attr("dy",-nodesFontSize*0.5)
.text(function(){return top;});
d3.select(this).append('tspan')
.attr("dx",-nodesFontSize*3)
.attr("dy",nodesFontSize)
.text(function(){return mid;});
d3.select(this).append('tspan')
.attr("dx",-nodesFontSize*2)
.attr("dy",nodesFontSize)
.text(function(){return "...";});
}
})
.on("mouseover",function(d,i){
//影藏其它连线上文字
edges_text.style("fill-opacity",function(edge){
if( edge.source === d || edge.target === d ){
return 1;
}
if( edge.source !== d && edge.target !== d ){
return 0;
}
})
//其他节点亮度调低
circle.style("opacity",function(edge){
var v = d.name;
if( edge.name == v || (edge[v] != undefined && edge[v].name == v)){
return 1;
}else{
return 0.2;
}
})
//其他连线亮度调低
edges_path.style("opacity",function(edge){
if( edge.source === d || edge.target === d ){
return 1;
}
if( edge.source !== d && edge.target !== d ){
return 0.2;
}
})
//其他节点文字亮度调低
nodes_text.style("opacity",function(edge){
var v = d.name;
if( edge.name == v || (edge[v] != undefined && edge[v].name == v)){
return 1;
}else{
return 0.2;
}
})
tooltip.html("<span>"+d.name+"</span>")
.style("left",(d3.event.pageX)+"px")
.style("top",(d3.event.pageY+20)+"px")
.style("display","block")
.style("opacity",1.0);
})
.on("mouseout",function(d,i){
//显示连线上的文字
edges_text.style("fill-opacity",1);
edges_path.style("opacity",1);
circle.style("opacity",1);
nodes_text.style("opacity",1);
tooltip.style("opacity",0.0);
})
.call(force.drag);
force.on("tick", function(){
//限制结点的边界
nodes.forEach(function(d,i){
d.x = d.x - radius < 0 ? radius : d.x ;
d.x = d.x + radius > width ? width - radius : d.x ;
d.y = d.y - radius < 0 ? radius : d.y ;
d.y = d.y + radius > height ? height - radius : d.y ;
});
edges_path.attr("d", function(d) {
//如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
if(d.target==d.source){
dr = 30/d.linknum;
return"M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y+1);
}else if(d.size%2!=0 && d.linknum==1){//如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
return 'M'+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y;
}
//根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
//注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
var curve=1.5;
var homogeneous=1.2;
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx*dx+dy*dy)*(d.linknum+homogeneous)/(curve*homogeneous);
//当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
if(d.linknum<0){
dr = Math.sqrt(dx*dx+dy*dy)*(-1*d.linknum+homogeneous)/(curve*homogeneous);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,0 " + d.target.x + "," + d.target.y;
}
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
//更新连接线上文字的位置
edges_text.attr("x",function(d){
return (d.source.x + d.target.x) / 2 + d.linknum*10; });
edges_text.attr("y",function(d){
return (d.source.y + d.target.y) / 2 + d.linknum*15; });
//更新结点图片和文字
circle.attr("cx",function(d){ return d.x });
circle.attr("cy",function(d){ return d.y });
nodes_text.attr("x",function(d){ return d.x });
nodes_text.attr("y",function(d){ return d.y });
});
}
function setLinkNumber(group,type){
if(group.length==0) return;
//对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
var linksA = [], linksB = [];
for(var i = 0;i<group.length;i++){
var link = group[i];
if(link.source < link.target){
linksA.push(link);
}else{
linksB.push(link);
}
}
//确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
//特殊情况:当关系都是连接到同一个实体时,不平分
var maxLinkNumber = 1;
if(type=='self'){
maxLinkNumber = group.length;
}else{
maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;
}
//如果两个方向的关系数量一样多,直接分别设置编号即可
if(linksA.length==linksB.length){
var startLinkNumber = 1;
for(var i=0;i<linksA.length;i++){
linksA[i].linknum = startLinkNumber++;
}
startLinkNumber = 1;
for(var i=0;i<linksB.length;i++){
linksB[i].linknum = startLinkNumber++;
}
}else{//当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
//如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
var biggerLinks,smallerLinks;
if(linksA.length>linksB.length){
biggerLinks = linksA;
smallerLinks = linksB;
}else{
biggerLinks = linksB;
smallerLinks = linksA;
}
var startLinkNumber = maxLinkNumber;
for(var i=0;i<smallerLinks.length;i++){
smallerLinks[i].linknum = startLinkNumber--;
}
var tmpNumber = startLinkNumber;
startLinkNumber = 1;
var p = 0;
while(startLinkNumber<=maxLinkNumber){
biggerLinks[p++].linknum = startLinkNumber++;
}
//开始负编号
startLinkNumber = 0-tmpNumber;
for(var i=p;i<biggerLinks.length;i++){
biggerLinks[i].linknum = startLinkNumber++;
}
}
}
</script>
</html>