Echarts与Neo4j可视化——记录不一样的想法

10,372 阅读8分钟

前言

以前逛掘金,都是白嫖别人的贴子,有啥不会就查啥。

今天例行白嫖,在网上搜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上。

看起来挺靠谱的,其实结果也还不错,雀食已经能可视化到页面上了,就是有亿点点丑。效果图如下:

image.png

可是有件事儿挺讨厌的,就是我不能拖动图中的节点,让它停留在任意位置。这样就烦了啊!总是一个鬼样子,看着难受。

然后咧,又是例行白嫖。看到了一个自定义位置的代码,赶紧嫖过来试试!真不愧为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图数据库中的数据规模,这样不像是自己能造的了得呀。。。

image.png

好吧,那就试试用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里弄的,结果这不就有了嘛!

image.png

成功查到了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。

来看下效果吧~ image.png

上面是我在neo4j数据库中查询的关于“Hulk”的相关信息。

使用同样的方法,可以在上面的搜索框中搜索你想要了解的电影,是不是很棒? image.png

当然,电影图谱关系展示板中,也可以通过配置Echarts插件,来使用不同的功能,比如高亮显示,点击上面对应的颜色方框就只显示对应的颜色,鼠标悬浮在节点和边上面显示信息等等等等。

所以说Echarts的功能还是很强大的嘛!(嘻嘻嘻嘻)

PS:其实我看的其他的类似的库也可以很轻易实现这种类似的功能。比如说刚开始提到的G6也可以~

结语

好了,说了这么多,已经不太符合自己的性格了,可是性格这东西谁说的准呢,习惯白嫖的我,也许今天是想写下来,也许明天就又不想写了。

所以趁着今天的自己,赶紧把这些东西写出来发了吧,我怕我明天就懒得写了....(🙂🙂🙂)

大家再见~~~