ECharts关系图,节点可拖拽,添加节点和边的点击事件
最近在做一个neo4j前端可视化的任务,对比了ECharts.js,Cytoscape.js和D3.js几个前端可视化工具,最终选择了ECharts.js,百度的一款开源工具,简单好用,比较容易上手。后台采用Python的Django框架,主要用于接收前端的请求,从neo4j数据库获取数据,并将数据传给前端服务器展示。这里只展示ECharts的用法以及自己的一些总结,为以后打下一个基础。首先展示下效果图:
EChart.js下载:https://www.echartsjs.com/download.html
点击进入下载页面后,选择一个版本,点击进入相应的github地址,在点击source code(zip),下载完成后将其解压,在dist文件夹下又一个echarts.js文件,这个文件就是我们需要的,将其复制到项目中相应的js目录中即可。
由于这里采用的布局是force,不是通过坐标进行节点的生成,所以不能参考网上其他的采用坐标的节点拖拽。对此百度了许多,最后找到了一种合适的方法。我们需要对源码进行一些修改,只需要注释掉一行代码即可进行拖拽,这个方法主要参考另一位作者:https://blog.csdn.net/qq_38880340/article/details/85683322。打开源码,大概在1317行,将下图所示注释掉之后,选择保存即可。
我们在echarts.js同级目录下新建一个html文件,在head中引入echarts.js,在body中设置一个div,一定要设置这个div的宽和高,不然不会显示出关系图,完整代码如下所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Echarts测试</title>
<script src="echarts.js"></script>
</head>
<body>
<div id="chart1" style="width: 1600px; height: 900px; margin: 0 auto; border: 2px solid #FF0000;"></div>
</body>
<script type="text/javascript">
var entityRelation = [{"r": {"name": "推荐食谱"}, "m": {"name": "绿豆薏米饭"}}, {"r": {"name": "推荐食谱"}, "m": {"name": "姜丝萝卜汤"}},
{"r": {"name": "推荐食谱"}, "m": {"name": "葱蒜粥"}}, {"r": {"name": "推荐食谱"}, "m": {"name": "薏米莲子粥"}},
{"r": {"name": "推荐食谱"}, "m": {"name": "赤小豆粥"}}, {"r": {"name": "推荐食谱"}, "m": {"name": "香椿芽粥"}},
{"r": {"name": "推荐食谱"}, "m": {"name": "凉拌香椿"}}, {"r": {"name": "推荐食谱"}, "m": {"name": "醋熘土豆丝"}},
{"r": {"name": "忌吃"}, "m": {"name": "猪油(板油)"}}, {"r": {"name": "忌吃"}, "m": {"name": "咸鱼"}}, {"r": {"name": "忌吃"}, "m": {"name": "白扁豆"}},
{"r": {"name": "忌吃"}, "m": {"name": "油条"}}, {"r": {"name": "宜吃"}, "m": {"name": "芝麻"}}, {"r": {"name": "宜吃"}, "m": {"name": "鸡蛋"}},
{"r": {"name": "宜吃"}, "m": {"name": "南瓜子仁"}}, {"r": {"name": "宜吃"}, "m": {"name": "鹌鹑蛋"}}, {"r": {"name": "所属科室"}, "m": {"name": "呼吸内科"}},
{"r": {"name": "常用药品"}, "m": {"name": "感冒灵颗粒"}}, {"r": {"name": "常用药品"}, "m": {"name": "利巴韦林颗粒"}}, {"r": {"name": "好评药品"}, "m": {"name": "伤风停胶囊"}},
{"r": {"name": "好评药品"}, "m": {"name": "感冒灵颗粒"}}, {"r": {"name": "好评药品"}, "m": {"name": "喉痛灵片"}}, {"r": {"name": "好评药品"}, "m": {"name": "阿莫西林颗粒"}},
{"r": {"name": "好评药品"}, "m": {"name": "洛索洛芬钠胶囊"}}, {"r": {"name": "好评药品"}, "m": {"name": "酚咖片"}},
{"r": {"name": "好评药品"}, "m": {"name": "洛索洛芬钠片"}}, {"r": {"name": "好评药品"}, "m": {"name": "风油精"}}, {"r": {"name": "好评药品"}, "m": {"name": "匹多莫德分散片"}},
{"r": {"name": "好评药品"}, "m": {"name": "依托红霉素片"}}, {"r": {"name": "好评药品"}, "m": {"name": "穿心莲片"}}, {"r": {"name": "好评药品"}, "m": {"name": "头孢丙烯分散片"}},
{"r": {"name": "好评药品"}, "m": {"name": "麻黄止嗽丸"}}, {"r": {"name": "好评药品"}, "m": {"name": "肺宁片"}}, {"r": {"name": "好评药品"}, "m": {"name": "抗病毒口服液"}},
{"r": {"name": "好评药品"}, "m": {"name": "消炎片"}}, {"r": {"name": "好评药品"}, "m": {"name": "头孢拉定胶囊"}}, {"r": {"name": "好评药品"}, "m": {"name": "蒲公英颗粒"}},
{"r": {"name": "好评药品"}, "m": {"name": "银芩胶囊"}}, {"r": {"name": "好评药品"}, "m": {"name": "愈美胶囊"}}, {"r": {"name": "诊断检查"}, "m": {"name": "白细胞计数(WBC)"}},
{"r": {"name": "诊断检查"}, "m": {"name": "血常规"}}, {"r": {"name": "诊断检查"}, "m": {"name": "肺和胸膜听诊"}}, {"r": {"name": "诊断检查"}, "m": {"name": "尿常规"}},
{"r": {"name": "诊断检查"}, "m": {"name": "内科检查"}}, {"r": {"name": "症状"}, "m": {"name": "鼻塞"}}, {"r": {"name": "症状"}, "m": {"name": "头痛"}},
{"r": {"name": "症状"}, "m": {"name": "浑身忽冷忽热"}}, {"r": {"name": "症状"}, "m": {"name": "情绪性感冒"}}, {"r": {"name": "症状"}, "m": {"name": "咽喉干燥及灼热感"}},
{"r": {"name": "症状"}, "m": {"name": "发烧"}}, {"r": {"name": "症状"}, "m": {"name": "发热伴寒战"}}, {"r": {"name": "症状"}, "m": {"name": "咽痛"}},
{"r": {"name": "症状"}, "m": {"name": "流鼻涕"}}];
// entityRelation中的r代表的关系,m代表的是第二个实体,这个数据是从neo4j数据库查询后进行json格式化后的,原数据过多,就删减了一部分。
var key_word = '感冒';
var data = []; // echarts中的节点数组
var links = []; // echarts中边的数组
// 获取实体1, 也就是中心节点
var node = {} ;
node['name'] = key_word;
node['draggable'] = true; // 设置中心节点是否可以拖拽
var id = 0;
node['id'] = id.toString();
data.push(node);
// 获取实体2, 存储在data中, 如果实体2已经存在于data中, 则不push
var maxDisPlayNode = 16; // 对展示节点的数量进行限制
// console.log("实体长度----->", entityRelation.length); // 显示关系个数
for(var i = 0; i < Math.min(maxDisPlayNode, entityRelation.length); i++) {
node = {};
node['name'] = entityRelation[i]['m']['name'];
node['draggable'] = true;
if(entityRelation[i]['r']['name'] == '所属科室') {
node['category'] = 1; // 节点分类, 数字代表option.series.categories中的索引
} else if (entityRelation[i]['r']['name'] == '常用药品') {
node['category'] = 2;
} else if (entityRelation[i]['r']['name'] == '宜吃') {
node['category'] = 3;
} else if (entityRelation[i]['r']['name'] == '诊断检查') {
node['category'] = 4;
} else if (entityRelation[i]['r']['name'] == '忌吃') {
node['category'] = 5;
} else if (entityRelation[i]['r']['name'] == '好评药品') {
node['category'] = 6;
} else if (entityRelation[i]['r']['name'] == '推荐食谱') {
node['category'] = 7;
} else if (entityRelation[i]['r']['name'] == '症状') {
node['category'] = 8;
} else if (entityRelation[i]['r']['name'] == '并发症') {
node['category'] = 9;
} else if (entityRelation[i]['r']['name'] == '生产药品') {
node['category'] = 10;
}
id = i + 1;
node['id'] = id.toString();
var flag = 1;
relationTarget = id.toString();
for (var j = 0; j < data.length; j++) {
if (data[j]['name'] === node['name']) {
flag = 0;
relationTarget = data[j]['id'];
break;
}
}
relation = {};
relation['source'] = 0; // 关系起点, 数字代表的是节点的id, 即node[id]
relation['target'] = relationTarget; // 关系指向的终点, 也是id, 即通过节点的id描述两个节点以及关系
relation['category'] = 0;
if (flag === 1) {
data.push(node);
relation['value'] = entityRelation[i]['r']['name'];
relation['symbolSize'] = 10;
links.push(relation);
} else {
maxDisPlayNode += 1;
for (var k = 0; k < links.length; k++) {
if (links[k]['target'] === relationTarget) {
links[k]['value'] = links[k]['value'] + " | " + entityRelation[i]['r']['name'];
break;
}
}
}
}
// console.log("data====>>>", data);
// console.log("links====>>>", links);
// 初始化echarts实例
var myChart = echarts.init(document.getElementById("chart1"));
option = {
title: {
text: ''
},
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
label: {
normal: {
show: true,
textStyle: {
fontSize: 12
},
}
},
legend: {
x: "center",
show: false
},
series: [
{
type: 'graph',
layout: 'force',
symbolSize: 45,
focusNodeAdjacency: false, // 是否在鼠标移到节点上的时候突出显示节点以及节点的边和邻接节点, 默认是true
roam: true,
edgeSymbol: ['none', 'arrow'],
categories: [{
name: '查询实体',
itemStyle: {
normal: {
color: "#FF0000",
}
}
}, {
name: '所属科室',
itemStyle: {
normal: {
color: "#4592FF",
}
}
}, {
name: '常用药品',
itemStyle: {
normal: {
color: "#FFFF00",
}
}
}, {
name: '宜吃',
itemStyle: {
normal: {
color: "#66CD00",
}
}
}, {
name: '诊断检查',
itemStyle: {
normal: {
color: "#B8860B",
}
}
}, {
name: '忌吃',
itemStyle: {
normal: {
color: "#B23AEE",
}
}
}, {
name: '好评药品',
itemStyle: {
normal: {
color: "#FFB90F",
}
}
}, {
name: '推荐食谱',
itemStyle: {
normal: {
color: "#7FFF00",
}
}
},{
name: '症状',
itemStyle: {
normal: {
color: "#F08080",
}
}
}, {
name: '并发症',
itemStyle: {
normal: {
color: "#87CEFA",
}
}
}, {
name: '生产药品',
itemStyle: {
normal: {
color: "#FFFF00",
}
}
}],
label: {
normal: {
show: true,
textStyle: {
fontSize: 12,
fontStyle : 'normal',
fontWeight : 'bolder',
fontFamily : 'sans-serif',
},
}
},
force: {
repulsion: 1000,
edgeLength: [150, 100],
layoutAnimation : false
// 因为力引导布局会在多次迭代后才会稳定, 这个参数决定是否显示布局的迭代动画(节点数量过多, 图在迭代的过程中会旋转),
// 在浏览器端节点数据较多(>100)的时候不建议关闭, 布局过程会造成浏览器假死。
},
edgeSymbolSize: [4, 50],
edgeLabel: {
normal: {
show: true,
textStyle: {
fontSize: 10
},
formatter: "{c}" // 标签内容格式器。模板变量有 {a}、{b}、{c},分别表示系列名,数据名,数据值。
}
},
data: data, // 节点
links: links, // 边或者关系
lineStyle: {
normal: {
opacity: 0.5,
width: 1.0,
curveness: 0,
color: "#262626"
}
}
}
]
};
myChart.setOption(option); // 这步很重要, 必须设置才可以有效
myChart.on('click', function (param){
console.log('param---->', param); // 打印出param, 可以看到里边有很多参数可以使用
//获取节点点击的数组序号
var arrayIndex = param.dataIndex;
console.log('arrayIndex---->', arrayIndex);
console.log('name---->', param.name);
if (param.dataType == 'node') {
alert("点击了节点" + param.name)
} else {
alert("点击了边" + param.value)
}
});
</script>
</html>
其中的entityRelation即为我们从后台获得的经过处理后的数据(这里只是为了展示最后的关系图,所以直接将数据写在了这里),r 代表的关系,m 代表的是第二个实体,原数据过多,就删除了其中的一部分。key_word为第一个实体,也是中心节点,entityRelation中的 r 指的就是中心点key_word到第二个实体 m 的关系。
data[]是echart中所有的节点的数组,包括所有的实体,links[]即为实体之间所有关系的数组。每个节点都有id,name,category等参数,category主要用于节点(实体)的分类,不同种类的实体用不同的颜色区分出来,相关注释可以在代码中查看。relation主要描述的是起始节点和指向的结束节点,是links[]数组的参数。
echarts中的参数过多,可以参考官方文档,或者下载的source code中的例子,或者参考下边的文章,对echarts的参数进行了总结:http://www.cnblogs.com/koala2016/archive/2016/12/01/6123003.html。
点击事件:
对节点和边添加点击事件,主要通过click方法实现,代码可以参考上边给出的。可以通过将param打印出来查看里边的具体参数,更方便下一步的处理。