前言
以前逛掘金,都是白嫖别人的贴子,有啥不会就查啥。
今天例行白嫖,在网上搜neo4j可视化相关的小demo,嫖着嫖着,良心发现,思绪如尿崩。
要不我也写点什么吧!嗯,就这样决定了。
连篇的废话
初衷是毕业设计里面有一部分有涉及到neo4j前端可视化的东西,这我哪会啊??没关系,白嫖!
我在网上看了下,可行的方案还真不少!什么 D3、Echarts、G6等等等等。 选来选去,像TM挑妃子一样,挑花了眼......
不巧的是,刚好看到了一篇帖子,名字我已经忘记了,对不起了老哥。他使用的是Echarts来实现的,于是乎,仔细看了下,发现实现思路还挺简单的,代码的整个过程也不复杂。嗯,那就决定是你了!妙!蛙!种!子!......不好意思,是Echarts(微笑脸🙂)
阶段1
我寻思着该怎么把像neo4j一样的节点和边可视化到前端页面上呢??嗯,那我就造!先造几组数据呗。
这是我造的节点数据:嗯,你没看错,就是喜欢漫威嘛~
[
{ "name": "复仇者联盟3", "category": 0, "id": 0 },
{ "name": "美国队长", "category": 0, "id": 1 },
{ "name": "安东尼·罗素", "category": 3, "id": 2 },
{ "name": "科幻", "category": 2, "id": 3 },
{ "name": "剧情", "category": 2, "id": 4 },
{ "name": "克里斯·埃文", "category": 1, "id": 5 },
{ "name": "斯嘉丽·约翰逊", "category": 1, "id": 6 }
]
这是造的关系数据:
[
{ "source": 0, "target": 2, "category": 0, "value": "导演", "symbolSize": 5 },
{ "source": 1, "target": 2, "category": 0, "value": "导演", "symbolSize": 5 },
{ "source": 0, "target": 3, "category": 0, "value": "类型", "symbolSize": 5 },
{ "source": 1, "target": 3, "category": 0, "value": "类型", "symbolSize": 5 },
{ "source": 0, "target": 4, "category": 0, "value": "类型", "symbolSize": 5 },
{ "source": 1, "target": 4, "category": 0, "value": "类型", "symbolSize": 5 },
{ "source": 0, "target": 5, "category": 0, "value": "主演", "symbolSize": 5 },
{ "source": 1, "target": 5, "category": 0, "value": "主演", "symbolSize": 5 },
{ "source": 2, "target": 5, "category": 0, "value": "朋友", "symbolSize": 5 },
{ "source": 5, "target": 2, "category": 0, "value": "", "symbolSize": 5 },
{ "source": 0, "target": 6, "category": 0, "value": "主演", "symbolSize": 5 },
{ "source": 1, "target": 6, "category": 0, "value": "主演", "symbolSize": 5 },
{ "source": 2, "target": 6, "category": 0, "value": "朋友", "symbolSize": 5 },
{ "source": 6, "target": 2, "category": 0, "value": "", "symbolSize": 5 },
{ "source": 5, "target": 6, "category": 0, "value": "朋友", "symbolSize": 5 },
{ "source": 6, "target": 5, "category": 0, "value": "", "symbolSize": 5 }
]
我擦!造数据真不是人干的事儿,就这么两条,我都觉得累得慌。。。
我在白嫖的过程中,看到了一份代码,就是现在要提到的阶段1,如下:
// 所要读取文件的路径所组成的数组
const urls = ['./node.json', './links.json'];
const doSth = async () => {
const results = await Promise.all(urls.map(url => fetch(url).then(response => response.json())));
// promise.all返回的是所有成功的结果所组成的数组
let mydata = results[0];
let links = results[1];
// 获取所要操作的echarts的DOM
var myChart = echarts.init(document.getElementById('graph'));
// 图谱的配置项
option = {
// 提示框的配置
tooltip: {
formatter: x => {
return [x.data.name];//设置提示框的内容和格式 节点和边都显示name属性
}
},
animationDurationUpdate: 5000,
animationEasingUpdate: 'quarticlnOut', // quarticlnOut quinticInOut
//图形上的文本标签,可用于说明图形的一些数据信息
label: {
show: true,
textStyle: {
fontSize: 12
},
},
legend: {
x: "center",
show: true
},
series: [
{
type: 'graph',// 类型:关系图
layout: 'force',//图的布局,类型为力导图
symbolSize: 65,//节点大小
focusNodeAdjacency: true,//当鼠标移动到节点上,突出显示节点以及节点的边和邻接节点
draggable: true,//指示节点是否可以拖动
roam: true,
edgeSymbol: ['none', 'arrow'],
categories: [{
name: '电影',
itemStyle: {
color: "lightgreen"
}
}, {
name: '主演',
itemStyle: {
color: "orange",
}
}, {
name: '类型',
itemStyle: {
color: "pink",
}
}, {
name: '导演',
color: "lightblue",
}],
label: {
show: true,
textStyle: {
fontSize: 12,
color: "black",
}
},
force: {
repulsion: 4000,//节点之间的斥力因子。支持数组表达斥力范围,值越大斥力越大。
edgeLength: 80,//边的两个节点之间的距离
gravity: 0.1, //节点受到的向中心的引力因子。该值越大节点越往中心点靠拢。
},
edgeSymbolSize: [4, 50], // 边两端的标记大小,可以是一个数组分别指定两端,也可以是单个统一指定。
edgeLabel: {
show: true,
textStyle: {
fontSize: 10
},
formatter: "{c}"
},
data: mydata,
links: links,
lineStyle: {
opacity: 0.9,
width: 1.1,
curveness: 0,
color: "#262626",
}
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
}
doSth();
这个代码大致意思呢,就是通过请求来访问造的那两个文件中的数据,来通过实例化Echarts,配置Echarts的option,将我所造的节点和边绘制到DOM上。
看起来挺靠谱的,其实结果也还不错,雀食已经能可视化到页面上了,就是有亿点点丑。效果图如下:

可是有件事儿挺讨厌的,就是我不能拖动图中的节点,让它停留在任意位置。这样就烦了啊!总是一个鬼样子,看着难受。
然后咧,又是例行白嫖。看到了一个自定义位置的代码,赶紧嫖过来试试!真不愧为CV工程师(-_-)...
代码大概长下面这个样子:
//节点自定义拖拽不回弹
myChart.on('mouseup', function (params) {
var op = myChart.getOption();
op.series[0].data[params.dataIndex].x = params.event.offsetX;
op.series[0].data[params.dataIndex].y = params.event.offsetY;
op.series[0].data[params.dataIndex].fixed = true;
myChart.setOption(op);
});
好起来了呀!~可以随便拖成自己想要的形状了,嘻嘻。
阶段2
我寻思着不能总是自己造节点吧??一个两个还简单,这数据集什么的,动辄几千个节点,几万个关系(没错,我找的数据集就是TMDb5000,已经非常小了...),这要是靠自己造得造到什么时候去?
没错,给你们看一眼我neo4j图数据库中的数据规模,这样不像是自己能造的了得呀。。。

好吧,那就试试用JS来访问neo4j咯。
白嫖着,白嫖着,春天到了。噢,不!代码找到了!大概长下面这个样子:
var request = require("request");
var host = 'localhost',
port = 7474;
var httpUrlForTransaction = 'http://' + host + ':' + port + '/db/data/transaction/commit';
function runCypherQuery(query, params, callback) {
request.post({
uri: httpUrlForTransaction,
json: { statements: [{ statement: query, parameters: params }] }
},
function (err, res, body) {
callback(err, body);
})
}
runCypherQuery(
'MATCH (m:Movie { title: $name}) RETURN m', {
name: 'Hulk'
}, function (err, resp) {
if (err) {
console.log(err);
} else {
console.log(resp.results[0].data[0]);
}
});
上面的代码大概意思就是,先通过URL连接到本地的neo4j数据库,然后通过在JS里面编写Cypher,然后发送请求进行查询,然后取到请求结果进行处理,就是我这里的阶段2。
看了一眼结果,查询了下无敌浩克(Hulk),在node里弄的,结果这不就有了嘛!

成功查到了Hulk的节点!
可是我要怎么才能把查到的数据进行可视化展示呢?格式什么的也都不一样啊??
这不,阶段3来了嘛!
阶段3
既然阶段2中查询到的结果和阶段1中节点与关系的格式不一样,不一样那就转成一样的嘛!小意思~
不过既然前面的测试工作做好了,那就直接转Vue了,其实主要的内容差不多,就是稍微把请求到的数据转换成自己想要的格式嘛,光说不练假把式,那就直接上代码了(PS:不过css什么的 越写越乱,干脆不管它了,跟着感觉走就对了,烦!):
<script>
export default {
data() {
return {
// 输入框内容
input:'',
// 电影展示板电影名称
movieName: '',
// 电影展示板电影主演
actorList: '',
// 电影展示板电影tmdbID
idList: '',
// 电影展示板电影导演
directorList: '',
// 电影展示板电影类型
genresList: '',
// 绘制知识图谱的节点数据
echartsData: [],
// 绘制知识图谱的关系数据
echartsRelation: [],
// 下面五个是连接neo4j数据库的参数
protocol: 'neo4j',
host: 'localhost',
port: 7687,
username: 'neo4j',
password: 'neo4j',
// 防止出现多个echarts初始化的情况
myChart: ''
}
},
methods: {
// 绘制知识图谱
getGraph(p1, p2) {
// 若已经存在有初始化了的echarts实例,就直接进行绘制
if(this.myChart) {
this.configGraph(p1, p2);
}else {
// 没有初始化的echarts实例,就初始化一个
this.myChart = this.$echarts.init(this.$refs.graph);
this.configGraph(p1, p2);
}
},
// 连接到neo4j数据库
connect() {
return this.$neo4j.connect(this.protocol, this.host, this.port, this.username, this.password)
},
// 查询所搜索电影的cypher
searchGraph() {
// 连接到neo4j数据库
this.connect();
// 在查找前先清空电影展示板中的具体内容
this.actorList = '';
this.directorList = '';
this.idList = '';
this.genresList = '';
this.movieName = '';
this.echartsData = [];
this.echartsRelation = [];
this.movieName = this.input;
const session = this.$neo4j.getSession()
// 根据搜索框中的电影开始查找
// 返回的是一个promise
session.run(`MATCH (m:Movie{title:'${this.movieName}'}) -->(n) RETURN m,n`)
.then(res => {
let records = res.records;
// 若输入的电影不存在于数据集中,则进行提示
if(records.length == 0) {
this.$message.warning("您所搜索的电影不存在于数据集中!");
}else {
// 将电影节点先进行保存
this.echartsData.push({
name: `${this.movieName}`,
category: 0});
for (let i = 0; i < records.length; i++) {
if(records[i]._fields[1].properties.name) {
// 保存主演节点和关系
if(records[i]._fields[1].labels[0] == 'Actor') {
this.actorList += records[i]._fields[1].properties.name + ', ';
this.echartsData.push({
name: records[i]._fields[1].properties.name,
category: 1});
this.echartsRelation.push({
source: records[i]._fields[0].properties.title,
target: records[i]._fields[1].properties.name,
value: records[i]._fields[1].labels[0]});
}else {
// 保存导演节点和关系
this.directorList += records[i]._fields[1].properties.name;
this.echartsData.push({
name: records[i]._fields[1].properties.name,
category: 3});
this.echartsRelation.push({
source: records[i]._fields[0].properties.title,
target: records[i]._fields[1].properties.name,
value: records[i]._fields[1].labels[0]});
}
}else if(records[i]._fields[1].properties.id) {
// 保存tmdbID节点和关系
this.idList += records[i]._fields[1].properties.id;
this.echartsData.push({
name: records[i]._fields[1].properties.id,
category: 4});
this.echartsRelation.push({
source: records[i]._fields[0].properties.title,
target: records[i]._fields[1].properties.id,
value: records[i]._fields[1].labels[0]});
}else {
// 保存电影类型节点和关系
this.genresList += records[i]._fields[1].properties.title + ', ';
this.echartsData.push({
name: records[i]._fields[1].properties.title,
category: 2});
this.echartsRelation.push({
source: records[i]._fields[0].properties.title,
target: records[i]._fields[1].properties.title,
value: records[i]._fields[1].labels[0]});
}
}
}
// 对于查询到的数据,调用绘制函数进行绘制
this.getGraph(this.echartsData, this.echartsRelation);
})
.then(() => {
session.close()
})
},
// 绘制知识图谱的配置项
configGraph(p1, p2) {
// 保存传进来的节点和关系数据
let mydata = p1;
let links = p2;
// 图谱的配置项
let option = {
// 提示框的配置
tooltip: {
trigger: 'item'//设置提示框的内容和格式 节点和边都显示name属性
},
//图形上的文本标签,可用于说明图形的一些数据信息
label: {
fontSize: 12
},
legend: {
x: "center",
show: true
},
series: [
{
type: 'graph',// 类型:关系图
layout: 'force',//图的布局,类型为力导图
symbolSize: 50,//节点大小
emphasis: {
focus: 'adjacency'
},//当鼠标移动到节点上,突出显示节点以及节点的边和邻接节点
draggable: true,//指示节点是否可以拖动
roam: true,
// 两端的样式(无 / 箭头)
edgeSymbol: ['none', 'arrow'],
// 不同节点的颜色之类的配置
categories: [{
name: '电影',
itemStyle: {
color: "lightgreen"
}
}, {
name: '主演',
itemStyle: {
color: "orange",
}
}, {
name: '类型',
itemStyle: {
color: "pink",
}
}, {
name: '导演',
itemStyle: {
color: "lightblue",
}
},{
name: 'TMDbID',
itemStyle: {
color: "#fcce4c",
}
}],
// 节点上的文字
label: {
show: true,
fontSize: 12,
color: "black",
},
force: {
repulsion: 1200,//节点之间的斥力因子。支持数组表达斥力范围,值越大斥力越大。
gravity: 0.1, //节点受到的向中心的引力因子。该值越大节点越往中心点靠拢。
},
edgeSymbolSize: [4, 6], // 边两端的标记(箭头)大小,可以是一个数组分别指定两端,也可以是单个统一指定。
// 边上显示的文字
edgeLabel: {
show: true,
fontSize: 12,
formatter: "{c}"
},
// 节点数据
data: mydata,
// 关系数据
links: links,
// 边的样式
lineStyle: {
opacity: 0.9,
width: 1.1,
curveness: 0,
color: "#262626",
}
}
]
};
//节点自定义拖拽不回弹
const chart = this.myChart;
chart.on('mouseup', function (params) {
var option = chart.getOption();
option.series[0].data[params.dataIndex].x = params.event.offsetX;
option.series[0].data[params.dataIndex].y = params.event.offsetY;
option.series[0].data[params.dataIndex].fixed = true;
chart.setOption(option);
});
// 使用刚指定的配置项和数据显示图表。
this.myChart.setOption(option);
},
}
}
</script>
上面的代码大概意思就是,当点击查询按钮时会触发searchGraph事件,在searchGraph函数中先通过调用connect函数使得URL连接到本地的neo4j数据库,然后同阶段2中一样,通过在JS里面编写Cypher,然后发送请求进行查询,然后取到请求结果进行处理,绘制成知识图谱,就是我这里的阶段3。
来看下效果吧~

上面是我在neo4j数据库中查询的关于“Hulk”的相关信息。
使用同样的方法,可以在上面的搜索框中搜索你想要了解的电影,是不是很棒?

当然,电影图谱关系展示板中,也可以通过配置Echarts插件,来使用不同的功能,比如高亮显示,点击上面对应的颜色方框就只显示对应的颜色,鼠标悬浮在节点和边上面显示信息等等等等。
所以说Echarts的功能还是很强大的嘛!(嘻嘻嘻嘻)
PS:其实我看的其他的类似的库也可以很轻易实现这种类似的功能。比如说刚开始提到的G6也可以~
结语
好了,说了这么多,已经不太符合自己的性格了,可是性格这东西谁说的准呢,习惯白嫖的我,也许今天是想写下来,也许明天就又不想写了。
所以趁着今天的自己,赶紧把这些东西写出来发了吧,我怕我明天就懒得写了....(🙂🙂🙂)
大家再见~~~